I’m adding a boundary
/break
implementation tailored to Either
s to ox, but I noticed there are some usability issues (or simply problems in my implementation), and I’m not sure if they can be addressed using current error message customisation mechanisms.
First, here’s my (rather simple) implementation so far:
package ox
import scala.util.boundary
import scala.util.boundary.{Label, break}
object either:
case class Fail[+A] private[ox] (a: A)
inline def apply[A, T](inline body: Label[Fail[A] | T] ?=> T): Either[A, T] =
boundary(body) match
case Fail(a: A) => Left(a)
case t: T => Right(t)
extension [A, B, T](t: Either[A, B])(using b: boundary.Label[either.Fail[A] | T])
inline def value: B =
t match
case Left(a) => break(either.Fail(a))
case Right(b) => b
This works nicely if used correctly, e.g.:
val ok1: Either[Int, String] = Right("x")
val fail1: Either[Int, String] = Left(1)
val r: Either[Int, (String, String)] =
either:
(ok1.value, fail1.value)
However, I noticed two potential problems with usability. As a clarification: I know these are standard error messages and in that respect they work correctly. However, my premise is that if a boundary-break implementation (tailored to Either
, Result
or whatever) is supposed to be one of the basic building blocks when it comes to error handling in Scala, the bar on usability is quite high. So, generic error messages are not sufficient (as demonstrated below).
Problem 1 - using break outside of boundary
If we try to use .value
outside of the either
boundary, we get the following error:
ok1.value
[error] -- [E008] Not Found Error: test.scala:38:8
[error] 38 | ok1.value
[error] | ^^^^^^^^^
[error] |value value is not a member of Either[Int, String].
[error] |An extension method was tried, but could not be fully constructed:
[error] |
[error] | ox.value[Int, B, T](this.ok1)(
[error] | /* missing */summon[scala.util.boundary.Label[ox.either.Fail[Int] | T]])
[error] |
[error] | failed with:
[error] |
[error] | No given instance of type scala.util.boundary.Label[ox.either.Fail[Int] | T] was found for parameter b of method value in package ox
[error] |
[error] | where: T is a type variable with constraint
[error] |
[error] one error found
I think from a user perspective the error is quite cryptic, and doesn’t really point to any possible solutions (wrapping with either:
). Maybe that’s a possible enhancement to the compiler, as we are missing a Label
implicit, which has a better error message:
@implicitNotFound("explain=A Label is generated from an enclosing `scala.util.boundary` call.\nMaybe that boundary is missing?")
final class Label[-T]
But that’s not used, I’m guessing because of the indirect implicit usage (via an extension method).
Ideally, I’d want to provide my own error message (via a type alias + @implicitNotFound
?). (I’ll try a macro next, but that’s also not perfect from an implementors PoV.)
Problem 2 - wrong type ascription
If we use an incorrect type, the error message is confusing as well:
val r: Either[Int, Long] = either(ok1.value)
[error] -- [E008] Not Found Error: test.scala:38:42
[error] 39 | val r: Either[Int, Long] = either(ok1.value)
[error] | ^^^^^^^^^
[error] |value value is not a member of Either[Int, String].
[error] |An extension method was tried, but could not be fully constructed:
[error] |
[error] | ox.value[Int, B, T](this.ok1)(
[error] | /* missing */summon[scala.util.boundary.Label[ox.either.Fail[Int] | T]])
[error] |
[error] | failed with:
[error] |
[error] | No given instance of type scala.util.boundary.Label[ox.either.Fail[Int] | T] was found for parameter b of method value in package ox
[error] |
[error] | where: T is a type variable with constraint
[error] |
[error] one error found
Again, I understand where the error is coming from, but I think we should be able to provide a better developer experience here (of course, still my implementation can simply be incorrect, so please correct me if I’m wrong). Using the custom error message from @implicitNotFound
could provide some guidance, but I’m not sure if it would resolve this specific problem fully. If it’s possible to solve in any way at all?