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.
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
[<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.
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.