In the previous post, we introduced
Functors, and observed a symmetry between operating with unaugmented functions on unaugmented and augmented values.
In this post, we’ll talk about how to augment functions as well, and then operate with them on augmented values.
Let’s consider addition:
We know how to do addition with unaugmented values in F#
But what happens when we want to add augmented values? Clearly, the following will not work
Of course, we could write some form of magic operator in
Maybe to handle addition, but this approach has two severe limitations:
- We would have to write a similar function for every other function that operates on unaugmented values - subtraction, multiplication and everything else. Clearly this is painful.
- We would have to replicate this effort for every other functor. This is exponentially more painful, and perhaps would prevent us from embarking down the road of using these structures in the first place… (Maybe this is why these approaches are not commonplace with other languages?)
Clearly we need a nicer way to apply existing functions.
Part of the reason this is a problem is because
(+) takes two arguments. If it had just taken one argument, we could use
Map to allow
Maybe to operate on the internally stored raw value with any old properly-typed function! Now that we have two arguments, we need to support partial application, and that is going to mess things up for us. Basically, what we need is some way to “lift” a function into the
Maybe context in a general way so we can support partial application.
In F#, partial application is the normal way to operate on functions with more than one argument. This automatically happens with anonymous lambdas but let’s get explicit here so we can see what’s going on
We have already encountered
Map. So far, we have written it as:
We could flip the arguments without any change to its behaviour
If we squint a little, we can see
Map as a function that takes a function of type
('a -> 'b), and returns a function of type
(F<'a> -> F<'b>). One might say that
Maybe.Map lifts a function
f into the
Maybe context. The lifted function is surely partially applicable, so perhaps we only need to use
Map for the
(+) as well!
But, alas, this is not quite sufficient, because there’s no way to apply a function of type
Maybe<int -> int> onto the second argument. Therefore, we need to extend
Maybe to give us this functionality.
This function applies a value of type
Maybe<'t> to a function of type
Maybe<'t -> 'r>, and returns a
Maybe<'r>. Now we have the building blocks in place to do this:
Let’s give this an infix form too!
which allows us to write
This is indeed quite elegant, although the bit of operator soup in the middle might seem somewhat irritating.
Let’s clean things up one more step by providing an operation to just lift something into the
Maybe context without actually mapping over a value:
We can now write something that should contrast very well indeed with our traditional function operations:
This is really very elegant indeed. It is very satisfying to get a visual similarity between partial application of unaugmented functions on unaugmented values, and partial application of augmented functions on augmented values.
This structure is still a
Functor, but the two new methods
Apply, which allow for a nice way to incorporate partial application within the functor’s context make it a special kind of functor called an Applicative Functor, or sometimes just an Applicative
In fact, in may usage scenarios, we may never actually use the
Map method explicitly, because, (and this is fun to work out),
We have come a long way. We started with functions and function application, chaining and composition; types, type composition and augmentation; functors and mapping; and now applicatives and (partial) application.
We had to put a fair bit of effort into arcane terminology, and do some mathematical gymnastics to get here.
Now, whilst all of this may be of academic interest, what does it have to do with real-world programming? In other words, why does knowing any of this help us in any practical way?
The next post addresses specifically this question. Read on!