A suggestion for change: Try[V] is, for most purposes, a more intuitive alternative to Either[V, T] for exception handling. It can be passed a block that may throw a Throwable and Do the Right Thing, it has functions named in ways that make it more clear which alternative they operate on (recover, isFailure, isSuccess), and works nicely in for comprehensions:
val tryManyThingsSequentially = for {
result <- someDangerousComputation
other <- otherDangerousComputation(result)
} yield other
(As far as I understand, it's implemented more or less as an Either[V, Throwable] under the hood.)
By using Either you're forced to enumerate all errors that can happen and those errors are no longer exceptional. This means that the ErrorType can be an ADT or even if you're using things inheriting from Throwable, you can have a base type that's more specific than Throwable. More than that, you're free to generate your own errors. Because you see, whenever you do "throw new SomeException", or in other words whenever you're using exceptions for validations of user input or flow control, God kills a kitten.
The problem with exceptions is that they signal a very strong-side effect, as throwing an exception obliterates your call-stack. Forget about functional programming, because this violates the laws of structured programming, functions and modules ending up with multiple exit points, blowing up in your face when you least expect them. There's also a big difference between NumberFormatException and OutOfMemoryError. One is truly exceptional, whereas the other could be prevented, guess which is which.
Btw, when using Either, as a matter of convention, the errors are at the Left, whereas the normal result is at the right. So that would be Either[Error, A]. And nothing stops you from using for comprehensions with Either ...
val tryManyThingsSequentially = for {
result <- someDangerousComputation.right
other <- otherDangerousComputation(result).right
} yield other
Some people don't like Either because it is unbiased. In the Cats library you've got Xor, which is like a right-biased Either (so you don't have to use right projections explicitly, like in the above sample). There's also a similar type in Scalaz.
Without algebraic types in Scala, it is unfortunately very difficult to have an Either that enumerates all the expected types of exceptions from a block of code (especially one that you did not write).
My general idiom for restricting the return types of Try is to recover or recoverWith only the types I consider "normal" and expected. e.g.
finalTryValue.recoverWith {
case FileNotFoundException => Success(HttpResponse(InternalServerError))
case JsonParseError => Success(HttpResponse(BadRequest))
case e: ComplicatedException where e.meetsSomePredicate => Failure(new MoreInformativeException(cause=e))
}
// at this point, if the Try is still a failure, something's wonky and we *should* crash, or do whatever our generic error-handling code specifies
// and the failure will still contain the original stack at the time of failure
With regard to for comprehensions, I just find the Either left/right convention hard to read and remember; I'd even like Try if it was just a thin wrapper that added handy names to Either, like "type Try[E, V] = Either[E <: Throwable, V]" with handy aliases for left and right.
Don't get me wrong, I do find Try useful. We are talking about public APIs, as in APIs meant to be consumed by people other than the author of that function.
The problem with a Try[T] is that if somebody else reads the signature of `def somethingDangerous()`, he won't be able to see that the code can throw a FileNotFoundException or a JsonParseError. Scala does have algebraic types and you can even handle the unexpected with a type like this:
sealed trait Error
case class FileNotFoundError(f: File) extends Error
case class JsonParseError(path: JsPath) extends Error
// ... we can even handle the unexpected
case class UnknownError(ex: Throwable) extends Error
Surely that's extra boilerplate, but if the function is public (meant to be used by other people), then it's worth it. Because now when you say that your function returns an `Either[Error, A]`, this type explicitly documents the important error types that can happen (and are hence not exceptional).
I know what you mean by Either. What you want is a right biased Either. Many people feel that way. You've got it implemented in libraries such as Cats or Scalaz. The left/right convention isn't hard to remember, because it's consistent. In general you'll see that types are biased to the right, which really means that the useful happy path result is usually at the right.
...and looking for this online, it turns out this is a feature in the experimental Dotty compiler. Yay.
And yeah, right-biased either would get halfway there. But it's really the terminology and naming that I have an issue. If I could get an Either knockoff that just called its left and right "error" and "success", that would go a long way to readability.
> Because you see, whenever you do "throw new SomeException", or in other words whenever you're using exceptions for validations of user input or flow control, God kills a kitten.
> One is truly exceptional, whereas the other could be prevented
Reading this I would think that exceptions are black magic. So many ill-defined words like "truly exceptional" (true Scottsman comes to mind) and dogma (exceptions bad, mkay? no explanation required).
Seriously, what is "truly exceptional"? Its interesting if you look at the definition of "exceptional" you get "unusual; not typical", but it seems like people who are strongly against exceptions mean something else...
And "don't use exceptions for flow control" - what does that even mean? Exceptions are a form of flow control. Perhaps you want `abort` instead? That certainly can't be used for flow control...
Truly exceptional means nondeterministic outcomes that should never happen, yet still do because of factors outside the developer's control.
This includes out of memory errors, segfaults, threads being interrupted, contract violations (incorrect usage of an API that cannot be prevented with static typing, but this does NOT include validating the user's input), file handlers not being opened because you reached the limit forced by the OS, running out of Inodes or space on the local hard-disk, network sockets becoming unresponsive because the network is temporarily down, requests timing out, etc.
> So many ill-defined words like "truly exceptional" (true Scottsman comes to mind) and dogma (exceptions bad, mkay? no explanation required).
Not sure what the "true Scottsman" has to do with it, maybe you can explain. At this point I wrote over 500 words to explain myself on an internet forum, without debating the finer details of the English language, but we can do that as well.
> incorrect usage of an API that cannot be prevented with static typing, but this does NOT include validating the user's input
This is the part I have the most contention with. Take parsing JSON for example: there are many situations in which you pretty much know that the input being invalid is truly exceptional (reading a local file generated by some part of the code, for example). There are others however where its not exceptional at all (reading incoming input from a user). So its up to the API consumer, not the producer to decide whether a situation is "truly exceptional" or not. Exceptions recognise this, and let the errors propagate through the layers so that each layer can decide whether the error is exceptional from their point of view.
Parsing JSON can produce a finite set of errors that you know about. Such errors are either lexer/parser errors (invalid token encountered, expecting a string and getting something else) or format errors (expecting a DateTime inside a string). One possible type that could express this, copied from Play's JSON, would be this one:
sealed trait JsResult[+A]
case class JsError(errors: Seq[(JsPath, Seq[ValidationError])])
extends JsResult[Nothing]
case class JsSuccess[+A](value: A, path: JsPath)
extends JsResult[A]
So instead of throwing exceptions, you return a JsResult that could represent one or multiple errors.
> So its up to the API consumer, not the producer to decide whether a situation is "truly exceptional" or not.
We kind of agree on this one, as long as we are talking about things the producer can control of course. The producer obviously can't guard well against a hard-disk getting full (and that would be truly exceptional). But for things it can control, the producer shouldn't throw exceptions on JSON parsing, when it could return a JsResult and leave the consumer to decide what to do with it. And the advantage is that JsError is explicit, its shape is explicit, so the consumer knows exactly what happened and it can return a human-readable error message, or it can fall-back to something else, etc.
And then, if you read from a file generated by your software and you expect a valid JSON and it's not what you expect, then that's obviously a bug, which is also an exceptional situation, so exceptions are OK. But that's no longer user input, as you've said.
Absolutely agree. I think the Play JSON libraries are a good example of leaving that distinction to the user: you can either do jsonValue.as[SomeParsedType] when you want an exception, or jsonValue.validate[SomeParsedType] if you want to keep JsErrors in the normal flow of control. The language makes it very clear.
(I tend to use .as() in practice, but in part that's because in the Try[X]/Future[X]/Either[T, X]/etc. monads exceptions work a lot like early returns, which is a nicely-bounded deviation from structured programming that I'm pretty okay with).
My team recently started boxing both integer and string IDs in a service of ours and, though the transition was a little painful, it has paid off immensely in both readability and safety. These kinds of tips aren't just pedantic programming language theory, they're improvements that will actually make your code nicer!
You should never extend AnyVal, unless you're using that for defining extension methods. The feature is a hack, because it has to workaround the JVM's lack of value classes, hence it has non-intuitive behavior and it probably doesn't work as you think it does.
In particular if you ever have to declare the type anywhere (or in other words, whenever you treat it as a type), like in:
case class Person(id: PersonId)
That's going to be an allocated PersonId instance. And the limitations don't stop there. You'll also get instances allocated when doing pattern matching, or when working with generic functions. Heck, if you'll actually measure the performance, you'll soon discover that the JVM will allocate more on the heap and not less. And there goes the primary use-cases out the window. So you might as well have it as a normal class and avoid the gotchas and your colleagues cursing ;-)
I admit that I am not an expert on Scala, but I am confused about your "Person" example. I thought that this was exactly the common use case that wouldn't cost you an allocation.
According to http://docs.scala-lang.org/overviews/core/value-classes.html:
A value class is actually instantiated when:
a value class is treated as another type.
a value class is assigned to an array.
doing runtime type tests, such as pattern matching.
A suggestion for change: Try[V] is, for most purposes, a more intuitive alternative to Either[V, T] for exception handling. It can be passed a block that may throw a Throwable and Do the Right Thing, it has functions named in ways that make it more clear which alternative they operate on (recover, isFailure, isSuccess), and works nicely in for comprehensions:
(As far as I understand, it's implemented more or less as an Either[V, Throwable] under the hood.)