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 a NonLocalReturnException 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, use Vector/IndexedSeq instead.

  • Use val instead of var. 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.tailrec
    def 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] to throw. Exceptions aren't documented in function signatures, are inefficient, and violate structured programming principles. Details.

  • Catch NonFatal instead of Throwable to avoid catching fatal exceptions like out-of-memory errors. Details.

  • Use Option instead of null and do not call Option.get. null isn't documented in function signatures and is error prone since the compiler cannot protect you. Calling Option.get defeats the purpose of Option, which is to explicitly handle the None 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 of Seq.head. The latter throws a NoSuchElementException on an empty list. Details.
  • Prefer stronger types and pattern matching to Any, AnyRef, isInstanceOf, and asInstanceOf. 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 an Object 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 => Running
    case 2 => Complete
    case _ => 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.

    Main.scala

    val service = Service(Database.forConfig("postgres"))

    Service.scala

    object Service {
    def apply(db: DatabaseDef): Service =
    new Service(Mapper(), Validator(), new WidgetRepository(db))
    }

  • 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, prefer someLib.revision for shared version numbers over variables.

    Dependencies.scala

    import sbt._
    object Dependencies {
    val someLib = "com.example" %% "core" % "1.0.0"
    val otherLib = "com.example" %% "logger" % someLib.revision
    }

  • for comprehensions are a simplified way of chaining flatMaps. Anything that exposes flatMap can be used in for comps. This includes Future[T], Option[T], Either[T], etc. Use <- if the statement you're calling returns something you want to flatMap over. Otherwise, use =.


    for {
    item <- methodReturningFuture()
    myValue = 5
    res <- anotherFuture(item, myValue)
    } yield res
    // Is equivalent to,
    val res = methodReturningFuture().flatMap { item =>
    val myValue = 5
    anotherFuture(item, myValue)
    }

Stay up to date

Get notified when I publish. Unsubscribe at any time.