I really enjoy trying to understand how and why things like work, but for this post I’m going to try to skip all that wonderful stuff and instead give a practical outline of how to use a very useful pattern arising from applicative functors.
I’ve found this pattern incredibly useful in F#, Swift and Haskell. The examples here are in F#, but as far as I can tell we can use it anywhere that has generic types and higher-order functions.
Aim
Say we have some generic type, let’s call it Widget<T>
(we’ll use the term “widget” as a placeholder for a generic type we are working with - feel free to substitute in Option<T>
, Either<E,A>
, Future<T>
, List<T>
etc.). There are lots of useful functions that work with non-widget types, and we would like them to work with Widget
values without having to re-write them.
// Some useful, non-widget functions:
(+) : Int -> Int -> Int
(::) : 'a -> ['a] -> ['a]
createThingoe :: Pop -> Blah -> Zap -> Thingoe
// Widget compatible versions:
widgetPlus : Widget<Int> -> Widget<Int> -> Widget<Int>
widgetCons : Widget<'a> -> Widget<'a> -> Widget<'a>
widgetThingoe : Widget<Pop> -> Widget<Blah> -> Widget<Zap> -> Widget<Thingoe>
Prerequisites
We can achieve this aim if the generic type has a map
(or Select
in C# terminology) and an apply
function. Continuing our Widget
example:
module Widget =
let map : ('a->'b) -> Widget<'a> -> Widget<'b> = ...
let apply : Widget<'a->'b> -> Widget<'a> -> Widget<'b> = ...
If the type does not have these functions provided we may still be able to write them. We’ll look at this later.
Apply pattern
We can use any non-widget function with widget values using map
for the first argument, and apply
for subsequent arguments.
let (<^>) = Widget.map
let (<*>) = Widget.apply
// Use non-widget function with non-widgets:
let normalResult =
nonWidgetFn firstArg secondArg thirdArg ... finalArg
// Use non-widget function with widgets. It's just like non-widget function
// application, only with more punctuation. :)
let widgetResult =
nonWidgetFn <^> firstWidget <*> secondWidget <*> thirdWidget <*> ... <*> finalWidget
// Convert any 2 argument function, (a -> b -> c) -> (Widget a -> Widget b -> Widget c)
let lift2 f a b = f <^> a <*> b
// Convert any 3 argument function:
let lift3 f a b c = f <^> a <*> b <*> c
// Convert any 4 argument function:
let lift4 f a b c d = f <^> a <*> b <*> c <*> d
// Widget-compatible plus and cons:
widgetPlus a b = (+) <^> a <*> b
widgetCons a b =
let cons a b = a :: b
cons <^> a <*> b
Example
Say we are using a library with a Result<'Error, 'T>
type that represents operations that can fail with a value of type 'Error
, or succeed with a value of type 'T
. The library also supplies map
and apply
functions for this type. We want to use this type to try to parse a Person
value from a UI form with name
, email
and age
text fields:
let nonEmpty (s : string) : Result<AppError, string> = ...
let validEmail (s : string) : Result<AppError, string> = ...
let parseInt (s : string) : Result<AppError, int> = ...
type Person = { name : string; email : string; age : int }
with
static member create a b c = { name=a; email=b; age=c }
// We want to use Person.create which takes strings and ints, but we need to try to
// parse values from text fields which will give us Result<AppError, string>
// and Result<AppError, int> values.
let (<^>) = Result.map
let (<*>) = Result.apply
Person.create <^> nonEmpty (name.text) <*> validEmail (email.text) <*> parseInt (age.text)
|> printfn "%A"
(*
When all fields are valid:
> Success {name = "Abc"; email = "[email protected]"; age = 42;}
When firstName.text is empty:
> Failed UnexpectedEmptyString
When age.text is invalid:
> Failed (CouldNotParseInt "12jf")
*)
When a generic type does not meet the prequisites
Sometimes a type will not have an apply
function provided, but will have map
, and also a flatMap
/bind
function provided with the following type:
// Also called "bind"
let flatMap : ('a-> Widget<'b>) -> Widget<'a> -> Widget<'b> = ...
This is the case with the F# Option module, which provides map
and bind
with the required signatures. In these cases we can implement apply
in terms of the these other functions:
module Option =
let apply ff a = Option.bind (fun f -> Option.map f a) ff
// General case:
module SomeOtherType =
let apply ff a = SomeOtherType.bind (fun f -> SomeOtherType.map f a) ff
We can now use the pattern with optionals (and any type with map
and flatMap
/bind
):
let (<^>) = Option.map
let (<*>) = Option.apply
let result : Option<int> = (+) <^> tryParseInt (first.text) <*> tryParseInt (second.text)
//> val result : Option<int> = Some 42
Mixing widget and non-widget arguments
In cases where we have a mix of arguments, some using our generic type and others not, we can still apply1 the pattern by converting the values to our generic type. For our Person.create
example, we could already have the person’s email as a valid string
value from earlier in the sign-up process:
let email : string = "[email protected]"
Person.create <^> nonEmpty (name.text) <*> Success email <*> parseInt (age.text)
|> ...
Here we convert email
from a string
to a Result<AppError,string>
value first using the Success
constructor. Then we have our three Result<AppError,'T>
values to use with the apply pattern.
Summary
This pattern is useful for being able reuse all our existing functions in the context of another type, like Future<T>
, Option<T>
, Result<E,A>
and lots, lots more. To do this for some generic type Widget<T>
we need:
let map : ('a -> 'b) -> Widget<'a> -> Widget<'b>
let apply : Widget<'a -> 'b> -> Widget<'a> -> Widget<'b>
// Alternatively, can also use bind/flatMap to get an apply function
let flatMap : ('a -> Widget<'b>) -> Widget<'a> -> Widget<'b>
We then apply the non-widget function to the first argument using map
, and use apply
for subsequent applications.
let (<^>) = Widget.map
let (<*>) = Widget.apply
let result : Widget<A> =
nonWidgetFn <^> firstWidgetArg <*> secondWidgetArg <*> ... <*> lastWidgetArg
Calls look similar to regular function application, with the additional operators taking care of conversion into our Widget<T>
context.
We can mix widget and non-widget arguments by converting non-widgets:
let result : Widget<A> =
nonWidgetFn <^> firstWidgetArg <*> toWidget secondArg <*> ... <*> lastWidgetArg
I wrote a bit more about how this works a while back, or search around for “applicative functor” if you are interested in the theory behind the practice. We can effectively use this pattern without delving into the details though - so we can apply now and ask questions later. :)
Sorry.↩