Kotlin property delegation not working as expected - kotlin

I'm confused about the different behaviour depending whether I use getters or delegated properties. Consider the following:
class Test {
class Parts(val a: String, val b: String)
var raw = ""
private var cachedParts: Parts? = null
val parts: Parts
get() {
println("#2")
return cachedParts
?: raw.split("/")
.let { Parts(it.getOrElse(0) { "" }, it.getOrElse(1) { "" }) }
.also { cachedParts = it }
}
// WITH GETTERS:
val partA get() = parts.a
val partB get() = parts.b
}
fun main() {
val t = Test()
println("#1")
t.raw = "one/two"
println("a=${t.partA}, b=${t.partB}")
}
This code splits the string raw into two parts the first time parts is accessed. All later calls to parts will return the cached parts, even if raw changes. Output:
#1
#2
#2
a=one, b=two
The value of raw is empty when Test is created, but the accessors aren't called until we've set raw to some string. When partA and partB are finally accessed, they contain the correct value.
If I use property delegation instead, the code no longer works:
class Test {
class Parts(val a: String, val b: String)
var raw = ""
private var cachedParts: Parts? = null
val parts: Parts
get() {
println("#2")
return cachedParts
?: raw.split("/")
.let { Parts(it.getOrElse(0) { "" }, it.getOrElse(1) { "" }) }
.also { cachedParts = it }
}
// WITH DELEGATION:
val partA by parts::a
val partB by parts::b
}
fun main() {
val t = Test()
println("#1")
t.raw = "one/two"
println("a=${t.partA}, b=${t.partB}")
}
All I've changed here is that partA is now delegated to parts::a, and the same for partB. For some strange reason, partA and partB are now accessed before the value of raw is set, so cachedParts is initilized with two empty parts. Output:
#2
#2
#1
a=, b=
Can someone explain what is going on here?

See what your delegated properties translate to in the documentation here. For example, partA translates to:
private val partADelegate = parts::a
val partA: String
get() = partADelegate.getValue(this, this::partA)
Notice that the callable reference expression part::a is used to initialise partADelegate. This expression is evaluated when the instance of Test is created, before println("#1").
To evaluate parts::a, parts must be first evaluated. After all, this is a reference to the a property of parts, not a reference to parts.
Therefore, parts ends up being evaluated before raw gets its value.

Related

Creating an object builder with error handling using Arrow - Pattern match multiple Eithers

I have class A:
class A (private var z: String, private var y: String, private var x: Int)
I want to create a failsafe builder for it. The builder should return Either the list of Exceptions (e.g. when values are missing) or the created values. What is the recommended way to create something like this? Or is there a conceptually better approach?
My own approach to it:
sealed class ABuilderException {
object MissingXValue : ABuilderException()
object MissingYValue : ABuilderException()
object MissingZValue : ABuilderException()
}
import arrow.core.Either
import arrow.core.Option
import arrow.core.none
import arrow.core.some
class ABuilder {
private var x : Option<Int> = none()
private var y : Option<String> = none()
private var z : Option<String> = none()
fun withX(x : Int) : ABuilder {
this.x = x.some();
return this;
}
fun withY(y : String) : ABuilder {
this.y = y.some();
return this;
}
fun withZ(z : String) : ABuilder {
this.z = z.some();
return this;
}
fun build() : Either<A, List<ABuilderException>> {
var xEither = x.toEither { ABuilderException.MissingXValue }
var yEither = y.toEither { ABuilderException.MissingYValue }
var zEither = z.toEither { ABuilderException.MissingZValue }
// If all values are not an exception, create A
// otherwise: Return the list of exceptions
}
}
How could I best complete the build code?
I favor a solution that avoids deep nesting (e.g. orElse or similar methods) and avoids repeating values (e.g. by recreating Tuples), because this may lead to typos and makes it harder to add/remove properties later.
First you need to change the signature of build to:
fun build() : Either<List<ABuilderException>, A>
The reason for doing that is because Either is right biased - functions like map, flatMap etc operate on the Right value and are no-op in case the value is Left.
For combining Either values you can use zip:
val e1 = 2.right()
val e2 = 3.right()
// By default it gives you a `Pair` of the two
val c1 = e1.zip(e2) // Either.Right((2, 3))
// Or you can pass a custom combine function
val c2 = e1.zip(e2) { two, three -> two + three } // Either.Right(5)
However there is an issue here, in case of an error (one of them is Left) it will fail fast and give you only the first one.
To accumulate the errors we can use Validated:
val x = none<Int>()
val y = none<String>()
val z = none<String>()
// Validated<String, Int>
val xa = Validated.fromOption(x) { "X is missing" }
// Validated<String, String>
val ya = Validated.fromOption(y) { "Y is missing" }
// Validated<String, String>
val za = Validated.fromOption(z) { "Z is missing" }
xa.toValidatedNel().zip(
ya.toValidatedNel(),
za.toValidatedNel()
) { x, y, z -> TODO() }
Validated, like Either has a zip function for combining values. The difference is that Validated will accumulate the errors. In the lambda you have access to the valid values (Int, String, String) and you can create your valid object.
toValidatedNel() here converts from Validated<String, String> to Validated<Nel<String>, String> where Nel is a list that can NOT be empty. Accumulating errors as a List is common so it's built in.
For more you can check the Error Handling tutorial in the docs.

Kotlin combine lists based on common property

I have two functions (GetPodsOne and GetPodsTwo) that return me a big csv string. I then do some processing to discard the part of the string I don't want. See snippet below.
var podValues = execGetPodsOne()
val testPodValuesLst: List<String> = podValues.split(",").map { it -> it.substringAfterLast("/") }
testPodValuesLst.forEach { it ->
 println("value from testPodList=$it")
 }
podValues = execGetPodsTwo()
val sitPodValuesLst: List<String> = podValues.split(",").map { it -> it.substringAfterLast("/") }
sitPodValuesLst.forEach { it ->
 println("value from sitPodList=$it")
 }
This leaves me with two lists. See output of the above below:
value from testPodList=api-car-v1:0.0.118
value from testPodList=api-dog-v1:0.0.11
value from testPodList=api-plane-v1:0.0.36
value from sitPodList=api-car-v1:0.0.119
value from sitPodList=api-dog-v1:0.0.12
value from sitPodList=api-plane-v1:0.0.37
What i would like to do is end up with the objects inside a data class like below:
data class ImageVersions(val apiName: String, val testPodVersion: String, val sitPodVersion: String)
api-car-v1, 0.0.118, 0.0.119
api-dog-v1, 0.0.11, 0.0.12
api-plane-v1, 0.0.36, 0.0.37
I've used test and sit above but I'm going to have maybe another 5 environments eventually. Looking for a nice way to get the versions for each api and easily combine into that ImageVersions data class.
thanks
Considering that you're going to have maybe another 5 environments eventually, I tried to write something that will scale well:
enum class Env { Test, Sit }
data class ImageVersions(val apiName: String, val versions: Map<Env, String?>)
fun String.getNameAndVersion() = substringBefore(':') to substringAfter(':')
fun getVersions(envMap: Map<Env, List<String>>): List<ImageVersions> {
val envApiNameMap = envMap.mapValues { it.value.associate(String::getNameAndVersion) }
val allApiNames = envApiNameMap.flatMap { it.value.keys }.distinct()
return allApiNames.map { apiName ->
ImageVersions(apiName, envApiNameMap.mapValues { it.value[apiName] })
}
}
Playground example
So instead of separate val testPodVersion: String, val sitPodVersion: String, here you have a map. Now the structure of ImageVersions always remains the same irrespective of how many environments you have.
getNameAndVersion is a helper function to extract apiName and version from the original string.
getVersions accepts a list of versions corresponding to each environment and returns a list of ImageVersions
envApiNameMap is same as envMap just that the list is now a map of apiName and its version.
allApiNames contains all the available apiNames from all environments.
Then for every apiName, we take all the versions of that apiName from all the environments.
In future, if your have another environment, just add it in the Env enum and pass an extra map entry in the envMap of getVersions. You need not modify this function every time you have a new environment.
How about this:
val testPodValuesMap = testPodValuesLst.associate { it.split(':').zipWithNext().single() }
val sitPodValuesMap = sitPodValuesLst.associate { it.split(':').zipWithNext().single() }
val mergedMap = (testPodValuesMap.keys + sitPodValuesMap.keys).associateWith { key ->
testPodValuesMap.getValue(key) to sitPodValuesMap.getValue(key)
}
val imageVersions = mergedMap.map { (k, v) -> ImageVersions(k, v.first, v.second) }
println(imageVersions.joinToString("\n"))
which prints
ImageVersions(apiName=api-car-v1, testPodVersion=0.0.118, sitPodVersion=0.0.119)
ImageVersions(apiName=api-dog-v1, testPodVersion=0.0.11, sitPodVersion=0.0.12)
ImageVersions(apiName=api-plane-v1, testPodVersion=0.0.36, sitPodVersion=0.0.37)
As a first step I would extract the apiNames from both lists:
val apiNames = list1.map { it.replace("value from ", "").split("[=:]".toRegex())[1] }
.plus(list2.map { it.replace("value from ", "").split("[=:]".toRegex())[1] })
.distinct()
Then I'd create the ImageVersions instances by looping over apiNames:
val result = apiNames
.map { apiName ->
ImageVersions(
apiName,
(list1.firstOrNull { it.contains(apiName) } ?: "").split(":")[1],
(list2.firstOrNull { it.contains(apiName) } ?: "").split(":")[1]
)
}
.toList()
The reason to first extract the apiNames is, that apiNames missing in one of the two lists will still end up in the final result.
Kotlin Playground

Map Key Values to Dataclass in Kotlin

how can I set properties of a dataclass by its name. For example, I have a raw HTTP GET response
propA=valueA
propB=valueB
and a data class in Kotlin
data class Test(var propA: String = "", var propB: String = ""){}
in my code i have an function that splits the response to a key value array
val test: Test = Test()
rawResp?.split('\n')?.forEach { item: String ->
run {
val keyValue = item.split('=')
TODO
}
}
In JavaScript I can do the following
response.split('\n').forEach(item => {
let keyValue = item.split('=');
this.test[keyValue[0]] = keyValue[1];
});
Is there a similar way in Kotlin?
You cannot readily do this in Kotlin the same way you would in JavaScript (unless you are prepared to handle reflection yourself), but there is a possibility of using a Kotlin feature called Delegated Properties (particularly, a use case Storing Properties in a Map of that feature).
Here is an example specific to code in your original question:
class Test(private val map: Map<String, String>) {
val propA: String by map
val propB: String by map
override fun toString() = "${javaClass.simpleName}(propA=$propA,propB=$propB)"
}
fun main() {
val rawResp: String? = """
propA=valueA
propB=valueB
""".trimIndent()
val props = rawResp?.split('\n')?.map { item ->
val (key, value) = item.split('=')
key to value
}?.toMap() ?: emptyMap()
val test = Test(props)
println("Property 'propA' of test is: ${test.propA}")
println("Or using toString: $test")
}
This outputs:
Property 'propA' of test is: valueA
Or using toString: Test(propA=valueA,propB=valueB)
Unfortunately, you cannot use data classes with property delegation the way you would expect, so you have to 'pay the price' and define the overridden methods (toString, equals, hashCode) on your own if you need them.
By the question, it was not clear for me if each line represents a Test instance or not. So
If not.
fun parse(rawResp: String): Test = rawResp.split("\n").flatMap { it.split("=") }.let { Test(it[0], it[1]) }
If yes.
fun parse(rawResp: String): List<Test> = rawResp.split("\n").map { it.split("=") }.map { Test(it[0], it[1]) }
For null safe alternative you can use nullableString.orEmpty()...

Kotlin Data class copy extension

I am trying to find a solution for a nice kotlin data class solution. I have already this:
data class Object(
var classMember: Boolean,
var otherClassMember: Boolean,
var example: Int = 0) {
fun set(block: Object.() -> kotlin.Unit): Object {
val copiedObject = this.copy()
copiedObject.apply {
block()
}
return copiedObject
}
fun touch(block: Object.() -> kotlin.Unit): Object {
return this.set {
classMember = true
otherClassMember = false
block() }
}
}
val test = Object(true,true,1)
val changedTest = test.touch { example = 2 }
the result of this method is that the changedTest object has classMember = true, otherClassMember = false and example = 2
The problem with this solution is, the class properties are not immutable with var declaration. Does somebody have an idea how to optimize my methods to change var to val?
val says that a variable can't change it's value after initialization at the definition point. Kotlin's generated copy method does not modify an existing copy after construction: this method actually uses retrieved values from an object, replaces these values with ones that provided in copy method (if any), and after that just constructs a new object using these values.
So, it is not possible to perform such an optimization if you are going to change object's state after construction.
If I understood what you want correctly, you can do
data class Object(
val classMember: Boolean,
val otherClassMember: Boolean,
val example: Int = 0) {
fun touch(example: Int = this.example): Object {
return copy(
classMember = true,
otherClassMember = false,
example = example)
}
}
val test = Object(true,true,1)
val changedTest = test.touch(example = 2)
Though you need to repeat parameters other than classMember and otherClassMember but without reflection you can't do better.

Kotlin: Maps comparison throws Fail even if Maps are equal

I'm trying to compare 2 Maps, both are filled with identical Strings (just for training).
When I try to compare them I got false returned through map.equals(map2)
But if I do the same and add .toString() to the comparison it returns true. map.toString().equals(map2.toString())
How's that? What am I doing wrong?
Also happens the same with Kluent library and its shouldEqual method.
I also tried to get values of that maps into arrays and tried to compare that arrays but it still returns false.
Also, when I print all items from each map I see the same 3 Strings but comparison returns fail.
Here's the code:
class MapsTesting {
class Items(name1: String, amount1: String, price1: String) {
var name: String = name1
var amount: String = amount1
var price: String = price1
override fun toString(): String {
return "name:$name,amount:$amount,price:$price \n"
}
}
#Test
fun mapTest() {
val mapOfItems: MutableMap<String, Items> = mutableMapOf()
mapOfItems["Materials"] = Items("STEP 1", "STEP 2", "View All Stones")
val mapOfItems2: MutableMap<String, Items> = mutableMapOf()
mapOfItems2["Materials"] = Items("STEP 1", "STEP 2", "View All Stones")
mapOfItems.forEach { t, u ->
println("map key:$t,map value:${u.toString()}")
}
mapOfItems2.forEach { t, u ->
println("map key:$t,map value:${u.toString()}")
}
val comparison : Boolean = mapOfItems.toString().equals(mapOfItems2.toString())
//this returns true
val comparison2 : Boolean = mapOfItems.equals(mapOfItems2)
//this returns false
println(comparison)
println(comparison2)
val map1 = mapOfItems.toString()
val map2 = mapOfItems2.toString()
//this returns true
//if I delete toString() it will return false
map1 shouldEqual map2
}
}
Here's the error I got:
java.lang.AssertionError: expected: java.util.LinkedHashMap<{Materials=name:STEP 1,amount:STEP 2,price:View All Stones
}> but was: java.util.LinkedHashMap<{Materials=name:STEP 1,amount:STEP 2,price:View All Stones
}>
When comparing two maps with the equals method, the standard implementation compares all keys and values also using the equals method.
Your Items class uses the standard equals implementation (two reference values are equal if and only if they refer to the same object). But you clearly have two different instances of the in different maps.
You either have to override the Items#equals method or mark the Items class as a data class:
data class Items(var name1: String, var amount1: String, var price1: String)
Kotlin will generate correct equals, hashCode and toString methods.
After this the map comparison will work fine.