Null-Safety vs Maybe/Option - A Thorough Comparison (Part 2/2)
Tags: null, maybe, option
Published: 2019-10-18 13:28:09

---
title: Null-Safety vs Maybe/Option - A Thorough Comparison (Part 2/2)
published: true
description: An in-depth and practical comparison between Null-safety and the Maybe/Option type, both used to discard the infamous null pointer error.
tags: null, maybe, option
cover_image: https://thepracticaldev.s3.amazonaws.com/i/mea3kjfqq02eco8m32ku.jpg
---

In [part 1](https://dev.to/practicalprogramming/null-safety-vs-maybe-option-a-thorough-comparison-part-1-jlk) of this article we saw how the null pointer error can be eliminated with null-safety or with the Maybe/Option type.

In this part we'll have a closer look at code snippets that illustrate frequent use cases of handling the 'absence of a value'.

Useful Null-Handling Features
-----------------------------

Besides null-safety, a language should also provide specific support for common null handling operations. Let's have a look at some examples.

Note: The additional support for null-handling presented in the following chapters is typically found only in null-safe languages. However, other languages can also provide these features, even if they are not null-safe.

### Searching the First Non-Null Value

Suppose we want to look up a discount for a customer. First we try to retrieve the value from a web-service. If the value is not available (i.e. the result is `null`), we try to retrieve it from a database, then from a local cache. If the value is still `null` we use a default value of 0.0.

Now we want to write a function that provides the discount for a given customer. To keep the example simple, we ignore error-handling. Moreover, we don't use asynchronous functions to make the lookup process faster.

#### Java

First attempt:

    static Double customerDiscount ( String customerID ) {

        Double result = discountFromNet ( customerID );
        if ( result != null ) {
            return result;
        } else {
            result = discountFromDB ( customerID );
            if ( result != null ) {
                return result;
            } else {
                result = discountFromCache ( customerID );
                if ( result != null ) {
                    return result;
                } else {
                    return 0.0; // default value
                }
            }
        }
    }

What an ugly monstrosity! Let's quickly rewrite it:

    static Double customerDiscount ( String customerID ) {
        
        Double result = discountFromNet ( customerID );
        if ( result != null ) return result;

        result = discountFromDB ( customerID );
        if ( result != null ) return result;

        result = discountFromCache ( customerID );
        if ( result != null ) return result;

        return 0.0; // default value
    }

Note: There's nothing wrong with using several `return` statement, as in the code above (although some people might disagree).

The complete Java source code for a test application is available <a href="https://github.com/pp-articles/null_vs_maybe/tree/master/examples/search_first_non_null/Java/SearchFirstNonNullTest.java" class="pml-link">here</a>.

#### Haskell

Let's start again with the straightforward, but ugly version:

    customerDiscount :: String -> Float
    customerDiscount customerID =
        case (discountFromNet customerID) of
        Just d -> d
        Nothing -> case (discountFromDB customerID) of
                   Just d -> d
                   Nothing -> case (discountFromCache customerID) of
                              Just d -> d
                              Nothing -> 0.0

There are different ways to write better code. Here is one way:

    customerDiscount :: String -> Float
    customerDiscount customerID =
        let discountMaybe = discountFromNet customerID
                            <|> discountFromDB customerID
                            <|> discountFromCache customerID
        in fromMaybe 0.0 discountMaybe

The complete Haskell source code for a test application, including alternative ways to write the above function, is available <a href="https://github.com/pp-articles/null_vs_maybe/tree/master/examples/search_first_non_null/Haskell/SearchFirstNonNullTest.hs" class="pml-link">here</a>.

More information (and even more alternatives) can be found in the Stackoverflow question <a href="https://stackoverflow.com/questions/5606228/using-the-maybe-monad-in-reverse" class="pml-link">Using the Maybe Monad in reverse</a>.

#### PPL

Again, first the ugly version, written in [PPL](http://www.practical-programming.org):

    function customer_discount ( customer_id string ) -> float_64
        if discount_from_net ( customer_id ) as net_result is not null then
            return net_result
        else
            if discount_from_DB ( customer_id ) as DB_result is not null then
                return DB_result
            else
                if discount_from_cache ( customer_id ) as cache_result is not null then
                    return cache_result
                else
                    return 0.0
                .
            .
        .
    .

The code becomes a one-liner and more readable with the practical `if_null:` operator designed for this common use case:

    function customer_discount ( customer_id string ) -> float_64 = \
        discount_from_net ( customer_id ) \
        if_null: discount_from_DB ( customer_id ) \
        if_null: discount_from_cache ( customer_id ) \
        if_null: 0.0

The `if_null:` operator works like this: It evaluates the expression on the left. If the result is non-null, it returns that result. Else it returns the expression on the right.

In our example we use a chain of `if_null:` operators to find the first non-null value. If the three functions called in the expression return `null`, we return the default value `0.0`.

The complete PPL source code for a test application is available <a href="https://github.com/pp-articles/null_vs_maybe/tree/master/examples/search_first_non_null/PPL/search_first_non_null_test.ppl" class="pml-link">here</a>.

### Getting a Value in a Path With Nulls

Sometimes we need to do the opposite of what we did in the previous chapter. Instead of stopping at the first non-null value, we continue until we've found the last non-null value.

For example, suppose a `customer` record type with two attributes:

-   `name`: a non-null string

-   `address`: a nullable address

Record type `address` is defined as follows:

-   `city`: a nullable string

-   `country`: a non-null string

Now we want to create a function that takes a customer as input, and returns the number of characters in the customer's city. If the customer's address attribute is `null`, or if the address's `city` attribute is `null` then the function should return 0.

#### Java

These are the record types written in idiomatic Java:

    static class Customer {

        private final String name;
        private final Address address;

        public Customer ( String name, Address address) {
            this.name = name;
            this.address = address;
        }

        public String getName() { return name; }
        public Address getAddress() { return address; }
    }

    static class Address {

        private final String city;
        private final String country;

        public Address ( String city, String country) {
            this.city = city;
            this.country = country;
        }

        public String getCity() { return city; }
        public String getCountry() { return country; }
    }

Note: We don't use setters because we want our types to be immutable.

As seen already, all types are nullable in Java. We cannot explicitly specify if `null` is allowed for class fields.

Function (method) `customerCitySize` can be implemented as follows:

    static Integer customerCitySize ( Customer customer ) {

        Address address = customer.getAddress();
        if ( address == null ) return 0;

        String city = address.getCity();
        if ( city == null ) return 0;
        
        return city.length();
    }

Alternatively we could have used nested if statements, but the above version is more readable and avoids the complexity of nested statements.

We can write a simplistic test:

    public static void main ( String[] args ) {

        // city is non-null
        Address address = new Address ( "Orlando", "USA" );
        Customer customer = new Customer ( "Foo", address );
        System.out.println ( customerCitySize ( customer ) );

        // city is null
        address = new Address ( null, "USA" );
        customer = new Customer ( "Foo", address );
        System.out.println ( customerCitySize ( customer ) );

        // address is null
        customer = new Customer ( "Foo", null );
        System.out.println ( customerCitySize ( customer ) );
    }

Output:

    7
    0
    0

The whole Java source code is available <a href="https://github.com/pp-articles/null_vs_maybe/tree/master/examples/search_last_non_null/Java/SearchLastNonNullTest.java" class="pml-link">here</a>.

#### Haskell

Defining the record types is easy:

    data Customer = Customer {
        name :: String,
        address :: Maybe Address
    }

    data Address = Address { 
        city :: Maybe String,
        country :: String 
    }

There are several ways to write function `customerCitySize` in Haskell. Here is, I think, the most readable one for people more familiar with imperative programming. It uses the do notation:

    import Data.Maybe (fromMaybe)

    customerCitySize :: Customer -> Int
    customerCitySize customer =
        let sizeMaybe = do
            address <- address customer        -- type Address
            city <- city address               -- type String
            return $ length city               -- type Maybe Int
        in fromMaybe 0 sizeMaybe

Here is a version that doesn't use the do notation:

    customerCitySize :: Customer -> Int
    customerCitySize customer =
        let addressMaybe = address customer    -- type Maybe Address
            cityMaybe = addressMaybe >>= city  -- type Maybe String
            sizeMaybe = length <$> cityMaybe   -- type Maybe Int
        in fromMaybe 0 sizeMaybe

If we are careful with operator precedence, we can shorten the code:

    customerCitySize :: Customer -> Int
    customerCitySize customer =
        fromMaybe 0 $ length <$> (address customer >>= city)

Instead of using `fromMaybe` we can use `maybe` to provide the default value:

    customerCitySize :: Customer -> Int
    customerCitySize customer =
        maybe 0 length $ address customer >>= city

Yes, this code is concise. But there is a lot going on behind the scenes. *A looooot!*. To really understand the above code one has to understand Haskell. And yes, we use a monad, indicated by the bind operator `>>=` in the code. For more information please refer to Haskell's documentation.

We can write a quick test:

    main :: IO ()
    main = do

        -- city is defined
        let address1 = Address {city = Just "Orlando", country = "USA"}
        let customer1 = Customer {name = "Foo", address = Just address1}
        putStrLn $ show $ customerCitySize customer1

        -- city is not defined
        let address2 = Address {city = Nothing, country = "USA"}
        let customer2 = Customer {name = "Foo", address = Just address2}
        putStrLn $ show $ customerCitySize customer2

        -- address is not defined
        let customer3 = Customer {name = "Foo", address = Nothing}
        putStrLn $ show $ customerCitySize customer3

Again, the output is:

    7
    0
    0

The whole Haskell source code is available <a href="https://github.com/pp-articles/null_vs_maybe/tree/master/examples/search_last_non_null/Haskell/SearchLastNonNullTest.hs" class="pml-link">here</a>. There are also two examples of `customerCitySize` implementations that compile without errors, but produce wrong results.

#### PPL

First, the record types:

    record type customer
        attributes
            name string
            address address or null
        .
    .

    record type address
        attributes
            city string or null
            country string
        .
    .

Function `customerCitySize` is written like this:

    function customer_city_size ( customer ) -> zero_pos_32 =
        customer.address.null?.city.null?.size if_null: 0

Note the embedded `null?` checks. The evaluation of `customer.address.null?.city.null?.size` stops as soon as a `null` is detected in the chain. In that case, the whole expression evaluates to `null`.

The `if_null:` operator is used to return the default value `0` if the expression on the left evaluates to `null`.

Instead of `.null?.` we can also simply write `?.`. Hence the function can be shortened to:

    function customer_city_size ( customer ) -> zero_pos_32 =
        customer.address?.city?.size if_null: 0

Simple test code looks like this:

    function start

        // city is non-null
        const address1 = address.create ( city = "Orlando", country = "USA" )
        const customer1 = customer.create ( name = "Foo", address = address1 )
        write_line ( customer_city_size ( customer1 ).to_string )

        // city is null
        const address2 = address.create ( city = null, country = "USA" )
        const customer2 = customer.create ( name = "Foo", address = address2 )
        write_line ( customer_city_size ( customer2 ).to_string )

        // address is null
        const customer3 = customer.create ( name = "Foo", address = null )
        write_line ( customer_city_size ( customer3 ).to_string )
    .

Output:

    7
    0
    0

The whole PPL source code is available <a href="https://github.com/pp-articles/null_vs_maybe/tree/master/examples/search_last_non_null/PPL/search_last_non_null_test.ppl" class="pml-link">here</a>.

#### Comparison

Here is a copy of the three implementations:

-   Java

        static Integer customerCitySize ( Customer customer ) {

            Address address = customer.getAddress();
            if ( address == null ) return 0;

            String city = address.getCity();
            if ( city == null ) return 0;
            
            return city.length();
        }

-   Haskell

        customerCitySize :: Customer -> Int
        customerCitySize customer =
            maybe 0 length $ address customer >>= city

-   PPL

        function customer_city_size ( customer ) -> zero_pos_32 =
            customer.address?.city?.size if_null: 0

Comparisons
===========

Now that we know how the null pointer error is eliminated, let us look at some differences between using the `Maybe` monad in Haskell and null-safety in PPL.

Note: The following discussion is based on the Haskell and PPL examples shown in the previous chapters. Hence, some of the following observations are not valid in other languages that work in a similar way. For example, F\#'s `Option` type is very similar to Haskell's `Maybe` type, but these two languages are far from being the same. Reader comments about other languages are of course very welcome.

Source Code
-----------

Here is a summary of the differences we saw in the source code examples.

### Declaring the type of a nullable reference

Haskell: `Maybe string` (other languages use `Option` or `Optional`)

PPL: `string or null` (other languages use `string?`)

As seen already, the difference between Haskell and PPL is not just syntax. Both use different concepts.

Haskell uses the `Maybe` type with a generic type parameter. Form the Haskell doc.: "A value of type Maybe a either contains a value of type a (represented as Just a), or it is empty (represented as Nothing). The Maybe type is also a monad."

On the other hand, PPL uses union types to state that a value is either a specific type, or `null`.

### A non-null value used for a nullable type

Haskell: `Just "qwe"` (other languages: `Some "qwe"`)

PPL: `"qwe"`

This difference is important!

In Haskell `"qwe"` is not type compatible to `Just "qwe"`. Suppose the following function signature:

    foo :: Maybe String -> String

This function can be called as follows:

    foo $ Just "qwe"

But a compiler error arises if we try to call it like this:

    foo "qwe"

There are a few consequences to be aware of.

First, if a type changes from `Maybe T` to `T`, then all occurrences of `Just expression` must be changed to `expression`. The inverse is true too. A change from type `T` to `Maybe T` requires all occurrences of `expression` to be refactored to `Just expression`.

This is not the case in PPL. An expression of type `string` is type-compatible to an expression of `string or null` (but the inverse is not true). For example, the function ...

    foo ( s string or null ) -> string

... can be called like this:

    foo ( "qwe" )

If the function is later refactored to ...

    foo ( s string ) -> string

... then it can still be called with:

    foo ( "qwe" )

Secondly, in Haskell some functions with the same name might exist for input type `Maybe T`, as well as for input `T`. But the semantics are different. For example, `length "qwe"` returns 3 in Haskell, while `length $ Just "qwe"` returns 1. It is important to be aware of this, because there is no compile-time error if function `length` is used for an expression whose type changes from `Maybe T` to `T` or vice-versa.

Thirdly, one has to be aware of the possibility of nested `Maybe`s in Haskell. For example, suppose again we declare:

    data Customer = Customer {
        name :: String,
        address :: Maybe Address
    }

    data Address = Address { 
        city :: Maybe String,
        country :: String 
    }

What is the return type of the following function?

    customerCity customer = city <$> address customer

Is it `Maybe string`. No, it's `Maybe ( Maybe string )` - a nested `Maybe`. Ignoring this can lead to subtle bugs. For an interesting discussion see the Stackoverflow question <a href="https://stackoverflow.com/questions/19655870/simplifying-nested-maybe-pattern-matching" class="pml-link">Simplifying nested Maybe pattern matching</a>.

### 'No value' symbol

Haskell: `Nothing` (other languages: `None`)

PPL: `null` (other languages: `nil`, `void`, ...)

### Checking for null

Haskell (one way to do it):

    intToString :: Maybe Integer -> Maybe String
    intToString i = case i of
        Just 1  -> Just "one"
        Nothing -> Nothing
        _       -> Just "not one"            

Note: Omitting the `Nothing` case does not produce a compiler error. Instead, the function returns `Just "not one"` if it is called with `Nothing` as input.

PPL (new version):

    function int_to_string ( i pos_32 or null ) -> string or null = \
        case value of i
            when null: null
            when 1   : "one"
            otherwise: "not one"

Note: Omitting the `when null` case results in the following compiler error:

    Clause 'when null' is required because the case expression might be null at run-time.

### Providing a default non-null value

Haskell: `fromMaybe 0 size` (requires Data.Maybe; F\#: `defaultArg 0 size`)

PPL: `size if_null: 0` (other languages: `size ?: 0`, ( `?:` is sometimes called 'Elvis operator')

### Getting the first non-null value in a chain, or else a default value

Haskell:

    customerDiscount :: String -> Float
    customerDiscount customerID =
        let discountMaybe = discountFromNet customerID
                            <|> discountFromDB customerID
                            <|> discountFromCache customerID
        in fromMaybe 0.0 discountMaybe

PPL:

    function customer_discount ( customer_id string ) -> float_64 = \
        discount_from_net ( customer_id ) \
        if_null: discount_from_DB ( customer_id ) \
        if_null: discount_from_cache ( customer_id ) \
        if_null: 0.0

### Getting the last value in a chain, or else a default value

Haskell:

    customerCitySize :: Customer -> Int
    customerCitySize customer =
        maybe 0 length $ address customer >>= city

PPL:

    function customer_city_size ( customer ) -> zero_pos_32 =
        customer.address?.city?.size if_null: 0

Implementation
--------------

Back in 1965, Tony Hoare introduced `null` in ALGOL "simply because it was so easy to implement", as he said.

In Java, and probably most other programming languages, `null` is implemented by simply using the value `0` for a reference. That is to say, if we write something like `name = "Bob"`, then the memory address used for variable `name` contains the starting address of the memory block that stores the string value `"Bob"`. On the other hand, when `name = null` is executed, then the content of the memory address used for variable `name` is set to `0` (i.e. all bits set to zero). Easy and efficient, indeed!

A more thorough explanation is available in chapter *Run-time Implementation* of my article <a href="http://www.practical-programming.org/blog/meaning-of-null/index.html" class="pml-link">A quick and thorough guide to 'null'</a>.

So, implementing `null` is easy. However, adding null-*safety* to a language is a totally different story. Implementing compile-time-null-safety in a practical way is far from being easy. Adding good support to simplify null-handling as far as possible is a challenge. Adding null-safety and good support for null-handling makes life more difficult for language *creators*, but much easier for language *users* (i.e. software developers). This doesn't come as a surprise, though. It's just a frequently observed fact of life:

-   It is easy to make it difficult to use.

-   It is difficult to make it easy to use.

On the other hand, a type like `Maybe` can simply be added to the language's standard library, without the need for special support in the language.

In he case of Haskell, `Maybe` is a monad in the standard prelude, and Haskell's standard functional programming features are used to handle `Maybe` values.

Space and Time
--------------

Let's starts with `null`.

There are just two kinds of basic operations needed at run-time:

-   Assign `null` to a reference (e.g. `name = null`): this is typically done by just writing `0` to a memory cell.

-   Check if a reference points to `null` (e.g. `if name is null`): this is very quickly done by just comparing the content of a memory cell with `0`.

The conclusion is obvious: `null` operations are extremely space- and time-efficient.

On the other hand, using a wrapper type is probably less efficient, unless the compiler uses very clever optimizations.

As a general observation, it is probably fair to say that, for a given language, using a wrapper type cannot be made *faster* than using 0 for a `null` reference.

In practice, however, the performance difference might not be an issue in many kinds of applications.

A Note On The "Billion Dollar Mistake"
======================================

Yes, Tony Hoare stated that `null` has "probably caused a billion dollars of pain and damage in the last forty years".

However, a few seconds later he <a href="https://everythingsysadmin.com/2009/01/tony-hoare-apologizes-for-inve.html" class="pml-link">said</a> the following, which is really important, but often ignored:

> More recent programming languages like Spec\# have introduced declarations for non-null references. This is the solution, which I rejected in 1965.
>
> -- Tony Hoare

The mistake was not the invention of null per se. The mistake was the *lack of compile-time-null-safety and good support for null-handling* in programming languages.

As seen in this article it *is* possible to eliminate the null pointer error in languages that use `null`. No "billion-dollar mistake" anymore!

Isn't it amazing that it took the software development industry over 40 years to recognize this and start creating null-safe languages?

Summary
=======

Here is a summary of the key points:

**Java (and most other popular programming languages)**

-   All reference types are nullable.

-   `null` can be assigned to any reference (variable, input argument, function return value, etc.).

-   There is no protection against null pointer errors. They occur frequently and are the reason for the *billion-dollar mistake*.

**Haskell (and some other programming languages using Maybe/Option)**

-   `null` is not supported. Hence null pointer errors cannot occur.

-   The `Maybe` type (a monad with a type parameter) is used to manage the 'absence of a value'.

-   Pattern matching is used to test for `Nothing`.

-   Standard language features are used to handle `Maybe` values (e.g. the monad's `bind` operator `>>=`).

**PPL (and some other null-safe programming languages)**

-   By default all reference types are non-nullable and `null` cannot be assigned.

-   `null` is an ordinary type with the single value `null`. Union types are used to handle the 'absence of a value' (e.g. `string or null`).

-   The compiler ensures a `null`-check is done before executing an operation on a nullable type. Thus null pointer errors cannot occur.

-   The language provides specific support to simplify `null`-handling as far as possible. Null-handling code is concise, and easy to read and write.

Conclusion
==========

The aim of this article was to compare null-safety in PPL with the `Maybe` type in Haskell. We did this by looking at a number of representative source code examples.

By comparing two languages, we must of course be careful not to generalize our observations to all other languages.

However, in the context of this article we saw that:

-   Null-safety, as well as the `Maybe` type eliminate the null pointer error.

-   Using the `Maybe/Optional` type is easier to implement in a language than null-safety. It *simplifies life for language designers and implementers*.

-   Providing good support for null-safety is a challenge for language creators. However, it *simplifies life for developers*.


Header image by [dailyprinciples](https://pixabay.com/users/dailyprinciples-3836461/) from [Pixabay](https://pixabay.com).