F# Computations 2
Map
Common Names: map, fmap, lift, Select
Common Operators: <$> <!>
What it does: Lifts a function into the elevated world
Signature: (a->b) -> E<a> -> E<b>
.
Alternatively with the parameters reversed: E<a> -> (a->b) -> E<b>
Description “map” is the generic name for something that takes a function in the normal world and transforms it into a corresponding function in the elevated world.
An alternative interpretation of map is that it is a two parameter function that takes an elevated value E<a>
and a normal function a->b
, and returns a new elevated value E<b>
generated by applying the function a->b to the internal elements of E<a>
.
/// map for Options
let mapOption f opt =
match opt with
| None ->
None
| Some x ->
Some (f x)
// has type : ('a -> 'b) -> 'a option -> 'b option
/// map for Lists
let rec mapList f list =
match list with
| [] ->
[]
| head::tail ->
// new head + new tail
(f head) :: (mapList f tail)
// has type : ('a -> 'b) -> 'a list -> 'b list
Return
Common Names: return, pure, unit, yield, point
Common Operators: None
What it does: Lifts a single value into the elevated world
Signature: a -> E<a>
Description “return” (also known as “unit” or “pure”) simply creates an elevated value from a normal value.
This function goes by many names, but I’m going to be consistent and call it return as that is the common term for it in F#, and is the term used in computation expressions.
// A value lifted to the world of Options
let returnOption x = Some x
// has type : 'a -> 'a option
// A value lifted to the world of Lists
let returnList x = [x]
// has type : 'a -> 'a list
Apply
Common Names: apply, ap
Common Operators: <*>
What it does: Unpacks a function wrapped inside an elevated value into a lifted function E<a> -> E<b>
Signature: E<(a->b)> -> E<a> -> E<b>
Description
“apply” unpacks a function wrapped inside an elevated value (E<(a->b)>) into a lifted function E<a> -> E<b>
An alternative interpretation of apply is that it is a two parameter function that takes an elevated value (E<a>)
and an elevated function (E<(a->b)>)
, and returns a new elevated value (E<b>)
generated by applying the function a->b
to the internal elements of E<a>
.
For example, if you have a one-parameter function (E<(a->b)>)
, you can apply it to a single elevated parameter to get the output as another elevated value.
If you have a two-parameter function (E<(a->b->c)>)
, you can use apply twice in succession with two elevated parameters to get the elevated output.
module Option =
// The apply function for Options
let apply fOpt xOpt =
match fOpt,xOpt with
| Some f, Some x -> Some (f x)
| _ -> None
module List =
// The apply function for lists
// [f;g] apply [x;y] becomes [f x; f y; g x; g y]
let apply (fList: ('a->'b) list) (xList: 'a list) =
[ for f in fList do
for x in xList do
yield f x ]
let add x y = x + y
let resultOption =
let (<*>) = Option.apply
(Some add) <*> (Some 2) <*> (Some 3)
// resultOption = Some 5
let resultList =
let (<*>) = List.apply
[add] <*> [1;2] <*> [10;20]
// resultList = [11; 21; 12; 22]
Apply vs. Map
The combination of apply and return is considered “more powerful” than map, because if you have apply and return, you can construct map from them, but not vice versa.
Here’s how it works: to construct a lifted function from a normal function, just use return on the normal function and then apply. This gives you the same result as if you had simply done map in the first place.
let resultOption2 =
let (<!>) = Option.map
let (<*>) = Option.apply
add <!> (Some 2) <*> (Some 3)
// resultOption2 = Some 5
let resultList2 =
let (<!>) = List.map
let (<*>) = List.apply
add <!> [1;2] <*> [10;20]
// resultList2 = [11; 21; 12; 22]
let batman =
let (<!>) = List.map
let (<*>) = List.apply
// string concatenation using +
(+) <!> ["bam"; "kapow"; "zap"] <*> ["!"; "!!"]
// result =
// ["bam!"; "bam!!"; "kapow!"; "kapow!!"; "zap!"; "zap!!"]
Zip, ZipList
Common Names: zip, zipWith, map2
Common Operators: <*>
(in the context of ZipList world)
What it does: Combines two lists (or other enumerables) using a specified function
Signature: E<(a->b->c)> -> E<a> -> E<b> -> E<c>
where E is a list or other enumerable type, or E<a> -> E<b> -> E<a,b>
for the tuple-combined version.
Description Some data types might have more than one valid implementation of apply. For example, there is another possible implementation of apply for lists, commonly called ZipList or some variant of that.
In this implementation, the corresponding elements in each list are processed at the same time, and then both lists are shifted to get the next element. That is, the list of functions [f; g] applied to the list of values [x; y] becomes the two-element list [f x; g y]
// alternate "zip" implementation
// [f;g] apply [x;y] becomes [f x; g y]
let rec zipList fList xList =
match fList,xList with
| [],_
| _,[] ->
// either side empty, then done
[]
| (f::fTail),(x::xTail) ->
// new head + new tail
(f x) :: (zipList fTail xTail)
// has type : ('a -> 'b) -> 'a list -> 'b list
Bind
Called for let! and do! in computation expressions.
M<'T> * ('T -> M<'U>) -> M<'U>
The let!
keyword binds the result of a call to another computation expression to a name. let! is defined by the Bind(x, f) member on the builder type.
The do!
keyword is for calling a computation expression that returns a unit-like type (defined by the Zero member on the builder)
- Bind is also actually functional concepts than functions
- Bind is about lifting a function input that to an “Elavated” world
- Bind is similar to Linq’s SelectMany method
Common Names: bind, flatMap, andThen, collect, SelectMany
Common Operators: >>=
(left to right), =<<
(right to left )
What it does: Allows you to compose world-crossing (“monadic”) functions
Signature: (a->E<b>) -> E<a> -> E<b>
Alternatively with the parameters reversed: E<a> -> (a->E<b>) -> E<b>
Description We frequently have to deal with functions that cross between the normal world and the elevated world.
An alternative interpretation of bind is that it is a two parameter function that takes a elevated value E<a>
and a “monadic function” a -> E<b>
, and returns a new elevated value E<b>
generated by “unwrapping” the value inside the input, and running the function a -> E<b>
against it.
For example: a function that parses a string to an int might return an Option<int>
rather than a normal int, a function that reads lines from a file might return IEnumerable<string>
, a function that fetches a web page might return Async
These kinds of “world-crossing” functions are recognizable by their signature a -> E<b>
; their input is in the normal world but their output is in the elevated world. Unfortunately, this means that these kinds of functions cannot be linked together using standard composition.
type Int32 with
static member ParseAsOption str =
match Int32.TryParse (str:string) with
| false, _ -> None
| true, x -> Some x
let bindOption f opt =
match opt with
| Some x -> f x
| None -> None
let joinOption opt =
match opt with
| Some innerOpt -> innerOpt
| None -> None
let bindOption2 f opt = joinOption (Option.map f opt)
let input1 = Some "abcd" |> bindOption Int32.ParseAsOption
printfn "%A" input1
// None
let input2 = Some "100" |> bindOption2 Int32.ParseAsOption
printfn "%A" input3
// Some 100
let input3 = Some "200" |> Option.map Int32.ParseAsOption
printfn "%A" input3
// Some (Some 200)
let (>>=) m f = Option.bind f m
let add x y = x + y
let liftedAdd = Some add
let apply f m =
match f, m with
| Some f, Some m -> Some(f m)
| _ -> None
let (<*>) = apply
let input4 =
liftedAdd <*>
(Some "100" >>= Int32.ParseAsOption) <*>
(Some "200" >>= Int32.ParseAsOption)
printfn "%A" input4