United Kingdom: +44 (0)208 088 8978

Making partial functions total, continued

This week's blog post sees Matt wrap up the functional programming fundamentals series. He discusses converting a partial function to a total function by constraining its input values.

We're hiring Software Developers

Click here to find out more

So far in the functional programming fundamentals blog series we've looked at pure functions, their benefits and immutability and expressions. My plan at this point was to tell you about partial functions, the benefits of total functions by comparison and a way of converting partial functions into total functions. Prash has already written that post and it's excellent! Check it out before reading this post.

Prash showed you one way to make partial functions total: extending the output set using the option type. Today, in the final post of this series, we'll generalise that approach and discuss a different one: restricting the input set.

Recap

The example of a partial function that Prash gave was List.head : 'T list -> 'T. It is partial because there are some 'T lists for which the output value is not of the stated type, 'T. In particular, List.head [] results in an exception.

Prash then compared it to the total function List.tryHead : 'T list -> 'T option. It is total because no matter what input value you give it, you always get an output value of the stated type, never an exception. It is in some sense the same function as List.head, but made total: if list is non-empty, List.tryHead list is Some (List.head list), so the output set is conceptually the same in either case, except that we've added a new value—None—for input values that previously resulted in exceptions to map to.

Generalisation: extending the output set

This approach can be applied more generally: given a partial function, we can make a total function by adding new values to the output set, to which we can map input values that previously resulted in exceptions.

For example, you might have a function to parse a string as an email address

type Email = Email of string

open System

let parseEmailPartial s =
    if String.IsNullOrWhiteSpace s then
        failwith "Email cannot be empty"
    else if (let chars = s :> seq<Char> in not (Seq.contains '@' chars)) then
        failwith "Email must have '@'"
    else
        Email s

You can make this total by using the Result type.

[<RequireQualifiedAccess>]
type EmailParsingFailure =
    | Empty
    | MissingAtSign

let parseEmailTotal s =
    if String.IsNullOrWhiteSpace s then
        Error EmailParsingFailure.Empty
    else if (let chars = s :> seq<Char> in not (Seq.contains '@' chars)) then
        Error EmailParsingFailure.MissingAtSign
    else
        Ok (Email s)

As in the previous example, valid input values are mapped to output values that are wrapped in a case signalling success and previously invalid input values are mapped to a value indicating invalid input, rather than throwing an exception. In this example, we've added two possible values to the output set, not just one. Now a developer using this function is prompted that the function might fail and can match the error cases to react appropriately.

Be careful not to take this approach too far: in many cases, you're better off using Exceptions.

Alternative: restricting the input set

We now know that partial functions can be made total by extending the output set. There is another option though: we can restrict the input set that the function takes so that the invalid values are removed.

Going back to the example of List.head, we could make a NonEmptyList type that guarantees that there is at least one element in the list and a corresponding NonEmptyList.head : NonEmptyList<'T> -> 'T function. Since the input always has at least one element, we know that we can always get the first (head) element from it.

type NonEmptyList<'T> = private NonEmpty of 'T list

[<RequireQualifiedAccess>]
module NonEmptyList =
    let tryCreate = function
        | [] -> None
        | list -> Some (NonEmpty list)

    let head nonEmptyList =
        match nonEmptyList with
        | NonEmpty list -> List.head list

[ 1; 2 ]
|> NonEmptyList.tryCreate
|> Option.map NonEmptyList.head // 1.

NonEmptyList.head is total: assuming they're created using NonEmptyList.tryCreate, all input values result in a valid output value.

Summary

In this post we saw that there are two ways to convert partial functions into total functions: mapping previously invalid input values to new output values or prohibiting invalid input. Both techniques are useful for making your code more robust.