Lesson 24 / 25
Idiomatic Scala and Common Pitfalls
Write clear Scala and avoid frequent mistakes.
Writing Scala that others can read
Idiomatic Scala favours immutability (val, immutable collections, case classes), expressions over statements, ADTs and pattern matching for domain modelling, Option/Either instead of null and exceptions, small pure functions and explicit types on public APIs. Format consistently with scalafmt, and use scalafix and compiler warnings (-Wunused:all, -Werror in CI) to catch problems early. Common pitfalls: calling .get on Option or .head on possibly empty lists (use getOrElse, headOption or pattern matching); overusing implicits or givens so code becomes hard to follow; writing "Haskell in Scala" or "Java in Scala" inconsistently across a team instead of agreeing on a style; forgetting that Future runs eagerly (and creating futures inside a for comprehension, which makes them sequential); blocking inside the default execution context; using return in lambdas; overly clever type-level code that slows compilation and confuses colleagues; using List for random access; and comparing case classes containing Array (arrays use reference equality). Prefer the simplest feature that solves the problem.
Unidiomatic versus idiomatic
The same lookup written two ways.
case class Customer(id: String, email: String, tier: String)
// unidiomatic: nulls, var and .get
def findEmailBad(customers: List[Customer], id: String): String =
var result: String = null
for c <- customers do
if c.id == id then result = c.email
if result == null then throw new RuntimeException("not found")
result.toLowerCase
// idiomatic: Option and an expression
def findEmail(customers: List[Customer], id: String): Option[String] =
customers.find(_.id == id).map(_.email.toLowerCase)
val customers = List(Customer("c1", "ASHA@example.com", "gold"))
val greeting = findEmail(customers, "c1") match
case Some(email) => s"Sending offer to $email"
case None => "No such customer"
// pitfall: .head on an empty list throws; prefer headOption
val firstGold: Option[Customer] = customers.filter(_.tier == "gold").headOption
// pitfall: arrays compare by reference inside case classes
case class Batch(ids: Array[String])
println(Batch(Array("a")) == Batch(Array("a"))) // false
case class SafeBatch(ids: Vector[String])
println(SafeBatch(Vector("a")) == SafeBatch(Vector("a"))) // trueAgree on a team style
Scala supports many styles, from "better Java" to pure functional programming. Teams are most productive when they agree on one level (for example, "Option and Either, Cats Effect for I/O, no custom type-level code") and enforce it in reviews.
Quick check: Why can calling .get on an Option be dangerous?
- It throws an exception when the Option is None
- It is slow
- It changes the Option
- It returns null
Answer
It throws an exception when the Option is None — Option.get on None throws NoSuchElementException; use getOrElse, fold or pattern matching.