https://kotlinlang.org logo
#arrow-contributors
Title
# arrow-contributors
y

Youssef Shoaib [MOD]

11/30/2023, 10:27 PM
I've been thinking about the need for a
transform
lambda for top-level fold for raise. I think perhaps top-level fold shouldn't exist, instead being replaced by
recover
, which describes the intent much more clearly. I've elaborated further in this PR Regardless, putting this into actual code, I tried implementing this against Arrow 2, but it's failing a test that seems to suggest an interesting design choice of Effect. More info in thread...
So, I implemented Effect.fold as follows (contract removed for brevity):
Copy code
public suspend fun <Error, A, B> Effect<Error, A>.fold(
  catch: suspend (throwable: Throwable) -> B,
  recover: suspend (error: Error) -> B,
  transform: suspend (value: A) -> B,
): B {
  return recover({ transform(invoke()) }, { recover(it) }, { catch(it) })
}
This passes all the tests, except one, which reveals an interesting implementation detail of top-level fold that I didn't consider before. The failing test is:
Copy code
@Test fun shiftLeakedResultsInRaiseLeakException() = runTest {
  effect {
    suspend { raise("failure") }
  }.fold(
    {
      it.message shouldStartWith "raise or bind was called outside of its DSL scope"
    },
    { unreachable() }) { f -> f() }
}
This fails because, without a pure transformation parameter being supported in top-level fold and recover, the transformation always happens before the raise scope is finished, and hence our effect actually executes like:
Copy code
effect {
  suspend { raise("failure") }.invoke()
}
To put this into code, top-level fold in the happy path currently does:
Copy code
val res = block(raise)
raise.complete()
transform(res)
where it completes the raise before it transforms the result. Without a transformation lambda, top level fold can only do:
Copy code
block(raise).also { raise.complete() }
thus executing any transformation before the raise is completed
My question is, is this limitation intentional? and more importantly, does it make sense? I think semantically, users expect a transformation to work exactly like applying a function inside an effect, but this is a weird edge case. A raise instance being captured in a non-local scope is, IMO, an edge case, and so I think we can arbitrarily decide what happens when such an effect is folded over and that captured raise instance is executed. It seems that the documented behaviour right now is that it fails, but could an argument be made for it to succeed?
3 Views