type : Maybe
Implementation
Values of Maybe[A]
either contain a single value of A
or nothing.
sealed abstract class Maybe[A]
It is usually implemented as a sum type with nullary and 1-ary “cases”.
private final case class Nothing[A]() extends Maybe[A] {} private final case class Just[A](a: A) extends Maybe[A] {}
Since it makes no sense to have Nothing[A]
and Some[A]
types available, we’ll
make them private and provide two constructor functions.
object Maybe { def nothing[A]: Maybe[A] = Nothing() def just[A](a: A): Maybe[A] = Just(a) }
There is only one method with access to the private internals of Maybe[A]
.
def fold[B](nothing: => B, just: A => B): B = this match { case Nothing() => nothing case Just(a) => just(a) }
There is no way to dereference the Maybe-wrapped value directly. fold
ensures
that the Nothing
case is handled.
Here’s the complete definition.
sealed abstract class Maybe[A] { def fold[B](nothing: => B, just: A => B): B = this match { case Nothing() => nothing case Just(a) => just(a) } } private final case class Nothing[A]() extends Maybe[A] {} private final case class Just[A](a: A) extends Maybe[A] {} object Maybe { def nothing[A]: Maybe[A] = Nothing() def just[A](a: A): Maybe[A] = Just(a) }
Maybe vs null checks
Assuming the following values:
final case class Name(first: String, last: String) val absent: String = "Nobody" def present(n: Name): String = s"${n.first} ${n.last}"
A typical null-check,
def describe(n: Name): String = if (n == null) absent else present(n)
exhibits a lack of type safety in two ways:
- assumption
- The programmer assumes
n
is notnull
and doesn’t write the null check. - boolean blindness
The compiler doesn’t stop accidental dereferencing, as in this transposition:
def describeT(n: Name): String = if (n == null) present(n) else absent
Boolean blindness is well described by Robert Harper. Here’s the money quote:
Another harm is the condition of Boolean blindness alluded to earlier. Suppose that I evaluate the expression e=e’ to test whether e and e’ are equal. I have in my hand a bit. The bit itself has no intrinsic meaning; I must associate a provenance with that bit in order to give it meaning. “This bit being true means that e and e’ are equal, whereas this other bit being false means that some other two expressions are not equal.” Keeping track of this information (or attempting to recover it using any number of program analysis techniques) is notoriously difficult. The only thing you can do with a bit is to branch on it, and pretty soon you’re lost in a thicket of if-the-else’s, and you lose track of what’s what. Evolve the program a little, and you’re soon out to sea, and find yourself in need of sat solvers to figure out what the hell is going on.
Maybe
solves both issues.
- assumption
- The programmer cannot get at the value of a
Maybe
without going throughMaybe.fold
. - boolean blindness
Maybe.fold
provides a value only to thejust
case:def describe(n: Maybe[Name]): String = n.fold(absent, present)
An accidental transposition results in a compile time error:
def describeT(n: Maybe[Name]): String = n.fold(present, absent)
Demos
- Null-check transposition with runtime failure. Scala-Js-Fiddle optimizes the generated Javascript, so member names in error output will not necessarily match those in the source file.
- Transposition with compile time failure. Uncomment the indicated code to cause compile errors.
Smart constructors and invariants
Maybe
can be used to enforce a type’s invariants. A trivial example is a type
for integers greater than zero.
Natural
- The type of integers
> 0
. natural: Int => Maybe[Natural]
- The only constructor for
Natural
values. It returnsnothing
if its input is< 1
. This creates a compile time guarantee thatNatural
values will always be> 0
.
final class Natural private(n: Int) { val value: Int = n } object Natural { import Maybe._ def natural(n: Int): Maybe[Natural] = if (n > 0) just(Natural(n)) else nothing }