The move to the new continuations API in Arrow brought with it a handy new function: shift
, in theory letting me get rid of ensure(false) { NewError() }
or NewError().left().bind()
constructs.
But I'm not sure how to properly use it. The documentation states that it is intended to short-circuit the continuation, and there are no conditionals, so it should always take the parameter, and (in either parlance) "make it a left value", and exit the scope.
So what is the type parameter B
intended to be used for? It determines the return type of shift
, but shift
will not return. Given no more context, B
can not be inferred, leading to this kind of code:
val res = either {
val intermediate = mayReturnNull()
if (intermediate == null) {
shift<Nothing>(IntermediateWasNull())
}
process(intermediate)
}
Note the <Nothing>
(and ignore the contrived example, the main point is that shift
s return type can not be inferred – the actual type parameter does not even matter).
I could wrap shift
like this:
suspend fun <L> EffectScope<L>.fail(left: L): Nothing = shift(left)
But I feel like that is missing the point. Any explanations/hints would be greatly appreciated.
CodePudding user response:
That is a great question!
This is more a matter of style, ideally we'd have both but they conflict so we cannot have both APIs available.
So shift
always returns Nothing
in its implementation, and so the B
parameter is completely artificial.
This is something that is true for a lot of other things in Kotlin, such as object EmptyList : List<Nothing>
. The Kotlin Std however exposes it as fun <A> emptyList(): List<A> = EmptyList
.
For Arrow to stay consistent with APIs found in Kotlin Std, and to remain as Kotlin idiomatic as possible we also require a type argument just like emptyList
. This has been up for discussion multiple times, and the Kotlin languages authors have stated that it was decided too explicitly require A
for emptyList
since that results in the best and most consistent ergonomics in Kotlin.
In the example you shared I would however recommend using ensureNotNull
which will also smart-cast intermediate
to non-null
.
Arrow attempts to build the DSL so that you don't need to rely on shift
in most cases, and you should prefer ensure
and ensureNotNull
when possible.
val res = either {
val intermediate = mayReturnNull()
ensureNotNull(intermediate) { IntermediateWasNull() }
process(intermediate) // <-- smart casted to non-null
}