This library contains some useful utility functions that feel missing from the F# core library. It also contains computation expression builders for Option and Result workflows.
The library is small and documented, so you could just read the source code, or keep reading for an overview of the additions and an in-depth look at the new workflows.
cnst
returns a function that returns the given constant value. Equivalent to(fun _ -> a)
.flip
flips the order of two arguments of a function.flip f a b = f b a
. Useful in pipelining when your piped value is the first argument to a function. Example:list1 |> flip List.append list2
. With the flip, the lists will occur in the resulting list in the order in which they appear in the code even though we're using a forward pipe. Without the flip, the elements oflist1
would occur after the elements oflist2
in the resulting list.tryResult
calls a function and returnsOk
of the result if no exception was thrown orError
of the thrown exception.tryCast<'a>
returnsSome
if the object is the given type; otherwise returnsNone
.nil
value is the nullSystem.Nullable<T>
value. This is purely to make it easier to refer to.|?
operator isdefaultArg
and equivalent to|> Option.defaultValue
.
This library extends several core modules:
String
is filled out with almost all System.String member functions as module functions, as well as a few new functions such asifEmpty
andifWhitespace
.Seq
has additions such asisNotEmpty
andtryMax
.Option
has additions such asiter2
,iter3
, andofCond
.Result
has additions to assist in validation workflows such asisOk
,isError
,okIf
,errorIf
,ofCond
,ofOption
,ofRegex
,accumulate
,partition
and more.Async
has additions to fill gaps in the base library:map
,mapAsync
, andAwaitPlainTask
.
The following new modules are introduced:
Parse
contains functions to parse strings into various types or check for parsability.Patterns
contains active patterns to parse strings.Tuple
contains some functions to construct tuples from arguments.Reflection
contains utilities that use reflection such asunionCaseName
.
The option
workflow is appropriate when you need to compute an option value from a chain of computations that return
an option.
let!
(Bind) calls Option.bind.return
wraps the value inSome
.- Zero returns
None
. - Combine calls
Option.orElseWith
which means that if you have more than one expression, the firstSome
value is returned orNone
is returned if all expressions evaluate toNone
. - Expression evaluation is delayed. If a bind expression returns
None
or if a non-last expression returnsSome
then the following code is not executed.
Let's look at an example where we perform a series of lookups. Using pattern matching, we might write:
// using pattern matching
let getCustomerNameForInvoice db invoiceId =
match db.GetInvoice invoiceId with
| Some inv ->
// an invoice might not have a customer ID
match inv.CustomerId with
| Some custId ->
match db.GetCustomer custId with
| Some cust -> Some cust.FullName
| None -> None
| None -> None
| None -> None
This function does a series of database lookups that return an option. The computation can only continue when each lookup succeeds.
Pattern matching is a bit cumbersome here. Every match against an option returns None when the input is None. Let's
replace the match
statements with Option.bind
to write our computation as a series of continuations.
// using Option.bind
let getCustomerNameForInvoice db invoiceId =
db.GetInvoice invoiceId
|> Option.bind (fun inv ->
inv.CustomerId
|> Option.bind (fun custId ->
db.GetCustomer custId
|> Option.map (fun cust ->
cust.FullName
)
)
)
Any time that you can write a computation as a series of continuations like this, a computation expression could make
it easier to read and write. Let's refactor again using the option
computation expression:
// using option builder
let getCustomerNameForInvoice db invoiceId = option {
let! inv = db.GetInvoice invoiceId
let! custId = inv.CustomerId
let! cust = db.GetCustomer custId
return cust.FullName
}
We've eliminated the nesting and made the code a straightforward series of bind statements with the let!
syntax.
The option
computation expression implements Combine with Option.orElseWith
, which means that multiple expressions
will evaluate to the first Some
value. We can use this to write code with "early returns" for Some
values.
Here's a contrived example of getting a shipping address where we can immediately return a value if a condition is met:
let getShippingAddress db order =
if not order.UseBillingAddress then
Some order.ShippingAddress
else
let pmt = db.GetPaymentInfo order.Id
pmt |> Option.map (fun p -> p.BillingAddress)
This isn't too bad, but what if that else
case was more complicated and also had these if-else structures inside of
it? The indentation involved could get quite ugly. Using the option
computation expression, we can eliminate the
indentation for the else case, and replace that map while we're at it:
let getShippingAddress db order = option {
if not order.UseBillingAddress then
return order.ShippingAddress
let! pmt = db.GetPaymentInfo order.Id
return pmt.BillingAddress
}
Here we have two top-level expressions: the if
expression and the code that follows.
Option Builder's Combine will evaluate the first expression and if it is Some
, it returns that value and does not
evaluate the following expressions. If it is None, it evaluates the next expression and repeats.
The way computation expressions work is that an if
with no else
evaluates to the Zero value when the if
condition
is not met. Option Builder's Zero value is None, so it's like an implicit else return! None
.
This allows us to write the almost C#-style code you see here that looks like an early return. The catch is that it only
short-circuits for Some
values, so writing if condition then return! None
would have no effect at all.
The result
workflow is appropriate when you need to compute a Result
value from a chain of computations that return
a Result
, such as validation.
let!
(Bind) calls Result.bind.return
wraps the value inOk
.- Zero returns
Ok ()
. - Combine restricts the first expression to
Result<unit, _>
. If the first expression evaluates to anError
value, that is returned as the value of the computation expression, but if it isOk ()
then the next expression is evaluated. - Expression evaluation is delayed. If a bind expression returns an
Error
or if a non-last expression returnsError
then the following code is not executed.
As an example, let's validate a date range from text user inputs. This could be written with pattern matching:
let validateDates beginText endText =
match Parse.Date beginText with // Parse.Date is an Acadian.FSharp function
| None -> Error "Begin date is not in a valid format"
| Some beginDate ->
match Parse.Date endText with
| None -> Error "End date is not in a valid format"
| Some endDate ->
if endDate < beginDate then
Error "End date cannot be before start date"
else
Ok (beginDate, endDate)
We can refactor this to use bind with the help of this library's Result.ofOption
:
let validateDates beginText endText =
Parse.Date beginText |> Result.ofOption "Begin date is not in a valid format"
|> Result.bind (fun beginDate ->
Parse.Date endText |> Result.ofOption "End date is not in a valid format"
|> Result.bind (fun endDate ->
if endDate < beginDate then
Error "End date cannot be before start date"
else
Ok (beginDate, endDate)
)
)
From here we can easily refactor to eliminate the nesting with the result
computation expression:
let validateDates beginText endText = result {
let! beginDate = Parse.Date beginText |> Result.ofOption "Begin date is not in a valid format"
let! endDate = Parse.Date endText |> Result.ofOption "End date is not in a valid format"
if endDate < beginDate then
return! Error "End date cannot be before start date"
else
return (beginDate, endDate)
}
Taking the previous example, we can tweak it to remove the nesting on the return by simply taking it out of the else
:
let validateDates beginText endText = result {
let! beginDate = Parse.Date beginText |> Result.ofOption "Begin date is not in a valid format"
let! endDate = Parse.Date endText |> Result.ofOption "End date is not in a valid format"
if endDate < beginDate then
return! Error "End date cannot be before start date"
return (beginDate, endDate)
}
This works similarly to the option
example, except the short-circuiting value is Error
instead of Some
.
This is more advantageous if you have multiple short-circuiting checks with calculations in-between where it could remove multiple levels of indentation.
The asyncOption
and asyncResult
workflows are appropriate when you need to compute option/result values
asynchronously. They work like their non-async counterparts, but also allow you to await Async values with let!
and
do!
.
Note that Bind (let!
/do!
) is overloaded to unwrap either Async or Option/Result, but not both. For example, if you
have an Async<Option<'a>>
value in ao
, you need to bind twice to get a Some value:
asyncOption {
let! o = ao // ao is awaited and the resulting option is assigned to o
let! x = o // if o is Some, bind x to its 'a value
...
}