Optional Values#

Sometimes we don’t have a value for a given variable. Perhaps the value is not known or available yet. In Python we represent the absence of a value with the special value None. In other languages there is usually a null value.

xs = None
print(xs)
None

Without type hints we don’t really know if the value is supposed to be `None´ or something else.

Null Reference Exceptions#

The billion-dollar mistake

Speaking at a software conference in 2009, Tony Hoare apologized for inventing the null reference:

I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object-oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

We don’t have null-values in Python, but we have None values. Dereferencing a None value will lead to a NameError:

xs.run()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[2], line 1
----> 1 xs.run()

AttributeError: 'NoneType' object has no attribute 'run'

With type hints we can say that this is supposed to be an integer, but the value is missing, so we currently don’t know what integer just yet:

from typing import Optional

xs: Optional[int] = None

print(xs)
None

Testing for optional values#

We can test for optional values using is None or is not None:

xs = None
assert xs is None
y = 42
assert y is not None

In addition we have a number of falsy values:

assert not None
assert not 0
assert not []
assert not {}
assert not ()
assert not ""

Problems with Nullable Types#

Using Optional and nullable types in general has a lot of advantages since a compiler or static type checker can help us avoid using optional values before we have done proper testing first. The type Optional[A] is the same as Union[A, None] which means that there still a few more problems:

  • It’s easy to forget to check for None, but a static type checker will help

  • Extensive None checking can create a lot of noise in the code, increasing the cognitive load

  • Optional types cannot be nested. How do we differ between None being a proper values and None for telling that the value is missing i.e Union[None, None]? There is no equivalent of e.g a list containing an empty list e.g [[]].

Example: for dictionaries, how do we know if the key is missing or if the value is None?

mapping = dict(a=None)
mapping.get("a")

Options#

In functional programming we use the Option (or Maybe) type instead of None and null. The Option type is used when a value could be missing, that is when an actual value might not exist for a named value or variable.

An Option has an underlying type and can hold a value of that type Some(value), or it might not have the value and be Nothing.

The Expression library provides an option module in the expression package:

from expression import Option, option, Some, Nothing

Create option values#

Create some values using the Some constructor:

from expression import Some

xs = Some(42)
print(xs)
Some 42

You should not normally want to retrieve the value of an option since you do not know if it’s successful or not. But if you are sure it’s Some value then you retrieve the value back using the value property:

from expression import Some

xs = Some(42)
assert isinstance(xs, Some) # important!
xs.value

To create the Nothing case, you should use the Nothing singleton value. In the same way as with None, this value will never change, so it’s safe to re-use it for all the code you write.

from expression import Nothing

xs = Nothing
print(xs)
Nothing

To test if an option is nothing you use is test:

xs = Nothing
assert xs is Nothing

Option returning functions#

Values are great, but the real power of options comes when you create option returning functions

def keep_positive(a: int) -> Option[int]:
    if a > 0:
        return Some(a)

    return Nothing
keep_positive(42)
Option(some=42)
keep_positive(-1)
Option(none=None)

We can now make pure functions of potentially unsafe operations, i.e no more exceptions:

def divide(a: float, divisor: float) -> Option[int]:
    try:
        return Some(a/divisor)
    except ZeroDivisionError:
        return Nothing
divide(42, 2)
Option(some=21.0)
divide(10, 0)
Option(none=None)

Transforming option values#

The great thing with options is that we can transform them without looking into the box. This eliminates the need for error checking at every step.

from expression import Some, option, pipe, Nothing

xs = Some(42)
ys = pipe(
    xs,
    option.map(lambda x: x*10)
)
print(ys)
Some 420

If we map a value that is Nothing then the result is also Nothing. Nothing in, nothing out:

xs = Nothing
ys = pipe(
    xs,
    option.map(lambda x: x*10)
)
print(ys)
Nothing

Option as an effect#

Effects in Expression is implemented as specially decorated coroutines (enhanced generators) using yield, yield from and return to consume or generate optional values:

from expression import effect, Some

@effect.option[int]()
def fn():
    x = yield 42
    y = yield from Some(43)

    return x + y

fn()
Option(some=85)

This enables “railway oriented programming”, e.g., if one part of the function yields from Nothing then the function is side-tracked (short-circuit) and the following statements will never be executed. The end result of the expression will be Nothing. Thus results from such an option decorated function can either be Ok(value) or Error(error_value).

from expression import effect, Some, Nothing

@effect.option[int]()
def fn():
    x = yield from Nothing # or a function returning Nothing

    # -- The rest of the function will never be executed --
    y = yield from Some(43)

    return x + y

fn()
Option(none=None)

For more information about options: