On Thursday Nick Blair gave a fabulous talk on using the functional design pattern called “applicatives” for data processing and validation in the context of AWS cloud programming (content). As part of a cooperative follow-up, I thought it would be cool if those of us in the London F# community use this as a practical sample to proof a new technique that will become available in F# 5.0. The aim is to simplify this kind of code even further than before.
Applicatives can be seen as a way of combining computation elements together “statically” – in the sense that all combination can be done as a pre-phase, prior to using the resulting computation. The move from “dynamic composition” to “static composition” is crucial in many areas and is not just applicable to functional programming – it is also highly relevant to imperative programming that builds graphs.
For example, in RFC FS-1063 there is an example of graph building that shows a 10000x speedup difference (really, O(N) to O(1)) when using applicatives for defining computation graph nodes, rather than using monadic binding. Often many optimization steps can be applied during of after the composition phase as well.
In this post I will explain why, once RFC FS-1063 is available and out of “preview”, I would like to see it become a standard way of writing applicatives in F#.
For applicatives, F# code today is often characterized by the operators <!> and <*>, called “Style A” below. One RFC FS-1063 becomes widely available I’ll admit I’d like to see the <!> and <*> patterns eventually removed or minimized in common F# usage, though I understand it will take a while for this to happen. If you like this post can be read as “apply considered harmful” – though not applicatives themselves. That said, I can absolutely see why “Style A” is in common use in F# today, as for some use cases the positives outweight the negatives.
As a running example, let’s use these three Result values:
let resultValue1 = Ok 2
let resultValue2 = Ok 3 // or: Error "fail!"
let resultValue3 = Ok 4
Our job is to process these up to produce a new Result value Ok (2 + 3 - 4) = Ok 1 (or Error "fail" if the commented out code is enabled).
Style A: Applicatives with apply
Prior to F# 5.0 RFC FS-1063, a functional encoding of applicatives is generally used, e.g
let (<!>) f x = Result.map f x
let (<*>) f x = Result.apply f x
let myfunction (a:int) (b:int) (c:int) =
a + b - c
let res0 =
myfunction <!> resultValue1 <*> resultValue2 <*> resultValue3
The code the library defines is shown at the end of this post.
The negatives here are:
- The technique relies on knowledge that
myfunctionis a curried function andmyfunction <!> resultValue1is a partial application. - The technique relies on defining a new function
myfunctionto build the overall result of processing the inputs. This is, by design, a curried function and the order of “combination” ofmyfunctionreally matters, so if for example you writemyfunction <!> resultValue1 <*> resultValue3 <*> resultValue2by mistake you get the wrong result. The error is syntactically distant from thea + b - ccode which helps us undertstand it as an error. - The technique relies on defining operators
<!>and<*>which must either be globally or locally defined. If globally defined then type conflicts normally occur between different variations of these (or else people turn to the hyper-generic libraries like “FSharpPlus”). If locally defined then the code can easily tend towards the obscure. - The user must understand the precedence of
<!>and<*>. - The types involved are quite subtle. When hovering over
<!>you’ll see type parameters instantiated to long chains of curried parameter types. - The ‘apply’ operator can be somewhat mind-bending to even experienced F# developers.
Some positives are:
- The style is visually quite declarative
- It emphasises compositionality
- It’s available for use since F# 0.1
- It doesn’t suffer the negatives of Style B below
Style B: Applicatives with map2, map3, …
An alternative encoding of applicatives is sometimes used via map2, map3 etc., e.g
let res0 =
(resultValue1, resultValue2, resultValue3) |||> Result.map3 (fun v1 v2 v3 -> v1 + v2 - v3)
Here, the types are simpler and the code is not too hard to follow for any F# user familiar with ||> and |||>. Further there is no need to define the separate myfunction to process the results. The main problems with the above code are
- You need more and more operators
||||||>etc. - The associations
resultValue1<->v1etc. depend on argument position across a curried functional operator. When there are 10 or 20 things involved that’sa problem, and potentially a source of subtle mistakes.
That said in many contexts Style B is quite natural. For example I’ve found it natural and useful in dependency graph programming with FSharp.Data.Adaptive.
Style C: Applicatives With FS-1063
With FS-1063, the new style for applicatives available is as follows:
let res1 =
result {
let! a = resultValue1
and! b = resultValue2
and! c = resultValue3
return a + b - c
}
Here let! ... and! ... is understood as “merge the sources on the right and bind them simultaneously”. The code the library defines is shown at the end of this post.
The desugared code effectively uses Result.zip and Result.map. An optimized version can just use Result.map3. See the note at the end for the de-sugaring and computation expression builder definition.
Note that no Result.apply is defined at all, and in this case there is also no Result.bind. Only applicatives can be written with the above computation expression builder.
The cognitive basis for this technique is primarily a working knowledge of F# computation expressions and awareness that the let! .. and!... syntax is available.
A further example
The Amazon AWS DynamoDB reader code presented by Nick is an excellent guide to using F# with a cloud database. The only possible improvement FS-1063 brings is in the use of applicatives at the end of the post. Consider this code:
let buildCustomer id email verified dob balance =
{ Id = id
Email = email
IsVerified = verified
DateOfBirth = dob
Balance = balance }
let readCustomer() =
buildCustomer
<!> guidAttrReader "id"
<*> stringAttrReader "email"
<*> boolAttrReader "verified"
<*> dateAttrReader "dob"
<*> decimalAttrReader "balance"
With FS-1063, in Style C this becomes:
let readCustomer() =
attrReader {
let! id = guidAttrReader "id"
and! email = stringAttrReader "email"
and! verified = boolAttrReader "verified"
and! dob = dateAttrReader "dob"
and! balance = decimalAttrReader "balance"
return
{ Id = id
Email = email
IsVerified = verified
DateOfBirth = dob
Balance = balance }
}
Here, the advantages are the same as before – no artificial buildCustomer is needed, no currying or partial application is used.
Recommendation
From where I sit, there are strong reasons to prefer styles B and C over style A. Indeed, in teams
I work on I would always remove style A in favour of the others, especially once RFC FS-1063 is available.
So, once FS-1063 is available I strongly prefer that the F# community orient towards offering both Style B (if no computation expressions are used) and Style C (when computation expressions are used). In particular I’d like to see apply functions slowly disappear from the F# universe in favour of the other techniques.
Thank you
A big shout out to Tom Davies (@TD5), Nicholas Cowle and the major F# users G-Research who did the first prototype of FS-1063 – a great community-initiated contribution to the F# language.
Appendix: Trialling FS-1063 today
FS-1063 is only in preview. If you are programming with applicatives I’d strongly encourage
you to try it out today. For example, we may be able to greatly improve the error messages to guide the user towards the right solution, and it is possible we can improve aspects of the design as well.
The easiest way to get started is to open a Jupyter Notebook instance on Binder. This link will provision a docker container and load the .NET kernel with a preview version of F# (at the time of writing) that contains the feature.
To use the preview bits in Visual Studio, at the time of posting I did the following:
- I built the “master” branch of the F# repository
- I added
/langversion:previewto my command line arguments for both projects andfsi.exeinvocations. For projects I used –langversion:preview - I wanted to use the Visual F# Tools, so I did
.\build -c Release -deployExtensionsand started Visual Studio withdevenv /RootSuffix RoslynDev
This will be simpler when the next preview release of Visual Studio comes out (please submit a PR if there is an easier way to use the preview bits)
Appendix: Library Code for Style A
For Style A the library defines:
module Result =
let apply f x =
match f,x with
| Ok fres, Ok xres -> Ok (fres xres)
| Error e, _ -> Error e
| _, Error e -> Error e
let (<!>) f x = Result.map f x
let (<*>) f x = Result.apply f x
Appendix: Library Code for Style B
For Style B the library defines:
module Result =
let map2 f x1 x2 =
match x1,x2 with
| Ok x1res, Ok x2res -> Ok (f x1res x2res)
| Error e, _ -> Error e
| _, Error e -> Error e
let map3 f x1 x2 x3 =
match x1,x2,x3 with
| Ok x1res, Ok x2res, Ok x3res -> Ok (f x1res x2res x3res)
| Error e, _, _ -> Error e
| _, Error e, _ -> Error e
| _, _, Error e -> Error e
Appendix: Library Code for Style C
For Style C the library defines:
module Result =
let zip x1 x2 =
match x1,x2 with
| Ok x1res, Ok x2res -> Ok (x1res, x2res)
| Error e, _ -> Error e
| _, Error e -> Error e
type ResultBuilder() =
member _.MergeSources(t1: Result<'T,'U>, t2: Result<'T1,'U>) = Result.zip t1 t2
member _.BindReturn(x: Result<'T,'U>, f) = Result.map f x
let result = ResultBuilder()
Appendix: Should Applicative CEs also define ‘Bind’?
One of the hardest decisions in designing an applicative CE (i.e. one that supports MergeSources, BindReturn and potentially other CE constructs) is whether the CE builder should also define a Bind method.
First, some computations simply don’t admit a sensible “Bind”. These are often “two phase” computations where there is a strict separation between composition and runtime (see also “Custom operators” below). In this case it’s simple: no “Bind” is needed.
Further, if the builder does define Bind, your users can very easily write code that has low performance. RCF FS-1063 contains one example. As another example, if the user defines Bind
the following code will compile:
let readCustomer() =
attrReader {
let! id = guidAttrReader "id"
let! email = stringAttrReader "email"
let! verified = boolAttrReader "verified"
let! dob = dateAttrReader "dob"
let! balance = decimalAttrReader "balance"
return
{ Id = id
Email = email
IsVerified = verified
DateOfBirth = dob
Balance = balance }
}
will by its nature be of lower performance – perhaps drastically so – since the creation of the readers will be repeated on each step every time the actual read is performed. Further, no warning will be given that and! can be used here (because the first five computations are independent).
The simplest solution to this is simply to not have a Bind. Alternatively, you can define two CEs:
attrReader { ... }without aBind, for attribute readers whose compositions are effectively known statically (non data-dependent, non-parametric).attrReaderDynamic { ... }with aBind, for attribute readers whose compositions are both data-dependent and non-parametric.
Alternatively, you can support a Bind and leave the user to decide.
Similar care must be taken when adding other semantics such as TryFinally, TryWith, While, For and so on.
Appendix: On performance with applicatives
In the above example, the computation
let res1 =
result {
let! a = resultValue1
and! b = resultValue2
and! c = resultValue3
return a + b - c
}
is equivalent to
let res1 =
result.BindReturn(result.MergeSources(resultValue1, resultValue2, resultValue3), fun (a,b,c) -> a + b - c)
Through the addition of a Bind3Return method (i.e. Result.map3) the computation can be reduced to
result.Bind3Return(resultValue1, resultValue2, resultValue3, fun a b c -> a + b - c)
and highly efficient code can be generated can normally be generated by marking this function inline.
Appendix: Using custom operations with applicatives
It is possible to define custom operation for F# computations expressions that permit applicative syntax.
To understand what custom operators can and can’t do for applicatives it is important to
appreciate the difference between the “composition” of a typical applicative
and the “runtime” when the results of composition are used.
In the example below I show:
- A logging operation “logAtComposition” that is executed during composition of the applicative. Typically the parameters to such a custom opertion do not use
ProjectionParameterand do not have access to the variables in scope. - A logging operation “logAtRuntime” that is executed at the runtime of the applicative, every time it is used.
This has access to the variables in scope. This must logically happen at the “runtime” of the applicative. - A custom operation “checkAtRuntime” that is executed at the runtime of the applicative, every time it is used.
This has access to the variables in scope. However this can’t change the result of the composition.
The code below shows how to define and use these.
let r1 = Reader.ret 3
let r2 = Reader.ret 4
let r3 = Reader.ret 5
let reader1 =
reader {
let! a = r1
and! b = r2
and! c = r3
logAtRuntime (sprintf "a = %d, b = %d, c = %d" a b c)
logAtComposition "building!"
checkAtRuntime (a > b)
logAtComposition "building more!"
logAtRuntime "we don't get here"
return a + b + c
}
let res = Reader.run reader1
printfn "res = %A" res
The output is
building!
building more!
a = 3, b = 4, c = 5
res = Error "runtime check failed"
The library code necessary is below.
type Reader<'T> = Reader of (unit -> Result<'T, string>)
module Reader =
let run (Reader f) = f()
let ret x = Reader (fun () -> Ok x)
let map f (Reader r) =
Reader (fun () -> Result.map f (r()))
let zip (Reader r1) (Reader r2) =
Reader (fun () ->
match r1(), r2() with
| Ok res1, Ok res2 -> Ok (res1, res2)
| Error e, _ -> Error e
| _, Error e -> Error e
)
let checkAtRuntime f (Reader r) =
Reader (fun () ->
let res = r()
match res with
| Ok x ->
if not (f x) then Error "runtime check failed"
else res
| Error _ -> res
)
let logAtRuntime f (Reader r) =
Reader (fun () ->
let res = r()
match res with
| Ok x ->
printfn "%s" (f x)
res
| Error _ -> res
)
let logAtComposition msg (r: Reader<'T>) =
printfn "%s" msg
r
type ReaderBuilder() =
member _.MergeSources(t1, t2) = Reader.zip t1 t2
member _.BindReturn(x, f) = Reader.map f x
[<CustomOperation("checkAtRuntime", MaintainsVariableSpaceUsingBind = true) >]
member _.CheckAtRuntime(x: Reader<'T>, [<ProjectionParameter>] f: 'T -> bool) =
Reader.checkAtRuntime f x
[<CustomOperation("logAtComposition", MaintainsVariableSpaceUsingBind = true) >]
member _.LogAtComposition(x: Reader<'T>, msg: string) =
Reader.logAtComposition msg x
[<CustomOperation("logAtRuntime", MaintainsVariableSpaceUsingBind = true) >]
member _.LogAtRuntime(x: Reader<'T>, [<ProjectionParameter>] f: ('T -> string)) =
Reader.logAtRuntime f x
let reader = ReaderBuilder()
As additional examples:
- this gist shows how to define named node types where the
prefixcustom operator adds a prefix to the name of the node being defined. - There is another sample for logging in the F# test code.
Appendix: Debugging in applicative builders
The debugging experience inside typical applicative builders is mixed. Let’s use the Reader<'T> example above with user code
let r1 = Reader.ret 3
let r2 = Reader.ret 4
let r3 = Reader.ret 5
let reader1() =
reader {
let! a = r1
and! b = r2
and! c = r3
logAtRuntime (sprintf "a = %d, b = %d, c = %d" a b c)
logAtComposition "building!"
checkAtRuntime (a > b)
logAtComposition "building more!"
logAtRuntime "we don't get here"
return a + b + c
}
// Composition
let r= reader1()
// Runtime
let res = Reader.run r
printfn "res = %A" res
Here is the current behaviour:
- During composition (when
reader1()is called), the code location for de-sugared calls to composition functions such asreader.LogAtComposition,reader.LogAtRuntime,reader.BindReturn,reader.MergeSourcesis set atreader {inreader1(). You can’t, for example, place a breakpoint at the linelogAtComposition "building!". - At runtime, you can set breakpoints in the usual places – e.g.
logAtRuntimeor thea > bwithincheckAtRuntime.
In both cases setting a breakpoint at logAtComposition won’t trigger. You can set a breakpoint in the implementation of logAtComposition however. The assumption behind this is that in most CEs (e.g. async) the “composition” phase is relatively bug-free and not of interest in user-code. However this assumption is by no means always valid, and you should be aware
of the possible pitfalls here.
To workaround this problem, composition-time functions can take lambdas, for example:
logAtComposition (fun () -> "building!")
and a breakpoint can be placed inside the lambda. With this in place breakpoints can now be placed in custom operators in the composition logic. The library code is:
[<CustomOperation("logAtComposition", MaintainsVariableSpaceUsingBind = true) >]
member _.LogAtComposition(x: Reader<'T>, msg: unit -> string) =
printfn "%s" (msg())
r