I’ve experimented with this specific form, and outside of error handling I didn’t really find it to be preferable to the two-sided form:
extension (objectEither: Either.type)
inline def Ret[L, R](inline r: Label[Either[L, R]] ?=> R): Either[L, R] =
boundary{ Right[L, R](r) }
extension [L, R](either: Either[L, R])
inline def ?[E >: Left[L, Nothing]](using Label[E]): R = either match
case Right(r) => r
case l: Left[L, R] => boundary.break(l.asInstanceOf[E])
The magic is in the .?. That is where most of the advantage is.
Then the question is: what do you do more, create a new Left value (in which case the asEither form slightly simplifies things), or pass on existing Left values (in which case the Either.Ret form avoids boxing/unboxing). For me the answer is: more the latter.
Furthermore, .? on a bare value that could be wrapped Left is dangerous, because it confuses unwrapping (which is what .? is brilliant at) and nonlocal control flow. So in practice, you really want to write something more like break(e). But Left(e).? is barely any longer, and for me anyway is visually way more distinct.
And then sometimes it is handy to want to return early with success, and it’s annoying to have to use a separate construct for it.
So, personal experience for me resolved to: something like asEither, but Either.Ret flavor instead. (I use the double boundary trick for some other control flow constructs I have, but .? is by far the one that saves me the most effort.)
One word of caution, however. Try murders constructs like these. You have to remember to keep the failure case OUT of the Try block. If you use it to, for instance, guard against IO failures, you have to make sure you manually propagate the error case, because at the very least, you won’t get the error you thought you were going to, and depending on what the Try was intended to catch, you might swallow errors entirely.
So instead, I distinguish between what should be caught at thread boundaries and not, and
extension (t: Throwable)
def catchable: Boolean = t match
case _: VirtualMachineError | _: InterruptedException | _: LinkageError |
_: ControlThrowable | _: Break[?] => false
case _ => true
inline def safe[X](inline x: => X): X Or Throwable =
try Is(x)
catch case e if e.catchable => Alt(e)
(I have an success-unboxed either called Or, where the thing either Is(x) because it’s unboxed, so it really is just x, or it’s Alt(y), the alternate type, which is boxed (necessarily). One can do the same thing with Either if one wants.)
Basically, Try is pernicious for code that uses control flow like this. (It is helpful, I guess, for threaded code.)
Edit: Ox does something similar for Either. I think it’s isomorphic to my flavor rather than asEither, but I forget.