Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent either block nesting to avoid type/try-catch mismatches #148

Merged
merged 7 commits into from
Jun 17, 2024

Conversation

lbialy
Copy link
Contributor

@lbialy lbialy commented Jun 4, 2024

Either blocks, based on boundaries, provide implicit evidence for safe jumps. One problematic case is when .ok() combinator is called on a fork (or inside of exception capturing scope that can swallow a Break), another is when nested boundaries provide evidences for mismatched cases:

val effectMatchingOuterEither: Either[Throwable, Unit] = Left(Exception("oh no"))
val effectMatchingInnerEither: Either[String, Int] = Right(3)

val outer: Either[Throwable, Unit] = either {
  val inner: Either[String, Int] = either {
    effectMatchingOuterEither.ok()
    effectMatchingInnerEither.ok()
  }

  ()
}

Notice that first Either value evaluated with .ok() matches outer either block while being evaluated in the inner block. This might very well be considered a feature - types allow us to decide where to jump - but in my short but stressful experience with this kind of combinators - when this is used with type inference, results can be surprising. In my case the inner block was the body of Tapir handler while outer either block was in main function. The result was, unsurprisingly, a thread killed with Break. I would get a compilation error if nested either blocks were not allowed because without the outer block I'd be informed that it's impossible to use .ok() as either block in tapir handler is inferred to be Either[Unit, String]. I know this is a rather heavy-handed change and there are possible use cases for nested either blocks (tagged jumps for example) but this is confusing enough imho to warrant such a change. I can easily imagine such a change to occur during refactoring of code where inner either block stops matching one of the eithers with .ok() in it's scope and outer either providing evidence where it's no longer handled correctly.

@lbialy lbialy changed the title Proposal: prevent either block nesting to avoid type/try-catch mismatches Prevent either block nesting to avoid type/try-catch mismatches Jun 4, 2024
@Ichoran
Copy link

Ichoran commented Jun 4, 2024

In my experience, once one is aware that this can happen, it's actually a nice feature. Normally, yes, you jump to the closest block. But you can get out farther if you need to. It's unusual--at least for me--to be so confused that I have both the type wrong and where to jump to wrong, but they accidentally align and work despite not being desired behavior.

The problem comes when you try to jump thread boundaries. So, from my experience at least, I think this avoidance of nesting is not desirable.

@lbialy
Copy link
Contributor Author

lbialy commented Jun 5, 2024

In simplistic examples, sure. The problem I encountered was in a block in which tapir endpoints were also declared, either block was opened on top for Either[Throwable, Unit] and inferred type of endpoint handler was Either[Unit, String]. I had a function that had Either[Throwable, String] that I .ok()ed in handler and it caught the top-level either handler instead of the one inside of the handler so two things happened simultaneously: either blocks overlapped and there was a thread boundary that won't be covered by #147. Now disallowing nested either blocks won't fix this case completely and neither will #147 because you can always do:

either:
  val t = Thread(() => e.ok())
  t.start()

but I still feel it would prevent bugs coming from refactoring that are possible due to the automatic application of implicits.

inline def apply[E, A](inline body: Label[Either[E, A]] ?=> A): Either[E, A] = boundary(Right(body))
inline def apply[E, A](inline body: Label[Either[E, A]] ?=> A)(using
@implicitNotFound(
"Nesting of either blocks is not allowed as it's bug prone with type inference. Consider extracting the nested either block to a separate function."
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe reformulate a bit? Sth like:

Nesting of either blocks is not allowed as it's error prone, due to type inference. Consider extracting the nested either block to a separate function.

@adamw
Copy link
Member

adamw commented Jun 11, 2024

Looks good - let's try and see when we get complaints :) Do you think we should mention this in the docs?

@lbialy
Copy link
Contributor Author

lbialy commented Jun 11, 2024

I guess we should with an example on how to refactor.

@adamw
Copy link
Member

adamw commented Jun 12, 2024

@lbialy Would you be able to add the docs as well? :)

@lbialy
Copy link
Contributor Author

lbialy commented Jun 12, 2024

Yeah. Btw: I was thinking whether there should be a either.nested: block that would allow for nested either blocks (maybe either.unsafe?).

@adamw
Copy link
Member

adamw commented Jun 12, 2024

Yeah. Btw: I was thinking whether there should be a either.nested: block that would allow for nested either blocks (maybe either.unsafe?).

Let's be lazy here and wiat for the first user complaint that they would like such behavior ;)

@lbialy
Copy link
Contributor Author

lbialy commented Jun 12, 2024

Ok, I did add docs and changed the error message to your suggestion.

def returnsEither: Either[Exception, Int] = ???

def inner(): Either[String, Int] = either:
val i = returnsEither.ok() // this can only jump to either on the opening of this function
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this does not compile (as indicated by the CI failure) - you can't .ok() here as the boundary is String-error-typed, and returnsEither is Exception-error-typed.

@adamw adamw merged commit cf4b0eb into softwaremill:master Jun 17, 2024
4 checks passed
@adamw
Copy link
Member

adamw commented Jun 17, 2024

Ok, one merged, couple more to go :) Thanks :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants