
Strategic Scala Style: Practical Type Safety - lihaoyi
http://www.lihaoyi.com/post/StrategicScalaStylePracticalTypeSafety.html
======
azernik
A great post!

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.)

~~~
bad_user
I don't agree with that advice.

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.

~~~
azernik
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.

~~~
bad_user
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.

~~~
azernik
Sure, if you're willing to create a sealed trait, and separate exception
classes, for every function that wants to do this.

What this really needs to be usable is union (sum) types

So in this case:

    
    
      def dangerousFunction(): Either[FileNotFoundError | JsonParseError, Int] = {
        ???
      }
    

...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.

------
aji
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!

~~~
neukoelln
If you have your class extend AnyVal, you even get the wrapping for free.

~~~
bad_user
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 ;-)

~~~
neukoelln
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](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.
    

none of which is the case in the example.

