Is there a way to abstract over the result of usin...
# getting-started
r
Is there a way to abstract over the result of using a Delegated Property? Context - I've got a
Map<String, Any>
. I want to use a delegated property to extract a key and assign it to a
val
without repetition:
Copy code
val aMap: Map<String, Any> = mapOf(
    "foo" to "bar"
)
val foo: String by aMap // much nicer than `val foo: String = aMap["foo"] as String`
BUT, I actually want to wrap the value of
foo
into a class, while still using the property delegate to extract the value from the map... something like this:
Copy code
val aMap: Map<String, Any> = mapOf(
    "foo" to "bar"
)

data class Wrapper(val wrapped: String)

val foo: Wrapper by aMap // obviously can't compile, the value is a String not a Wrapper...
val foo = Wrapper(by aMap) // also can't compile...
val foo = (by aMap).let { Wrapper(it) } // you're just being silly now
Is this possible?
r
not sure if it matches your use case, but this works:
Copy code
val aMap: Map<String, Any> = mapOf(
    "foo" to "bar"
)

class Wrapper {
    val foo by aMap
}

val fooWrapper = Wrapper()

println(fooWrapper.foo) // prints "bar"
r
Wrapper
can't know about
aMap
directly
r
like this then?
Copy code
val aMap: Map<String, Any> = mapOf(
    "foo" to "bar"
)

class Wrapper(someMap: Map<String, Any>) {
    val foo by someMap
}

val fooWrapper = Wrapper(aMap)

println(fooWrapper.foo)
r
Sorry, again
Wrapper
needs to just take a single String or I'll have to make hundreds of different
Wrapper
classes
w
You can make
val foo: Wrapper by aMap
valid syntax..
👍 1
r
Thanks, yes, I think that's the way to go...
Needs a bit of work on pleasant error messages, but this is working:
Copy code
import kotlin.properties.ReadOnlyProperty

fun <V1, V2> Map<String, Any?>.transform(
    mapper: (V1) -> V2
): ReadOnlyProperty<Any, V2?> = ReadOnlyProperty { _, prop ->
    val rawValue = this[prop.name]
    if (rawValue == null) rawValue
    else mapper(rawValue as V1)
}

data class Wrapper(val wrapped: String)
data class IntWrapper(val wrapped: Int)

val aMap: Map<String, Any?> = mapOf(
    "foo" to "bar",
    "num" to 5,
    "nullable" to null,
)

val foo by aMap.transform(::Wrapper)
val num by aMap.transform(::IntWrapper)
val nullable by aMap.transform(::IntWrapper)
d
Can't the Map receiver be defined as
Map<String, V1?>
? That way you could avoid the cast.
Sorry, I see now that not because you indeed have a "generic" mixed-type map.
r
Yes, it's basically json
d
And I assume you don't know the structure in advance, right?
r
I'm looking to use this mechanism to transform from the untyped json to my knowledge of the type. Sort of thing that Jackson and co do but without the reflection.
d
I see, because that's exactly what I'm doing in a similar use case. Eg. I define a data class corresponding to the json structure and then simply call
objectMapper.convertValue
. Your imperative of avoiding reflection is due to performance?
r
No, 23 years of JVM development have left me with a slightly idiosyncratic hatred of all reflection / annotation driven libraries & frameworks because of the pain they have caused me over the years.
d
Good to hear. Because I'm now just 3 years on JVM (mostly Kotlin) and from what I see around me (the standard web ecosystem) I almost can't imagine a world without annotations and reflection. Probably just didn't burn myself enough yet 🙂
r
I've had to accept over the years that I'm pretty unusual in this respect...
d
By the way, how would you apply your approach to the case when the input json/map is nested?
Hmm, you would probably just create a generic
MapWrapper
and then repeat the same process on it.
w
I would always advocate against using reflection, and annotations are often abused. But I do think that well crafted libraries such as
kotlinx.serialization
's JsonSerialization are a very powerful tool and have very little tradeoff when it comes to performance as well as clarity of exceptions and documentation. And my favourite feature is that they are simple data classes that can easily be used for tests. I'm curious to hear the reason to do this manually 🙂
r
This is kind of where I'm going:
As to why... well, it's just a toy at the moment, interested to see what was possible, I'm not (yet!) advocating for it as an approach. But: 1. I really like the fact that I can do arbitrary transformations with a generic, non-library specific call like this:
Copy code
fun JsonObject.uuid() = ReadOnlyProperty<Any, UUID> { _, prop ->
    UUID.fromString(string(prop.name))
}
I've got a String, I need a UUID, so I call
UUID.fromString
. I don't need to know special annotations or how to register converters. I don't need compiler plugins. If something I didn't expect happens it's absolutely trivial to drop in a breakpoint and see what's going on. 2. It's non-destructive - I don't throw away the JSON keys I didn't know about, I just expose the ones I did Negatives: 1. I haven't worked out nice error handling. Jackson at least is pretty good at telling you where in the JSON structure it is. 2. It would need work to make it less memory inefficient - pretty sure most JSON parsers convert to objects on the fly, rather than reading the whole JSON document into memory as untyped objects and then converting it into domain relevant objects. I suspect something could be done here. 3. The resulting objects are (at the moment) hard to construct new instances from - much harder than a data class instance with its copy method. Again I have a vague notion that there are ways to improve that, but I haven't worked them out... 4. Needs some thought around null handling / missing keys - apart from anything else it's always bothered me the way that json parsers tend to elide the absence of a key in an object and its presence but with value null, they aren't the same thing... one of the few cases where I quite like `Option`/`Optional` because it's nestable, a Scala val with type
Option[Option[String]]
can have values
Some(Some(value))
,
Some(None)
and
None
which is quite a nice way to capture this. I guess in Kotlin you could make it
Optional<String?>?
...
w
Ah cool experiment! Fun fact, there are languages that have
?
syntax for
Optional
and also allow nested chaining. In Swift
Int?? != Int?
👍 1