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.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]
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.
- Related: Prefer
// š
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.- 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.
- Prefer stronger types and pattern matching to
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, prefersomeLib.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 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 = 5
res <- anotherFuture(item, myValue)
} yield res
// Is equivalent to,
val res = methodReturningFuture().flatMap { item =>
val myValue = 5
anotherFuture(item, myValue)
}
Top comments (0)