I do exactly this in my own code, and it is wonderful.
It’s fast, clear, composable–there are really no downsides. For me, at least, it blows every other error-handling method out of the water (for routine use–in specialized cases you can need other things, such as when there are resources to clean up, or you may need to combine it with some sort of Using
clause).
If you want to try it out, you can find it in my kse library for Scala 2.12–it works by inserting a return statement using a macro (which works because of nonlocal returns).
In mill: ivy"com.github.ichoran::kse:0.11"
in dependencies.
import kse.flow._
to get it to work. Here’s an example, using my Ok
instead of Either
(because Ok
has a favored Yes
branch = Right
, and because Yes[Y]
is typed as Ok[Nothing, Y]
unlike Either
which keeps both type parameters for Right
…super awkward in practice).
// Setup: define functions that have correct and error branches
scala> import kse.flow._
import kse.flow._
scala> val zipPattern = """(\d\d\d\d\d)""".r
zipPattern: scala.util.matching.Regex = (\d\d\d\d\d)
scala> def parseZip(s: String) = s match {
case zipPattern(digits) => Yes(digits.toInt)
case _ => No("Not a zip: " + s)
}
parseZip: (s: String)kse.flow.Ok[String,Int]
scala> val phonePatternUS = """(\d\d\d)-(\d\d\d)-(\d\d\d\d)""".r
phonePatternUS: scala.util.matching.Regex = (\d\d\d)-(\d\d\d)-(\d\d\d\d)
scala> def parsePhone(s: String) = s match {
case phonePatternUS(area, pre, post) => Yes((area.toInt, pre.toInt*10000 + post.toInt))
case _ => No(s"Not a phone number: " + s)
}
parsePhone: (s: String)kse.flow.Ok[String,(Int, Int)]
So, now what, some for (Yes(y) <- parseZip(s)
-type thing to monadically thread through both zips and phone numbers?
No! That’s the magic of .?
:
scala> def parse2(s: String, t: String): Ok[String, (Int, (Int, Int))] =
Yes((parseZip(s).?, parsePhone(t).?))
parse2: (s: String, t: String)kse.flow.Ok[String,(Int, (Int, Int))]
// That's it! You just write the answer you want
// except decorate things that might fail with `.?`
// Could it get any easier?!
scala> parse2("fish", "salmon")
res0: kse.flow.Ok[String,(Int, (Int, Int))] = No(Not a zip: fish)
scala> parse2("12345", "salmon")
res1: kse.flow.Ok[String,(Int, (Int, Int))] = No(Not a phone number: salmon)
scala> parse2("12345", "987-654-3210")
res2: kse.flow.Ok[String,(Int, (Int, Int))] = Yes((12345,(987,6543210)))
And you can’t return the wrong type:
scala> def parseWrong(s: String): Ok[Int, (Int, Int)] = Yes(parsePhone(s).?)
<console>:15: error: type mismatch;
found : kse.flow.Ok[String,(Int, Int)]
required: kse.flow.Ok[Int,?]
def parseWrong(s: String): Ok[Int, (Int, Int)] = Yes(parsePhone(s).?)
I have not yet published the both improved-and-worse version for Scala 3. It’s improved because the macro was less fragile for Scala 3.1 (but it broke for 3.2, haven’t fixed it yet), and worse because you can’t merely type a return type–I wasn’t able to get the macro to read off the type information correctly (maybe my mistake?), so it looks like the following (note the required Or.Ret
, making things clear but ugly; note also that I’ve changed from Ok[N, Y]
to Y Or N
, where Or
is a union type with unboxed success type Is
and boxed failure type Alt
, courtesy of opaque types in Scala 3!..but…anyway):
scala> def parse2(s: String, t: String): (Int, (Int, Int)) Or String =
| Or.Ret{
| (parseZip(s).?, parsePhone(t).?)
| }
|
def parse2(s: String, t: String): kse.flow.Or[(Int, (Int, Int)), String]
scala> parse2("fish", "salmon")
val res0: kse.flow.Or[(Int, (Int, Int)), String] = Alt(Not a zip: fish)
scala> parse2("12345", "salmon")
val res1: kse.flow.Or[(Int, (Int, Int)), String] = Alt(Not a phone number: salmon)
scala> parse2("12345", "987-654-3210")
val res2: kse.flow.Or[(Int, (Int, Int)), String] = (12345,(987,6543210))
When Scala drops nonlocal returns, assuming that still will happen, it will have to rely on inserting its own code after hopefully successfully detecting whether to throw a stackless exception or return directly. I am really, really not looking forward to that, because at the moment .?
is probably my favorite single feature, and coming to the right conclusion about stackless exceptions seems very hard outside of the compiler.
Anyway, gigantic thumbs up to .?
. Please, however, if anyone picks this up for a standard feature, don’t implement it in a more limited way than I have–mine works with Try
, with Either
, with Option
, with Or
/Ok
…anything where there’s a clear “oh, that other type”, you can toss upstream with .?
.
(For bonus points: I added a .?+(f: A => B)
to switch the type on the way out. Yeah, you could first map, then use .?
, but .?+
is clearer.)