This is an abridged summary of what I’ve learned from watching a few people get started with Scala. There are a handful of mistakes that most people (who aren’t coming from a Haskell background) tend to fall into.
My advice for Scala beginners:
- Don’t use collections methods that throw.
- Don’t use
Try
(often). - Don’t use
Option.get
. - Reduce block depth with
for
comprehensions. - Use implicit Java conversions.
I believe this is all pretty uncontroversial among Scala mavens, though the unindoctrinated may require some discussion of functional programming to justify it (which I make no attempt to do here).
Collection exceptions
These methods are killers:
(_: Seq[A].head) : => A
(_: Seq[A].apply) : Int => A
(_: Map[A, B]).apply : A => B
They’re innocuously-named and look like the right methods to call, but in most
cases you need something that returns an Option
instead:
(_: Seq[A].headOption) : => Option[A]
(_: Seq[A].lift) : Int => Option[A]
(_: Map[A, B].get) : A => Option[B]
The correct methods are all slightly longer (when we elide apply
) than their
improperly typed counterparts, so coders exploring the API on their own tend to
find the wrong methods first.
Try
If one then discovers Try
before correcting those habits, you end up a lot of
expressions like
Try(xs.head).toOption
instead of
xs.headOption
This can be tricky because there are some places where exceptions are unavoidable. For example, I write this function often:
def parseDecimal(x: String): Option[BigDecimal] =
Try(BigDecimal(x)).toOption
It’s important to explain that this is just a workaround for a deficiency in the library, not an idiom to emulate.
Option.get
These examples are at least careful enough to check the Option
before
unwrapping it, similar to how one would guard against null
dereferences
in other languages.
if (xOption.isDefined) f(xOption.get)
if (xOption.isDefined) f(xOption.get) else y
xOption.isEmpty || (xOption.isDefined && !f(xOption.get))
In Scala we can do better and preserve type safety all the way through.
The easy go-to solution for any Option
problem is a pattern match:
xOption match {
case Some(x) => f(x)
case None =>
}
xOption match {
case Some(x) => f(x)
case None => y
}
xOption match {
case Some(x) => !f(x)
case None => true
}
And then with a little more API familiarity these expressions can be reduced:
xOption.foreach(f)
xOption.map(f).getOrElse(y)
!xOption.exists(f)
Block depth
You then start to encounter the nauseating block nesting known in some circles as “callback hell”:
xs.foreach(x =>
f(x).headOption.foreach(y =>
map.get(y).foreach(z =>
g(z, x)
)
)
)
w.toRight("a").flatMap(x =>
f(x).toRight("b").flatMap(y =>
g(y).toRight("c")
)
)
So at this point you have to introduce for
comprehensions.
for {
x <- xs
y <- f(x).headOption
z <- map.get(y)
} g(z, x)
for {
x <- w.toRight("a")
y <- f(x).toRight("b")
z <- g(y).toRight("c")
} yield z
Once someone is comfortable with for
expressions, I think they’ve got the
Scala essentials pretty well under control.
Java conversions
There’s one more thing that will only come up if you’re doing Java interop. There are several options for conversions between Java and Scala collections:
scala.collection.convert
scala.collection.JavaConversions
scala.collection.JavaConverters
Unfortunately these names are all very similar, and the documentation doesn’t explain how to choose.
I find this one easiest in most situations:
import scala.collection.JavaConversions._
This import provides implicit conversions between the two collections libraries, which usually works without thinking about it at all.