5 ms·
Strategic Scala Style: Practical Type Safety
- aji 11y agoMy 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 11y agoIf you have your class extend AnyVal, you even get the wrapping for free.
- bad_user 11y agoYou 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 ;-)
- julian_t 11y agoValue classes solve this problem without the overhead of runtime allocation...
- modersky 11y agoI disagree. Value classes are a great tool in my arsenal, and not just for extension methods.
- neukoelln 11y agoI 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.
- azernik 11y agoA 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 11y agoI 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 11y agoWithout 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.