Field Notes

Scala/Play, Part 3: A Policy Lookup That Might Find Nothing

Scala/Play, Part 3: A Policy Lookup That Might Find Nothing

Scala/Play · Part 3

Our Scala learning project now has a policy snapshot, a constructor and a calculation. The next question is familiar from backend development: what happens when I ask for a policy that does not exist?

I want that possibility to appear in the method's return type. Coming from Java, Optional gives me a useful starting point. In Scala, I can express the same kind of optional result with Option, then handle its two outcomes with match.

Zakaria explores an archive, comparing an occupied drawer with a visibly empty drawer.
A lookup can find data or find nothing. The empty result is something our code handles deliberately.

Put possible absence in the signature

A Java method might declare:

Optional<PolicySnapshot> findPolicy(String number)

The Scala 2 signature is:

def findPolicy(number: String): Option[PolicySnapshot]

Square brackets hold the type argument, where Java uses angle brackets. Option[PolicySnapshot] means the result may contain a policy snapshot. It is not itself a PolicySnapshot.

We keep the data type from the previous lesson:

case class PolicySnapshot(
    number: String,
    basePremium: Int,
    claimFreeYears: Int
)

The fictional values remain "POL-001", 600 whole euros and three claim-free years. There is no database in this exercise. A hardcoded lookup is enough to see exactly which branch runs.

Some contains a value; None represents absence

def findPolicy(number: String): Option[PolicySnapshot] = {
  if (number == "POL-001") {
    Some(PolicySnapshot("POL-001", 600, 3))
  } else {
    None
  }
}

For "POL-001", the condition is true. We construct the snapshot, wrap it in Some, and return that wrapper. For "POL-999", the condition is false and the result is None.

Scala resultMeaningJava comparison
Some(policy)A policy is presentOptional.of(policy), for a non-null policy
NoneNo policy is presentOptional.empty()

None is a value, not null and not an exception. The Scala 2.13 Option API describes this optional-value model. One boundary matters: Scala 2 still permits nulls, and Some(null) is possible. Here we construct a real snapshot; using Option is not a compiler-enforced ban on null throughout the program.

The wrapper is not the policy

val found: Option[PolicySnapshot] = findPolicy("POL-001")
val missing: Option[PolicySnapshot] = findPolicy("POL-999")

println(found)
// Some(PolicySnapshot(POL-001,600,3))

println(missing)
// None

I cannot write found.number: found has type Option[PolicySnapshot], and that type has no policy-number property. The value inside a successful result has that property.

found.isDefined returns true, and missing.isEmpty returns true. Those methods inspect presence; neither extracts the snapshot. To produce a useful message, I need to handle both outcomes.

Read the match expression literally

def describePolicy(result: Option[PolicySnapshot]): String = {
  result match {
    case Some(policy) => "Found policy " + policy.number
    case None => "Policy not found"
  }
}

result match starts the inspection. Each case introduces a pattern. The => separates that pattern from the expression evaluated when it matches.

In case Some(policy), the name policy is introduced by the pattern. It refers to the existing snapshot contained in the successful result. Within that branch, its type is PolicySnapshot, so policy.number is available.

The name is my choice. case Some(foundPolicy) would work too, with foundPolicy.number on the right. It is a branch-local binding, not a variable I must declare beforehand.

An Option result follows one of two paths: Some binds its policy and produces Found policy POL-001; None produces Policy not found. Both results are strings.
The selected branch supplies a String. Only the Some branch has a contained policy to use.

Constructing and matching use different contexts

These two lines look related, but perform different operations:

Some(policy)             // Expression: construct a wrapper
case Some(policy) => ... // Pattern: match and bind its contents

In the first line, policy already exists and is supplied to Some. In the second, the pattern introduces the name for an existing contained value. Matching does not create another wrapper or another snapshot.

Scala chooses the first matching case. A successful result takes the Some branch; None takes the second branch. There is no fall-through into the next case and no break to add. The pattern-matching tour covers the syntax and its expression behaviour.

Match produces a value, just like if

Both branches produce a string. That string becomes the value of the entire match. Because the match is the final expression in describePolicy, it also becomes the method's result. This extends the same rule we used for the premium calculation in Part 1.

Some(PolicySnapshot("POL-001", 600, 3))
  → matches Some(policy)
  → reads policy.number
  → "Found policy POL-001"

None
  → matches None
  → "Policy not found"

These branches cover the normal present-or-absent outcomes of this lookup. Missing data is handled deliberately, rather than discovered while trying to dereference it.

Run both outcomes

In our existing sbt project, OptionLesson.scala contains both methods. Its run method performs the successful and unsuccessful lookups and prints their descriptions; PremiumLesson.main calls it.

println(describePolicy(findPolicy("POL-001")))
println(describePolicy(findPolicy("POL-999")))

Run sbt run from the directory containing build.sbt. We verified these messages in the lab:

Found policy POL-001
Policy not found

A lookup now tells its caller that absence is possible, and the caller handles it explicitly. Next I will use this same example to understand Option.map, starting from the match we can already read.