On being exceptional

What a better way to finish a tour than by talking about failing, right? Different languages solve the problem of failure differently. Many use the concept of exceptions: when an exception occurs, the program breaks its control flow and starts back-tracking the calls until it finds an appropriate exception handler. Other languages, such as Go do it differently, they employ error values that the programmer must manually check.

In this part, we'll talk about how error handling can be done in Funky. Of course, just like pretty much anything else, error handling isn't built-in to the language but is a part of the standard library instead.

The standard library currently only implements the simplest form of error handling: the Maybe type. More sophisticated error types will be added when the need and specific use-cases arise.

The Maybe type

Most functions always succeed. But some can also fail. To represent failure, we need a new type. The simplest (and so far only) type for this purpose is Maybe:

union Maybe a = none | some a

It has two alternatives: none and some. The former is a failure. The latter is a success.

Do you remember the list manipulation functions first! and rest!? Well, they crash when the list is empty. Crashing the whole program isn't usually desirable and so there are non-crashing alternatives: first and rest. What do they return? A Maybe of course:

func first : List a -> Maybe a
func rest  : List a -> Maybe (List a)

For example:

func main : IO =
    let [1, 2, 3, 4] \nums
    println (
        switch first nums
        case none
            "nothing there"
        case some \x
            "the first is " ++ string x
    );
    quit

It works:

$ funkycmd maybe.fn
the first is 1

If we don't wanna use the switch, we can use none? and extract! instead:

func main : IO =
    let [1, 2, 3, 4] \nums
    println (
        let (first nums) \maybe-x
        if (none? maybe-x) "nothing there";
        "the first is " ++ string (extract! maybe-x)
    );
    quit

Note. The function extract! can crash, so use at your own risk.

Yet another alternative would be to use ? and map. They're quite simple, but I'll explain them. Here are their types:

func ?   : a -> Maybe a -> a
func map : (a -> b) -> Maybe a -> Maybe b

The ? function takes a default value and a maybe and evaluates to the default value if the maybe is none. Otherwise, it evaluates to the value inside the maybe. For example: 0 ? none evaluates to 0, while 0 ? some 3 evaluates to 3.

The map function transforms the value inside a maybe using the supplied function. If the maybe is none, the result will be none as well. For example: map (+ 1) none results in none, while map (+ 1) (some 3) results in some 4. It works very much like map for lists.

We can use the map function to turn the Maybe Int obtained from first nums into a Maybe String like this:

map (\x "the first is " ++ string x) (first nums)

Then we can use ? to fill the case when the list is empty:

"nothing there" ? map (\x "the first is " ++ string x) (first nums)

And here's the final program:

func main : IO =
    let [1, 2, 3, 4] \nums
    println ("nothing there" ? map (\x "the first is " ++ string x) (first nums));
    quit

That line is quite long. Too long!

But, there's a solution! The functions ? and map have synonyms that are suitable for vertical code. Namely: if-none and let-some.

func if-none  : a -> Maybe a -> a
func let-some : Maybe a -> (a -> b) -> Maybe b

The function if-none is a precise synonym of ?. And the only difference between let-some and map is the order of arguments.

We use let-some to safely extract a value from a maybe:

let-some (first nums) \x
"the first is " ++ string x

The whole thing becomes a new maybe. To turn it into a naked value, we handle the none case:

if-none "nothing there";
let-some (first nums) \x
"the first is " ++ string x

And here's the whole thing:

func main : IO =
    let [1, 2, 3, 4] \nums
    println (
        if-none "nothing there";
        let-some (first nums) \x
        "the first is " ++ string x
    );
    quit

That's more readable!

Note. Of course, there are many cases when ? and map are the right choices. Perhaps this one is too. Depends on your personal preference.

In this short refactoring exercise, we've seen most of the useful Maybe functions: none?, extract!, ?, map, if-none, let-some.

There are some more, like let-::, some?, for-some, filter-some, and when-some. You can try them out on your own.

Just for the sake of giving another example, here's a simple function to get the third element of a list:

func third : List a -> Maybe a =
    \list
    let-some (rest list) \tail
    let-some (rest tail) \tail
    first tail

Or, here's a more complicated example that uses let-:: (used for destructuring lists into the first element and the rest, otherwise very similar to let-some):

func merge : List Int -> List Int -> List Int =
    \left \right
    if-none (left ++ right);
    let-:: left  \l \ls
    let-:: right \r \rs
    if (l < r) (l :: merge ls right);
    r :: merge left rs

func merge-sort : List Int -> List Int =
    \list
    let (length list) \len
    if (len <= 1) list;
    let (take (len / 2) list) \left
    let (drop (len / 2) list) \right
    merge (merge-sort left) (merge-sort right)