https://kotlinlang.org logo
#arrow
Title
# arrow
w

Wesley Hartford

02/29/2024, 12:13 AM
I'm working on upgrading from 1.2.1 to 1.2.2 and have encountered an error I've never seen before:
Copy code
java.lang.IllegalStateException: Returning a lazy computation or closure from 'fold' breaks the context scope, and may lead to leaked exceptions on later execution.
Make sure all calls to 'raise' and 'bind' occur within the lifecycle of nullable { }, either { } or similar builders.

See Arrow documentation on 'Typed errors' for further information.
Is this error new in version 1.2.2? I haven't changed any code other than the arrow upgrade. It seems like it's trying to tell me that I'm doing something like this:
Copy code
fun bad() = either {
  { calledLazily().bind() }
}
Which is bad because the
bind
may be called outside of the
either
scope it implicitly refers to. Unfortunately, I cannot see what part of my code is doing anything like that.
I've reduced my code to a trivial example which produces the error:
Copy code
fun main() {
  either<String, () -> String> { {"foo bar"} }
}
Running this main function results in the following:
Copy code
Exception in thread "main" java.lang.IllegalStateException: Returning a lazy computation or closure from 'fold' breaks the context scope, and may lead to leaked exceptions on later execution.
Make sure all calls to 'raise' and 'bind' occur within the lifecycle of nullable { }, either { } or similar builders.

See Arrow documentation on 'Typed errors' for further information.
	at com.ze.juicy.grpc.stub.TestKt.main(test.kt:32)
	at com.ze.juicy.grpc.stub.TestKt.main(test.kt)
It seems that any either block which returns a closure is causing this exception, is that intentional?
a

Alejandro Serrano.Mena

02/29/2024, 8:16 AM
this is intentional: this is a case of "captured context" which, in the case the returned function uses
raise
inside, would lead to problems
or like the
bind
you do here
the reasoning is the following: the
either
in that case would return a
Right
... but inside the lazy computation/lambda you may
raise
and then... what would you expect to happen? since this interaction is always problematic, and lead to hard-to-diagnose problems, we've decided to leave it out
s

simon.vergauwen

02/29/2024, 8:34 AM
Ouch.. I knew there were some people doing this, and we've had a chat about this before @Wesley Hartford 😅 If you really know what you're doing, it can be done but in general it's probably a bug. You can work around it though if you really need to. In general our "advise" will be to use
Raise
as a context (or extension) that way your lazy computation remains within the scope
Raise
. There is 2 checks that happen: 1. When the result is returned from
either
, (actually
fold
for
Raise
) we check (on best effort) if no lazily value is returned. 2. When you capture a lazy
bind
call (so leak
Raise
) and try to invoke it out of its scope then it'll throw an
IllegalStateException
. So, what you can do is the following:
However, be aware that in the lazy computation you still won't be able to call
bind
.
Copy code
data class MyLazyCode(val block: () -> Int)

either<String, MyLazyCode> {
  MyLazyCode {
    //bind is illegal here
  }
} // <-- returns Right(MyLazyCode)
a

Alejandro Serrano.Mena

02/29/2024, 8:36 AM
another way to remove the "training wheels" is to call
foldUnsafe
yourself
s

simon.vergauwen

02/29/2024, 8:37 AM
Oh right,
foldUnsafe
is there too. Forgot about that. 👍 You could define a custom
eitherUnsafe
as well.
w

Wesley Hartford

02/29/2024, 3:58 PM
Thanks both of you. I've got a pretty big code base of Kotlin with Arrow and I think there is only one place where I encountered this. I implemented a trivial work around and am good to go. I was doing two things inside the
either
block: validating some input, and building a function using that input. Only the validation portion made use of the
Raise
, so I moved the creation of the resulting function out of the
either
block.
🙌 1
s

simon.vergauwen

02/29/2024, 4:30 PM
Our pleasure! ☺️ Exciting to hear you have a big codebase with Arrow, and glad the workaround is non-problematic for you. If you have any feedback would love to hear it
w

Wesley Hartford

02/29/2024, 7:53 PM
Is there any documentation or discussion related to this new restriction? I've found a couple other places this exception is being thrown and it's kind of hard to figure out exactly what the problem is. In the case I'm looking at now, it's not actually a lambda being returned, but an instance of a sealed class and I can't really see why it's an issue.
I guess my feedback is two things: 1. The types that trigger the exception (mostly Function) are kind of arbitrary and too broad. They're some of the most likely culprits for leaking the raise context, but I would say most occurrences of types being returned from fold or either aren't actually problematic, and the checks certainly don't prevent someone from leaking the context in some other type. 2. The exception message is misleading. It says "...Make sure all calls to 'raise' and 'bind' occur within...", but doesn't mention what actually triggers the exception (returning
Function
,
Lazy
, or
Sequence
. None of the instances I've found in my code were actually calling those functions, they were just returning a
Function
.
The second location I encountered this exception was a bit harder to work around. I was validating user input to construct a
Predicate
(which is a typealias for
() -> Boolean
). All the implementations I'm returning are quite simple and don't interact with the raise context at all. I ended up having to implement three functions following the unsafe pattern you mentioned above:
eitherUnsafe
,
withErrorUnsafe
, and
recoverUnsafe
.
3 Views