https://kotlinlang.org logo
Title
r

Rob Elliot

02/03/2023, 11:25 AM
s

Stephan Schröder

02/05/2023, 8:43 AM
interesting, but why
properties.apply { put(prop.name, initialValue) }
instead of the way easier to read
properties.put(prop.name, initialValue)
??
s

Sam

02/05/2023, 8:45 AM
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

Stephan Schröder

02/05/2023, 8:52 AM
ah, i overlooked that! probably because the return type of
property
is implicit 😆
r

Rob Elliot

02/05/2023, 1:33 PM
Relatedly the compiler can't prove that the properties have a subtype; in this class
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!
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

Sam

02/05/2023, 3:18 PM
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:
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

Rob Elliot

02/05/2023, 10:40 PM
Nice, that meets all my disappointments! I will update the post...
m

Matteo Mirk

03/14/2023, 2:19 PM
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

Rob Elliot

03/15/2023, 3:27 PM
I may not be understanding; which do you feel has more boilerplate?
m

Matteo Mirk

03/16/2023, 2:32 PM
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:
get(Symbol<T>): T
put(Symbol<T>, T): Unit
r

Rob Elliot

03/16/2023, 2:56 PM
The solution we came up with looks like this:
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

Matteo Mirk

03/16/2023, 3:06 PM
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?