Search code examples
kotlingenericsarrow-kt

Why does `EffectScope.shift` need the type parameter `B`?


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 shifts 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.


Solution

  • 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
    }