UP | HOME

type : Maybe

Navigation   notoc

Motivations

Examples

Types   notoc nav

Introduction   notoc

  • Handle optional values without guessing and without exceptions
  • Maintain type invariants in smart constructors

Maybe guarantees exception free and guess free optional value handling by providing only one function for getting at the optional value, a function that requires function arguments that handle both present and absent states.

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 not null 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 through Maybe.fold.
boolean blindness

Maybe.fold provides a value only to the just 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

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 returns nothing if its input is < 1. This creates a compile time guarantee that Natural 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
}

Links