Scala Best Practices
Rather than using random examples from Scala projects in the wild, these are meant as my evolving "best" way of doing things. If you're new to Scala, start here.
-
Scala returns the last expression in a function. Use this to return things over using
return
directly. The latter is implemented by throwing and catching aNonLocalReturnException
and is inefficient. Details. -
Scala Collection Performance Characteristics. Of particular note,
List
/Seq
has O(n) access (apply
), update, and append. If often accessing indexes directly, useVector
/IndexedSeq
instead. -
Use
val
instead ofvar
. Immutable data is easier to reason about and simplifies concurrency. -
Use immutable data structures for the same reason as above. Details. An example of recursive depth-first traversal via immutable
Map
,def traverseDepthFirst(treeId: TreeId) = {val tree = getTree(treeId)@scala.annotation.tailrecdef traverseDepthFirst(stack: Set[NodeId],visited: List[Node],nodeIdToAncestorIds: Map[NodeId, Set[NodeId]],ord: Int): (List[Node], Map[NodeId, Set[NodeId]]) =if (stack.isEmpty) (visited, nodeIdToAncestorIds)else {val state = tree.state(stack.head)traverseDepthFirst(state.childNodeIds.filter(pId => !visited.exists(_.nodeId === pId)) ++ stack.tail,createNode(tree, stack.head, ord, state) :: visited,nodeIdToAncestorIds + (stack.head -> Option(state.ancestorNodeIds).getOrElse(Set())),ord + 1)}traverseDepthFirst(Set(tree.rootNodeId), Nil, Map[NodeId, Set[NodeId]](), 0)} -
Prefer
Either[SomeError, ExpectedResult]
tothrow
. Exceptions aren't documented in function signatures, are inefficient, and violate structured programming principles. Details. -
Catch
NonFatal
instead ofThrowable
to avoid catching fatal exceptions like out-of-memory errors. Details. -
Use
Option
instead ofnull
and do not callOption.get
.null
isn't documented in function signatures and is error prone since the compiler cannot protect you. CallingOption.get
defeats the purpose ofOption
, which is to explicitly handle theNone
case. Details.- Related: Prefer
Option
's.map
and.fold
to.isDefined
/.isEmpty
. They are more idiomatic.
// 👎if (someOption.isDefined) s"value=${someOption.get}" else "Default"// 👍someOption.fold("Default")(v => s"value=$v")- Related: use
Seq.headOption
instead ofSeq.head
. The latter throws aNoSuchElementException
on an empty list. Details.
- Related: Prefer
-
Prefer stronger types and pattern matching to
Any
,AnyRef
,isInstanceOf
, andasInstanceOf
. The latter circumvent the type system that is meant to protect you. Details. -
Use
===
from the Cats library instead of==
. The latter is syntactic sugar for Java's.equals
, which accepts anObject
parameter. This allows comparing values of differing types. Details.import cats.instances.string._import cats.syntax.eq._"hi" === "hi" -
Use sealed traits for enumerations (until Scala 3 comes out). Sealed traits can only be extended in the file they're declared so the compiler knows all subtypes and can issue warnings for non-exhaustive matches. Details.
sealed trait JobStatus { def value: Int }object JobStatus {def apply(code: Int): JobStatus =code match {case 1 => Runningcase 2 => Completecase _ => Invalid}}case object Invalid extends JobStatus { val value = 0 }case object Running extends JobStatus { val value = 1 }case object Complete extends JobStatus { val value = 2 } -
Use simple constructor arguments for dependency injection instead of a framework.
-
Avoid hard-coding execution contexts, pass them as implicit parameters instead. Details.
-
Declare dependencies in
project/Dependencies.scala
. If a dependency is failing to resolve, ensure you're using the proper number of%
. To make it easier for automated dependency updates, prefersomeLib.revision
for shared version numbers over variables. -
for
comprehensions are a simplified way of chainingflatMap
s. Anything that exposesflatMap
can be used infor
comps. This includesFuture[T]
,Option[T]
,Either[T]
, etc. Use<-
if the statement you're calling returns something you want toflatMap
over. Otherwise, use=
.for {item <- methodReturningFuture()myValue = 5res <- anotherFuture(item, myValue)} yield res// Is equivalent to,val res = methodReturningFuture().flatMap { item =>val myValue = 5anotherFuture(item, myValue)}