Why does lazy delegation break contracts?
# getting-started
m
Why does lazy delegation break contracts?
removing lazy from the group correctly applies the non-null contract on the list
j
When you go through a delegate, the compiler cannot be sure that the value of
group
won't change between 2 subsequent calls
m
Since the
group
has a non trivial getter (it is calling a method on
Lazy
), the compiler cannot assume that the same value will be returned on every call.
j
As a small justification to the above statements, you could work around this just like for nullable vars using `let`:
Copy code
fun main() {
    val group: List<String>? by lazy { null }

    val result = group.let {
        when {
            !it.isNullOrEmpty() -> it
            else -> emptyList()
        }
    }.toSet() // no error here
}
But honestly in this specific case, I believe this should be sufficient:
Copy code
fun main() {
    val group: List<String>? by lazy { null }

    val result = group?.toSet() ?: emptySet()
}
👍 1
m
Could also use
val result = group?.toSet().orEmpty()
👍 1
m
the original example had multiple lazy lists like so
which doesn't let (lol) itself to using 'let'
m
listOfNotNull(group, group2, group3).firstOrNull()?.toSet().orEmpty()
j
Then you could maybe build a list of those lists, and find the first non-null and non-empty one with standard collection operations instead of writing a big
when
expression like this. For instance:
Copy code
fun main() {
    val group1: List<String>? by lazy { null }
    val group2: List<String>? by lazy { null }
    val group3: List<String>? by lazy { null }

    val geldigePrestaties = listOf(group1, group2, group3).firstOrNull { !it.isNullOrEmpty() }?.toSet().orEmpty()
}
@mkrussel haha I guess we keep giving similar advice 😄 (although your version here will take the first non-null list, while OP wants a non-empty one as well)
m
Yeah, I missed that
m
sadly; building a list out of lazy delegations calls the lazy function whilst building the list of
j
You could also build a sequence of them instead:
Copy code
fun main() {
    val group1: List<String>? by lazy { null }
    val group2: List<String>? by lazy { null }
    val group3: List<String>? by lazy { null }

    val groups = sequence { 
        yield(group1)
        yield(group2)
        yield(group3)
    }
    val geldigePrestaties = groups.firstOrNull { !it.isNullOrEmpty() }?.toSet().orEmpty()
}
m
That would indeed work
m
I also came up with this. ``
Copy code
val geldigePrestaties: Set<String> = (group.takeUnless { it.isNullOrEmpty() }
            ?: group2.takeUnless { it.isNullOrEmpty() }
            ?: group3.takeUnless { it.isNullOrEmpty() }
                )?.toSet().orEmpty()
j
@mkrussel which could work but at this point I'd either extract the duplicate code into a function, or rework the lazy delegate bodies so they return null instead of empty lists (if the rest of the program doesn't need this subtle difference)
m
Thanks for your feedback guys; sadly I don't think the solution becomes much cleaner this way. I just wish there was a way to convince the compiler that a lazy call is a special delegate where the value doesn't change
j
I do prefer a list/sequence approach to a
when
-based approach here, because semantically you're considering these groups as a collection of things that you're searching. All branches of the
when
are the same because of the semantics of what you're doing, that's why I personally find it cleaner to have this "search for a good group" apparent as a search through a collection/sequence.
So even if the compiler understood
lazy
in a special way (which could indeed be nice!), I would still prefer the sequence approach here.
e
I prefer
Copy code
group1?.ifEmpty { null }
    ?: group2?.ifEmpty { null }
    ?: group3?.ifEmpty { null }
    ?: emptyList()