πComputation expression (CE)
Intro
Presentation
π Learn F# - Computation Expressions, by Microsoft:
Computation expressions in F# provide a convenient syntax for writing computations that can be sequenced and combined using control flow constructs and bindings.
Depending on the kind of computation expression, they can be thought of as a way to express monads, monoids, monad transformers, and applicatives.
Note
monads, monoids, monad transformers, and applicatives
These are Functional patterns, except monad transformers π
Built-in CEs: async and task, seq, query
β Easy to use, once we know the syntax and its keywords
We can write our own CE too. β More challenging!
Syntax
CE = block like myCE { body } where body looks like imperative F# code with:
regular keywords:
let,do,if/then/else,match,for...dedicated keywords:
yield,return"banged" keywords:
let!,do!,match!,yield!,return!
These keywords hide a β machinery β to perform background specific effects:
Asynchronous computations like with
asyncandtaskState management: e.g. a sequence with
seqAbsence of value with
optionCE...
Builder
A computation expression relies on an object called Builder.
Warning
This is not exactly the Builder OO design pattern.
For each supported keyword (let!, return...), the Builder implements one or more related methods.
βοΈ Compiler accepts flexibility in the builder method signature, as long as the methods can be chained together properly when the compiler evaluates the CE on the caller side. β β Versatile, β οΈ Difficult to design and to test β Given method signatures illustrate only typical situations.
Builder example: logger {}
logger {}Need: log the intermediate values of a calculation
β οΈ Issues
Verbose: the
log xinterfere with readingError prone: forget a
log, log wrong value...
π‘ Solutions
Make logs implicit in a CE by implementing a custom let! / Bind() :
The 3 consecutive let! are desugared into 3 nested calls to Bind with:
1st argument: the right side of the
let!(e.g.42withlet! x = 42)2nd argument: a lambda taking the variable defined at the left side of the
let!(e.g.x) and returning the whole expression below thelet!until the}
Bind vs let!
Bind vs let!logger { let! var = expr in cexpr } is desugared as:
logger.Bind(expr, fun var -> cexpr)
π Key points:
varandexprappear in reverse ordervaris used in the rest of the computationcexprβ highlighted using theinkeyword of the verbose syntaxthe lambda
fun var -> cexpris a continuation function
CE desugaring: tips π‘
I found a simple way to desugar a computation expression: β Write a failing unit test and use Unquote - π Example

Constructor parameters
The builder can be constructed with additional parameters. β The CE syntax allows us to pass these arguments when using the CE:
Builder example: option {}
option {}Need: successively try to find in maps by identifiers β Steps:
roomRateIdinpolicyCodesByRoomRatemap β findpolicyCodepolicyCodeinpolicyTypesByCodemap β findpolicyTypepolicyCodeandpolicyTypeβ buildresult
Implementation #1: based on match expressions
Implementation #2: based on Option module helpers
Option module helpersπ Issues β οΈ:
Nesting too
Even more difficult to read because of parentheses
Implementation #3: based on the option {} CE
option {} CEπ Both terse and readable β π
CE monoidal
A monoidal CE can be identified by the usage of yield and yield! keywords.
Relationship with the monoid: β Hidden in the builder methods:
+operation βCombinemethodeneutral element βZeromethod
CE monoidal builder method signatures
Like we did for functional patterns, we use the generic type notation:
M<T>: type returned by the CEDelayed<T>: presented later π
CE monoidal vs comprehension
Comprehension definition
It is the concise and declarative syntax to build collections with control flow keywords
if,for,while... and rangesstart..end.
Comparison
Similar syntax from caller perspective
Distinct overlapping concepts
Minimal set of methods expected for each
Monoidal CE:
Yield,Combine,ZeroComprehension:
For,Yield
CE monoidal example: multiplication {}
multiplication {}Let's build a CE that multiplies the integers yielded in the computation body:
β CE type: M<T> = int β’ Monoid operation = * β’ Neutral element = 1
Exercise
Copy this snippet in vs code
Comment out builder methods
To start with an empty builder, add this line
let _ = ()in the body.After adding the first method, this line can be removed.
Let the compiler errors in
shouldBe10andfactorialOf5guides you to ad the relevant methods.
Desugared multiplication { yield 5; yield 2 }:
Desugared multiplication { for i in 2..5 -> i }:
CE monoidal Delayed<T> type
Delayed<T> typeDelayed<T> represents a delayed computation and is used in these methods:
Delayreturns this type, hence defines it for the CECombine,Run,WhileandTryFinallyused it as input parameter
Delayis called each time converting fromM<T>toDelayed<T>is neededDelayed<T>is internal to the CERunis required at the end to get back theM<T>...... only when
Delayed<T>βM<T>, otherwise it can be omitted
π Enables to implement laziness and short-circuiting at the CE level.
Example: lazy multiplication {} with Combine optimized when x = 0
Delay return type
int
unit -> int
Run
Omitted
Required to get back an int
Combine 2nd parameter
int
unit -> int
For calling Delay
Omitted
Explicit but not required here

CE monoidal kinds
With multiplication {}, we've seen a first kind of monoidal CE:
β To reduce multiple yielded values into 1.
Second kind of monoidal CE:
β To aggregate multiple yielded values into a collection.
β Example: seq {} returns a 't seq.
CE monoidal to generate a collection
Let's build a list {} monoidal CE!
Notes π‘
M<T>is't listβ type returned byYieldandZeroForuses an intermediary sequence to collect the values returned byf.
Let's test the CE to generate the list [begin; 16; 9; 4; 1; 2; 4; 6; 8; end]
&#xNAN;(Desugared code simplified)

Comparison with the same expression in a list comprehension:

list { expr } vs [ expr ]:
[ expr ]uses a hiddenseqall through the computation and ends with atoListAll methods are inlined:
Method
list { expr }
[ expr ]
Combine
xs @ ys => List.append
Seq.append
Yield
[x] => List.singleton
Seq.singleton
Zero
[] => List.empty
Seq.empty
For
Seq.collect & Seq.toList
Seq.collect
CE monadic
A monadic CE can be identified by the usage of let! and return keywords, revealing the monadic bind and return operations.
Behind the scene, builders of these CE should/can implement these methods:
CE monadic vs CE monoidal
Return (monad) vs Yield (monoid)
Return (monad) vs Yield (monoid)Same signature:
T -> M<T>A series of
returnis not expected β MonadicCombinetakes only a monadic commandM<unit>as 1st paramCE enforces appropriate syntax by implementing 1! of these methods:
seq {}allowsyieldbut notreturnasync {}: vice versa
For and While
For and WhileFor
Monoidal
seq<T> * (T -> M<U>) -> M<U> or seq<M<U>>
Monadic
seq<T> * (T -> M<unit>) -> M<unit>
While
Monoidal
(unit -> bool) * Delayed<T> -> M<T>
Monadic
(unit -> bool) * (unit -> M<unit>) -> M<unit>
π Different use cases:
Monoidal: Comprehension syntax
Monadic: Series of effectful commands
CE monadic and delayed
Like monoidal CE, monadic CE can use a Delayed<'t> type.
β Impacts on the method signatures:
CE monadic examples
βοΈ The initial CE studiedβlogger {} and option {}βwas monadic.
Let's play with a result {} CE!
Desugaring:
CE monadic: FSharpPlus monad CE
monad CEFSharpPlus provides a monad CE
Works for all monadic types:
Option,Result, ... and evenLazyπSupports monad stacks with monad transformers π
β οΈ Limits:
Confusing: the
monadCE has 4 flavours to cover all cases: delayed or strict, embedded side-effects or notBased on SRTP: can be very long to compile!
Documentation not exhaustive, relying on Haskell knowledges
Very Haskell-oriented: not idiomatic Fβ―
Monad stack, monad transformers
A monad stack is a composition of different monads.
β Example: Async+Option.
How to handle it? β Academic style vs idiomatic Fβ―
1. Academic style (with FSharpPlus)
Monad transformer (here MaybeT)
β Extends Async to handle both effects
β Resulting type: MaybeT<Async<'t>>
β reusable with other inner monad β less easy to evaluate the resulting value β not idiomatic
2. Idiomatic style
Custom CE asyncOption, based on the async CE, handling Async<Option<'t>> type
β οΈ Limits: not reusable, just copiable for asyncResult for instance
CE Applicative
An applicative CE is revealed through the usage of the and! keyword (Fβ― 5).
An applicative CE builder should define these methods:
CE Applicative example - validation {}
validation {}Usage: validate a customer
Name not null or empty
Height strictly positive
Desugaring:
CE Applicative trap
β οΈ The compiler accepts that we define ValidationBuilder without BindReturn but with Bind and Return. But in this case, we can loose the applicative behavior and it enables monadic CE bodies!
CE Applicative - FsToolkit validation {}
validation {}FsToolkit.ErrorHandling offers a similar validation {}.
The desugaring reveals the definition of more methods: Delay, Run, Sourceπ
Source methods
Source methodsIn FsToolkit validation {}, there are a couple of Source defined:
The main definition is the
idfunction.Another overload is interesting: it converts a
Result<'a, 'e>into aValidation<'a, 'e>. As it's defined as an extension method, it has a lower priority for the compiler, leading to a better type inference. Otherwise, we would need to add type annotations.
βοΈ Note: Source documentation is scarce. The most valuable information comes from a question on stackoverflow mentioned in FsToolkit source code!
Creating CEs
Types
The CE builder methods definition can involve not 2 but 3 types:
The wrapper type
M<T>The
Delayed<T>typeAn
Internal<T>type
M<T> wrapper type
M<T> wrapper typeβοΈ We use the generic type notation M<T> to indicate any of these aspects: generic or container.
Examples of candidate types:
Any generic type
Any monoidal, monadic, or applicative type
stringas it containscharsAny type itself as
type Identity<'t> = 'tβ see previouslogger {}CE
Delayed<T> type
Delayed<T> typeReturn type of
DelayParameter to
Run,Combine,While,TryWith,TryFinallyDefault type when
Delayis not defined:M<T>Common type for a real delay:
unit -> M<T>- seemember _.Delay f = f
Delayed<T> type example: eventually {}
Delayed<T> type example: eventually {}Union type used for both wrapper and delayed types:
The output values are maint to be evaluated interactively, step by step:
Internal<T> type
Internal<T> typeReturn, ReturnFrom, Yield, YieldFrom, Zero can return a type internal to the CE. Combine, Delay, and Run handle this type.
π‘ Highlights the usefulness of ReturnFrom, YieldFrom, implemented as an identity function until now.
Builder methods without type
Another trick regarding types
π‘ Any type can be turned into a CE by adding builder methods as extensions.
Example: activity {} CE to configure an Activity without passing the instance
Type with builder extension methods:
System.Diagnostics.ActivityReturn type:
unit(no value returned)Internal type involved:
type ActivityAction = delegate of Activity -> unitCE behaviour:
monoidal internally: composition of
ActivityActionlike a
Statemonad externally, with only the setter(s) part
The
activityinstance supports the CE syntax thanks to its extensions.The extension methods are marked as not
EditorBrowsablefor proper DevExp.Externally, the
activityis implicit in the CE body, like aStatemonad.Internally, the state is handled as a composition of
ActivityAction.The final
Runenables us to evaluate the builtActivityAction, resulting in the change (mutation) of theactivity(the side effect).
Custom operations π
What: builder methods annotated with [<CustomOperation("myOperation")>]
Use cases: add new keywords, build a custom DSL β Example: the query core CE supports where and select keywords like LINQ
β οΈ Warning: you may need additional things that are not well documented:
Additional properties for the
CustomOperationattribute:AllowIntoPattern,MaintainsVariableSpaceIsLikeJoin,IsLikeGroupJoin,JoinConditionWordIsLikeZip...
Additional attributes on the method parameters, like
[<ProjectionParameter>]
π Computation Expressions Workshop: 7 - Query Expressions | GitHub
CE creation guidelines π
Choose the main behaviour: monoidal? monadic? applicative?
Prefer a single behaviour unless it's a generic/multi-purpose CE
Create a builder class
Implement the main methods to get the selected behaviour
Use/Test your CE to verify it compiles (see typical compilation errors below), produces the expected result, and performs well.
CE creation tips π‘
Overload methods to support more use cases like different input types
Async<Result<_,_>>+Async<_>+Result<_,_>Option<_>andNullable<_>
CE benefits β
Increased Readability: imperative-like code, DSL (Domain Specific Language)
Reduced Boilerplate: hides a "machinery"
Extensibility: we can write our own "builder" for specific logic
CE limits β οΈ
Compiler error messages within a CE body can be cryptic
Nesting different CEs can make the code more cumbersome
E.g.
async+resultAlternative: custom combining CE - see
asyncResultin FsToolkit
Writing our own CE can be challenging
Implementing the right methods, each the right way
Understanding the underlying concepts
π Quiz
Question 1: What is the primary purpose of computation expressions in F#?
A. To replace all functional programming patterns
B. To provide imperative-like syntax for sequencing and combining computations
C. To eliminate the need for type annotations
D. To make F# code compatible with C#
Question 2: Which keywords identify a monadic computation expression?
A. yield and yield!
B. let! and return
C. let! and and!
D. do! and while
Question 3: In a computation expression builder, what does the Bind method correspond to?
Bind method correspond to?A. The yield keyword
B. The return keyword
C. The let! keyword
D. The else keyword when omitted
Question 4: What is the signature of a typical monadic Bind method?
Bind method?A. M<T> -> M<T>
B. T -> M<T>
C. M<T> * (T -> M<U>) -> M<U>
D. M<T> * M<U> -> M<T * U>
π CE wrap up
Syntactic sugar: inner syntax: standard or "banged" (
let!) β Imperative-like β’ Easy to useCE is based on a builder
instance of a class with standard methods like
BindandReturn
Separation of Concerns
Business logic in the CE body
Machinery behind the scene in the CE builder
Little issues for nesting or combining CEs
Underlying functional patterns: monoid, monad, applicative
Libraries: FSharpPlus, FsToolkit, Expecto, Farmer, Saturn...
π Additional resources
Code examples in FSharpTraining.sln βRomain Deneau
The "Computation Expressions" series βF# for Fun and Profit
All CE methods | Learn F# βMicrosoft
The F# Computation Expression Zoo βTomas Petricek and Don Syme
Documentation | Try Joinads βTomas Petricek
Last updated
Was this helpful?