https://kotlinlang.org logo
Title
t

than_

03/31/2023, 12:02 PM
Hi, not sure if this is the best place for this discussion, but it's tangentially related to Arrow, so I hope it's ok. For a while now I'm thinking about a unified way to design functions that can fail. I came up with basically copying
Either::getOrElse
. The behaviour I like there is being able to affect the return type in the recover block.
val a: Either<Something, Int>
val r1: Int = a.getOrElse{ 0 }
val r2: Int = a.getOrElse { throw ... }
val r3: Int = a.getOrElse { raise(...) }
val r4: Int? = a.getOrElse { null }
While playing around I came up with:
inline fun <ERR, A, B : BB, BB> A.toB(
    transform: Raise<ERR>.(A) -> B,
    recover: (ERR) -> BB,
) = fold({ transform(this@toB) }, { recover(it) }, ::identity)
So now I can do (just as example):
val r1: Long? = "abc".toB({
    it.toLongOrNull() ?: raise(Unit)
}) {
    null
}
The behaviour is correct but it'd be nice to wrap it in a function
String::toLongOrElse
The issue is how would you implement that?
inline fun <T: Long?> String.toLongOrElse(recover: () -> T): T =
    this.toB({ 
        it.toLongOrNull() ?: raise(Unit)
    }) {
        recover()
    }
This doesn't really work since the transform requires to return
T
but returns`Long`
s

simon.vergauwen

03/31/2023, 12:05 PM
I think you're missing a generic constraint in
toB
for this to work 🤔
Actually, why
T : Long?
🤔 It can only be
Long?
so no generic is needed.
Ah, you want it to be nullable if
recover
returns a nullable but non-null if there is a non-null
Long
returned from
recover
? 🤔
t

than_

03/31/2023, 12:10 PM
toB
when used on it's own works well. It infers common parent from return types of transform and recover. The issue is wrapping it in a function for a concrete type. I'm able to implement
Either::getOrElse
with it without issues:
fun <ERR, B : BB, BB> Either<ERR, B>.getOrElse(recover: (ERR) -> BB) = toB({
    it.fold(
        ifLeft = { raise(it) },
        ifRight = ::identity
    )
}) {
    recover(it)
}
Yes. I want it to infer common parent and use it as a return type.
TBH I don't really see what's blocking the inference. Since I can make it work when directly calling
toB
and even when implementing the
getOrElse
the inference is possible, (at least in some cases).
s

simon.vergauwen

03/31/2023, 12:34 PM
Hehe, this is a tricky thing. The compiler knows that
T : Long?
but not that
Long
is intersects with
T
in a generic environment, but when doing it in a "concrete" environment it can infer the upper bound correctly.
t

than_

03/31/2023, 12:54 PM
My understanding was that the
Long?
extends
Long
. Am I not correct? You can assign
Long
to
Long?
but not vice versa so in that case compiler can infer the relationship.
s

simon.vergauwen

03/31/2023, 1:00 PM
Yes,
Long
extends
Long?
but
Long
is not perse
T : Long?
since
T
can be a different subtype
So either of this is valid:
inline fun <T: Long?> String.toLongOrElse(recover: () -> T): Long? =
    this.toB({ 
        it.toLongOrNull() ?: raise(Unit)
    }) {
        recover()
    }
t

than_

04/01/2023, 12:29 PM
Thanks, makes sense. So basically the compiler cannot infer that there is only one possible subtype (understandably). But in the concrete environment it operates on the generics without bounds, so it infers it correctly. I guess I have to wait for union types (fingers crossed) to get this to work (if there's gonna be common parent inference).
s

simon.vergauwen

04/01/2023, 5:50 PM
My pleasure ☺️ Yes, that is exactly what is going on. I've ran into this a couple of times, some other languages can do this correctly but it's quite tricky.