The previous sections demonstrated how to define F# computation expression for monoidal and monadic
computations. In this section, we look at computations that combine multiple aspects. We look at
computations that form additive monads (also called MonadPlus in Haskell), which means that they
provide both monadic and monoidal operations.
An additive monad is a monad (with bind and return) that also implements a binary operation
combine (that can be used for combining two computations) and has a unit element. Two examples
of additive monads are parsers (where binary operation represents non-deterministic choice) and
lists or other collections (where binary operation is the concatenation).
The following snippet defines simple parser combinators and implements the basic operations that are
required in order to define a computation builder. The parser is represented as a function that takes
a list of characters and returns an sequence of possible results. Every element contains the
parsed result and an unconsumed buffer. Note that the zero operation is the
unit with respect to combine.
typeParser<'T>=Pof (list<char>->seq<'T*list<char>>)
/// Run the parser and return the first result (or None)letrun (Pf) =List.ofSeq>>f>>Seq.mapfst>>Seq.tryPickSomemoduleParsers=/// Parser that succeeds without consuming any inputletunitv=P (funbuffer->seq [v, buffer])
/// Runs the first parser and then a parser /// generated by the provided functionletbindf (Pp1) =P (funbuffer->seq { for (v, buffer) inp1bufferdolet (Pp2) =fvyield!p2buffer })
/// Represents a parser that always failsletzero () =P (funbuffer->Seq.empty)
/// Non-deterministic choice - combine all possible resultsletcombine (Pp1) (Pp2) =P (funbuffer->Seq.concat [ p1buffer; p2buffer ])
Using the above operations, we can define a computation builder for working with additive monads.
The computation Parser<'T> can capture untracked effects (in the function) and so we also provide
an explicit Delay member (similarly to the monadic examples). Although the
computation builder looks similar to the one for monads, it behaves quite differently, because
zero and combine are unique operations that are not defined in terms of bind and return:
/// Computation builder for working with parserstypeParserBuilder() =memberx.Return(v) =Parsers.unitvmemberx.ReturnFrom(m) =mmemberx.Bind(v, f) =Parsers.bindfvmemberx.Zero() =Parsers.zero()
memberx.Combine(p1, p2) =Parsers.combinep1p2memberx.Delay(f) =P (funbuffer->let (Pop) =f () inopbuffer)
/// A unique instance of the computation builderletparse=ParserBuilder()
Programming with parsers
Using the above definition, we can write a number of interesting parsers as a computation. The
following two examples (from the paper) represent parsers that repeat the specified parser p
one or more times and zero or more times respectively:
/// Parse one or more occurrences of 'p'letreconeOrMorep=parse {
let!x=plet!xs=zeroOrMorepreturnx::xs }
/// Parse zero or more occurrences of 'p'andzeroOrMorep=parse {
return!oneOrMorepreturn [] }
As discussed in the paper, the oneOrMore computation uses the monadic interface. It
sequentially composes parser p follwed by zeroOrMore p, which means that it always needs
to parse p first (at least once). The parser zeroOrMore uses the monoidal interface - it
combines the results of oneOrMore p (which parses p at least once) with a parser that
succeeds immediately, returning an emtpy list. In the translation, the first function
uses Bind and Return, while the second one uses Combine:
To run the sample parsers defined earlier, we need to add a number of primitive parsers.
In the following snippet, sat parses a single character if it matches a given predicate
and letter with number parse individual letter or numeric symbol, respectively:
/// Primitive parser that parses a char if it matches a predicateletsatf=(...)// Parsers that recognize single letter and numberletletter=satSystem.Char.IsLetterletnumber=satSystem.Char.IsNumber// Examples: Parse one or more & zero or more numbersrun (oneOrMorenumber) "hi"run (oneOrMorenumber) "123"run (zeroOrMorenumber) "hi"run (zeroOrMorenumber) "123"
The last four lines can be evalauted to demonstrate the behaviour of oneOrMore and zeroOrMore.
The first one fails when given "hi", because that string does not start with any number.
On the other hand, zeroOrMore succeeds, returning an empty list (of parsed numeric characters)
as the result.
We could extend the parse computation a bit further, but that is beyond the scope of this
online appendix. In particular, it would be possible to provide for and while loops in a
standard fashion (similarly to monads) in order to write parsers that consume some parser in
a loop. This would give us an elegant way to parse, for example, constant number of repetitions.
Other examples of a computation that matches the additive monad pattern are lists, sequences
and other collections. The built-in computation builder seq { .. } can be viewed as an instance
of this structure, but it uses quite different syntax. This is the case, because sequences
emphasize the monoidal structure (which uses the yield keyword) in favor of the monadic
structure (using let! and return). The standard operations for the seq<'T> type
can be defined as follows (here, we use the same naming as in the previous example):
moduleSequence=/// Returns a singleton sequence containing just 'v'letunitv=Seq.singletonv/// Apply 'f' to all elements and concatenate generated sequencesletbindfs=s|>Seq.mapf|>Seq.concat/// Concatenate elements of two sequencesletcombines1s2=Seq.concat [ s1; s2 ]
/// Returns an empty sequenceletzero () =Seq.empty
The computation builder uses unit in the definition of the Yield member (and it also
defines YieldFrom instead of ReturnFrom to enable the yield! keyword). For sequences,
the type of the standard control flow construct for overlaps with the type of let! (bind)
and so we implement For using bind in order to make the computation builder easier to use:
/// Computation builder for working with sequencestypeSeqBuilder() =memberx.Yield(v) =Sequence.unitvmemberx.YieldFrom(m) =mmemberx.For(v, f) =Sequence.bindfvmemberx.Zero() =Sequence.zero()
memberx.Combine(p1, p2) =Sequence.combinep1p2memberx.Delay(f) =Seq.delayf/// A unique instance of the computation builderletsq=SeqBuilder()
Just like parsers, sequences can capture untracked F# effects, so the type of delayed computations
is also seq<'T>. The Delay member can be implemented using a standard library function
Seq.delay.
Programming with sequences
The following example shows how to generate a sequence of Fibonacci numbers using the
computation builder:
letrecfibs=sq { // Yield the first two elementsyield!seq [1; 1]
// Generate by adding previous and pre-previous numbersfora, binSeq.zipfibs (Seq.skip1fibs) doyielda+b }
// Run this code to generate a sample sequencefibs|>Seq.take12|>List.ofSeq
The translated code combines both monadic and monoidal operations. The operation Combine is used
to combine the first two elements of the sequence with the rest of the sequence generated by for.
The for keyword corresponds to monadic bind (although exposed as the For member) and it performs
a simple map operation over a sequence:
letrecfibs=sq.Delay(fun () ->sq.Combine
( sq.YieldFrom( seq [1; 1] ),
sq.Delay(fun () ->sq.For(Seq.zipfibs (Seq.skip1fibs), fun (a, b) ->sq.Yield(a+b))) ))
Additive monad laws
To conclude, additive monads also obey a set of laws. Although these are less widely known (and standardized)
than for monads or monoids, they form an important component of the interface. They can be expressed in
terms of both notations that were used above - we use the first one that is, arguably, more standard.
As expected, the laws overlap with the laws that were discussed earlier about monads and monoids:
/// The associativity monoid law letmonoidAssociativityn1n2n3=letm1=m { return!n1return!n2return!n3 }
letm2=m { return!m { return!n1return!n2 }
return!n3 }
m1|>shouldEqualm2/// The unit monoid laws letunitn1n2=letm1=m { return!n1iffalsethenreturn!n2 }
letm2=m { iffalsethenreturn!n2return!n1 }
letm3=m { return!n1 }
m1|>shouldEqualm3m2|>shouldEqualm3/// The left identity monad lawletleftIdentityxf=letm1=m { let!v=m { returnx }
return!fv }
letm2=m { return!fx }
m1|>shouldEqualm2/// The right identity monad law letrightIdentityn=letm1=m { let!x=nreturnx }
letm2=m { return!n }
m1|>shouldEqualm2/// The associativity monad law letmonadAssociativitynfg=letm1=m { let!y=m { let!x=nreturn!fx }
return!gy }
letm2=m { let!x=nreturn!m { let!y=fxreturn!gx } }
letm3=m { let!x=nlet!y=fxreturn!gx }
m1|>shouldEqualm2m1|>shouldEqualm3/// Left zero law of additive monadsletleftZeron1k=letm1=m { let!v=m { iffalsethenreturn!n1 }
return!kv }
letm2=m { iffalsethenreturn!n1 }
m1|>shouldEqualm2
The only law that has not been discussed previously for either monads or
monoids is the last law, which captures the relationship between monadic
bind and the zero operation. In Haskell, the law is usually expressed as
mzero >>= k = mzero.
The listing above includes widely accepted laws only. However, additional laws such as left catch and
left distribution (see MonadPlus page) could be
expressed using the computation expression notation as well.
type Parser<'T> = | P of (char list -> seq<'T * char list>)
Full name: TryJoinads.Parser<_>
union case Parser.P: (char list -> seq<'T * char list>) -> Parser<'T>
type 'T list = List<'T>
Full name: Microsoft.FSharp.Collections.list<_> type: 'T list
Multiple items val char : 'T -> char (requires member op_Explicit)
Full name: Microsoft.FSharp.Core.Operators.char
-------------------- type char = System.Char
Full name: Microsoft.FSharp.Core.char type: char inherits: System.ValueType
Multiple items val seq : seq<'T> -> seq<'T>
Full name: Microsoft.FSharp.Core.Operators.seq
-------------------- type seq<'T> = System.Collections.Generic.IEnumerable<'T>
Full name: Microsoft.FSharp.Collections.seq<_> type: seq<'T> inherits: System.Collections.IEnumerable
val run : Parser<'a> -> (seq<char> -> 'a option)
Full name: TryJoinads.run
Run the parser and return the first result (or None)
val f : (char list -> seq<'a * char list>)
Multiple items module List
from Microsoft.FSharp.Collections
-------------------- type List<'T> = | ( [] ) | ( :: ) of 'T * 'T list with interface System.Collections.IEnumerable interface System.Collections.Generic.IEnumerable<'T> member Head : 'T member IsEmpty : bool member Item : index:int -> 'T with get member Length : int member Tail : 'T list static member Cons : head:'T * tail:'T list -> 'T list static member Empty : 'T list end
Full name: Microsoft.FSharp.Collections.List<_> type: List<'T>
val ofSeq : seq<'T> -> 'T list
Full name: Microsoft.FSharp.Collections.List.ofSeq
Full name: Microsoft.FSharp.Collections.Seq.tryPick
union case Option.Some: 'T -> Option<'T>
Multiple items val unit : 'a -> Parser<'a>
Full name: TryJoinads.Parsers.unit
Parser that succeeds without consuming any input
-------------------- type unit = Unit
Full name: Microsoft.FSharp.Core.unit type: unit
val v : 'a
val buffer : char list type: char list
val bind : ('a -> Parser<'b>) -> Parser<'a> -> Parser<'b>
Full name: TryJoinads.Parsers.bind
Runs the first parser and then a parser generated by the provided function
val f : ('a -> Parser<'b>)
val p1 : (char list -> seq<'a * char list>)
val p2 : (char list -> seq<'b * char list>)
val zero : unit -> Parser<'a>
Full name: TryJoinads.Parsers.zero
Represents a parser that always fails
val empty<'T> : seq<'T>
Full name: Microsoft.FSharp.Collections.Seq.empty
val combine : Parser<'a> -> Parser<'a> -> Parser<'a>
Full name: TryJoinads.Parsers.combine
Non-deterministic choice - combine all possible results
val p2 : (char list -> seq<'a * char list>)
val concat : seq<#seq<'T>> -> seq<'T>
Full name: Microsoft.FSharp.Collections.Seq.concat
type ParserBuilder = class new : unit -> ParserBuilder member Bind : v:Parser<'d> * f:('d -> Parser<'e>) -> Parser<'e> member Combine : p1:Parser<'b> * p2:Parser<'b> -> Parser<'b> member Delay : f:(unit -> Parser<'a>) -> Parser<'a> member Return : v:'g -> Parser<'g> member ReturnFrom : m:'f -> 'f member Zero : unit -> Parser<'c> end
Full name: TryJoinads.ParserBuilder
Computation builder for working with parsers
val x : ParserBuilder
member ParserBuilder.Return : v:'g -> Parser<'g>
Full name: TryJoinads.ParserBuilder.Return
val v : 'g
module Parsers
from TryJoinads
val unit : 'a -> Parser<'a>
Full name: TryJoinads.Parsers.unit
Parser that succeeds without consuming any input
member ParserBuilder.ReturnFrom : m:'f -> 'f
Full name: TryJoinads.ParserBuilder.ReturnFrom
val m : 'f
member ParserBuilder.Bind : v:Parser<'d> * f:('d -> Parser<'e>) -> Parser<'e>
Full name: TryJoinads.ParserBuilder.Bind
val v : Parser<'d>
val f : ('d -> Parser<'e>)
member ParserBuilder.Zero : unit -> Parser<'c>
Full name: TryJoinads.ParserBuilder.Zero
member ParserBuilder.Combine : p1:Parser<'b> * p2:Parser<'b> -> Parser<'b>
Full name: TryJoinads.ParserBuilder.Combine
val p1 : Parser<'b>
val p2 : Parser<'b>
member ParserBuilder.Delay : f:(unit -> Parser<'a>) -> Parser<'a>
Full name: TryJoinads.ParserBuilder.Delay
val f : (unit -> Parser<'a>)
val op : (char list -> seq<'a * char list>)
val parse : ParserBuilder
Full name: TryJoinads.parse
A unique instance of the computation builder
val oneOrMore : Parser<'a> -> Parser<'a list>
Full name: TryJoinads.oneOrMore
Parse one or more occurrences of 'p'
val p : Parser<'a>
val x : 'a
val xs : 'a list type: 'a list
val zeroOrMore : Parser<'a> -> Parser<'a list>
Full name: TryJoinads.zeroOrMore
Parse zero or more occurrences of 'p'
val oneOrMore : Parser<'a> -> Parser<'a list>
Full name: TryJoinads.ParseTranslation.oneOrMore
member ParserBuilder.Delay : f:(unit -> Parser<'a>) -> Parser<'a>
member ParserBuilder.Bind : v:Parser<'d> * f:('d -> Parser<'e>) -> Parser<'e>
val zeroOrMore : Parser<'a> -> Parser<'a list>
Full name: TryJoinads.ParseTranslation.zeroOrMore
member ParserBuilder.Return : v:'g -> Parser<'g>
member ParserBuilder.Combine : p1:Parser<'b> * p2:Parser<'b> -> Parser<'b>
member ParserBuilder.ReturnFrom : m:'f -> 'f
val sat : (char -> bool) -> Parser<char>
Full name: TryJoinads.sat
Primitive parser that parses a char if it matches a predicate
val f : (char -> bool)
P (fun buffer -> seq { match buffer with | x::xs when f x -> yield x, xs | _ -> () })
val letter : Parser<char>
Full name: TryJoinads.letter
namespace System
type Char = struct member CompareTo : obj -> int member CompareTo : char -> int member Equals : obj -> bool member Equals : char -> bool member GetHashCode : unit -> int member GetTypeCode : unit -> System.TypeCode member ToString : unit -> string member ToString : System.IFormatProvider -> string static val MaxValue : char static val MinValue : char static member GetNumericValue : char -> float static member GetNumericValue : string * int -> float static member GetUnicodeCategory : char -> System.Globalization.UnicodeCategory static member GetUnicodeCategory : string * int -> System.Globalization.UnicodeCategory static member IsControl : char -> bool static member IsControl : string * int -> bool static member IsDigit : char -> bool static member IsDigit : string * int -> bool static member IsLetter : char -> bool static member IsLetter : string * int -> bool static member IsLetterOrDigit : char -> bool static member IsLetterOrDigit : string * int -> bool static member IsLower : char -> bool static member IsLower : string * int -> bool static member IsNumber : char -> bool static member IsNumber : string * int -> bool static member IsPunctuation : char -> bool static member IsPunctuation : string * int -> bool static member IsSeparator : char -> bool static member IsSeparator : string * int -> bool static member IsSurrogate : char -> bool static member IsSurrogate : string * int -> bool static member IsSurrogatePair : string * int -> bool static member IsSurrogatePair : char * char -> bool static member IsSymbol : char -> bool static member IsSymbol : string * int -> bool static member IsUpper : char -> bool static member IsUpper : string * int -> bool static member IsWhiteSpace : char -> bool static member IsWhiteSpace : string * int -> bool static member ToLower : char -> char static member ToLower : char * System.Globalization.CultureInfo -> char static member ToLowerInvariant : char -> char static member ToString : char -> string static member ToUpper : char -> char static member ToUpper : char * System.Globalization.CultureInfo -> char static member ToUpperInvariant : char -> char static member TryParse : string * char -> bool end
Full name: System.Char type: System.Char inherits: System.ValueType
Full name: Microsoft.FSharp.Collections.Seq.singleton
val bind : ('a -> #seq<'c>) -> seq<'a> -> seq<'c>
Full name: TryJoinads.Sequence.bind
Apply 'f' to all elements and concatenate generated sequences
val f : ('a -> #seq<'c>)
val s : seq<'a> type: seq<'a> inherits: System.Collections.IEnumerable
val combine : 'a -> 'a -> seq<'b> (requires 'a :> seq<'b>)
Full name: TryJoinads.Sequence.combine
Concatenate elements of two sequences
val s1 : #seq<'b> type: #seq<'b>
val s2 : #seq<'b> type: #seq<'b>
val zero : unit -> seq<'a>
Full name: TryJoinads.Sequence.zero
Returns an empty sequence
type SeqBuilder = class new : unit -> SeqBuilder member Combine : p1:'b * p2:'b -> seq<'c> (requires 'b :> seq<'c>) member Delay : f:(unit -> seq<'a>) -> seq<'a> member For : v:seq<'e> * f:('e -> #seq<'g>) -> seq<'g> member Yield : v:'i -> seq<'i> member YieldFrom : m:'h -> 'h member Zero : unit -> seq<'d> end
Full name: TryJoinads.SeqBuilder
Computation builder for working with sequences
val x : SeqBuilder
member SeqBuilder.Yield : v:'i -> seq<'i>
Full name: TryJoinads.SeqBuilder.Yield
val v : 'i
module Sequence
from TryJoinads
val unit : 'a -> seq<'a>
Full name: TryJoinads.Sequence.unit
Returns a singleton sequence containing just 'v'
member SeqBuilder.YieldFrom : m:'h -> 'h
Full name: TryJoinads.SeqBuilder.YieldFrom
val m : 'h
member SeqBuilder.For : v:seq<'e> * f:('e -> #seq<'g>) -> seq<'g>
Full name: TryJoinads.SeqBuilder.For
val v : seq<'e> type: seq<'e> inherits: System.Collections.IEnumerable