Cool (I think!) little trick <@U030S9809PH> showed...
# feed
r
👌 2
K 1
s
interesting, but why
Copy code
properties.apply { put(prop.name, initialValue) }
instead of the way easier to read
Copy code
properties.put(prop.name, initialValue)
??
s
The property delegate provider also has to return the map, because the map is what's actually being used as the property delegate.
apply
lets you modify and return it in a single line. As usual there's definitely a trade-off between succinctness and readability though 👍 two lines might be easier to understand
s
ah, i overlooked that! probably because the return type of
property
is implicit 😆
r
Relatedly the compiler can't prove that the properties have a subtype; in this class
Copy code
class Props : AbstractPropertyMap<Any>() {
  val anInt by property(1)
}
anInt
has compile time type
Any
. I couldn't find a way to make its type defined by the type of the argument to
property
.
Bizarrely you can just declare it - but the compiler is not actually checking it!
Copy code
class Props : AbstractPropertyMap<Any>() {
    val notAnInt: Int by property("a string")
}
compiles (I don't understand why!
properties.getValue("a string")
returns
Any
...) but fails at runtime with the inevitable
ClassCastException
when you dereference
notAnInt
. Oddly you can call
Props().entries
happily, and it will contain
"notAnInt" to "a string"
so the type of
notAnInt
must be late bound despite the
by property
being evaluated on construction.
(It's also a shame that the type parameter
V
on
AbstractPropertyMap
has to be invariant despite
AbstractPropertyMap
being effectively immutable, because
V
is both a parameter to
protected fun property
and a return type. It would have been nice to make it
out V
as from a client's perspective it could be.)
And I've just discovered
@UnsafeVariance
🙂
s
Oh, nicely spotted! The property delegate provider always uses the
Map<String, V>
as the property delegate, meaning the property type is always just
V
, never a subtype. Not very useful if
V
is
Any
. I think it can be improved like this, though:
Copy code
abstract class AbstractPropertyMap<V>(
    private val properties: MutableMap<String, V> = mutableMapOf()
) : Map<String, V> by properties {
    protected fun <V2: V> property(initialValue: V2) =
        PropertyDelegateProvider<Any, ReadOnlyProperty<Any, V2>> { _, prop ->
            properties[prop.name] = initialValue
            ReadOnlyProperty(properties::getValue)
        }
}
Basically wraps up the
Map
as a property delegate that provides a specific type (
V2
) for whichever property it's currently trying to provide.
That was a lot of words.
I think it would still need your
@UnsafeVariance
to get the
out V
variance, which would make it
protected fun <V2: @UnsafeVariance V> property(initialValue: V2)
... and I've got to be honest, I'm not entirely sure what that means 😅
r
Nice, that meets all my disappointments! I will update the post...
m
Uhm, reminds me of the Heterogeneous Type Safe Container pattern, later evolved to SymbolMap, but with much more boilerplate. https://stevewedig.com/2014/08/28/type-safe-heterogenous-containers-in-java/
The usage of delegates is interesting, nonetheless!
r
I may not be understanding; which do you feel has more boilerplate?
m
At first I'd say your solution, since the THC/SymbolMap has a very short implementation, don't know if you're familiar with it or read the article. Anyway, I apologise in advance if I may have misunderstood what your solution accomplishes. If I got it right, yours will enable a class to act both as a Map and have typed properties delegated to it, with related checks. While a SymbolMap sure has less features, but is actually a map and lets you store and retrieve values in a type-safe way, that is:
Copy code
get(Symbol<T>): T
put(Symbol<T>, T): Unit
r
The solution we came up with looks like this:
Copy code
class Foo: AbstractPropertyMap<Any>() {
  val prop1 by property("value1")
  val prop2 by property(21)
}
The two things seem to have different goals to me; SymbolMap is about having a
Map<Symbol<Any>, Any>
where you can be sure you put and retrieve a specific type.
AbstractPropertyMap
is about having something which is both a
Map<String, T>
and an object such that the fields on the object are also entries in the Map with the same name and value.
Given
val symbolMap: SymbolMap = map().put($bool, boolValue).put($int, intValue).put($null, null).solid()
you cannot do `symbolMap.bool`; you'd have to wrap it in a class with a
val bool: Boolean = symbolMap[$bool]
field, wouldn't you? At which point you've repeated
bool
multiple times, which is what I was trying to avoid.
m
Watch out: SymbolMap doesn't work blindly for Any but for T, meaning it can store and retrieve different types in a safe way 🙂 But Yeah, I get that AbstractPropertyMap does more, that is providing object properties. You're right, to use a SymbolMap you need to pass around a symbol (which is immutable, constant or not), and you don't have compile time properties, but the trade-off is that it's both a dynamic and type-safe structure, while an AbstractPropertyMap derivate obviously has to declare its properties. Apologies again, I misunderstood the features of your solution, but it was a good discussion nonetheless.
Hey Rob, sorry to disturb again, but I had an epiphany, kind of. 😄 Your solution gave me a deja-vu when I first saw it, at first I thought it reminded me of SymbolMap, but I was actually wrong, as we saw. But finally a memory struck me and now I remember: your implementation is very close to the Abstract Document Pattern! Minus the traits, but I think this is what gave me that deja-vu, what do you think?