Applicative Computation Expressions
In the last post we saw how to implement
applicatives using map
, map2
and apply
to define <!>
and <*>
operators.
This time, we will use Computation Expressions to achieve the same result. This is not yet part of F# 5.0, you need the --langversion:preview flag to compile the following code.
Let's start again with our Query<'t>
type.
As a reminder, we created it to access a service that is called with a list of properties to return for a given document.
This is a mock version of such a service. In the real world, you'll call Elastic Search indicating the document id and the properties you need:
let queryService (properties: string Set) : Map<string,string> =
Map.ofList [
if Set.contains "firstname" properties then
"firstname", "John"
if Set.contains "lastname" properties then
"lastname", "Doe"
if Set.contains "age" properties then
"age", "42"
if Set.contains "favoritelanguage" properties then
"favoritelanguage", "F#"
]
The problem with this kind of service is that there is usually no way to be sure that all properties used in the result have been correctly requested. This type contains both a list of properties to query from an external service as well as the code using fetched properties to build the result.
type Query<'t> =
{ Properties: string Set
Get: Map<string,string> -> 't }
It can be used to call the service:
let callService (query: Query<'t>) : 't =
queryService query.Properties
|> query.Get
From here we defined a function to create a query from a single column:
module Query =
let prop name =
{ Properties = Set.singleton name
Get = fun m ->
m.[name] }
And the map
function that applies a given function to the result.
let map f q =
{ Properties = q.Properties
Get = fun m ->
let value = q.Get m
f value }
We also defined a map2
to combine two queries as a single one. This query
will request the unions of the argument queries properties, and call the
first query to get its result, the second to get the other result, and pass
both to the given function to combine them:
let map2 f x y =
{ Properties = x.Properties + y.Properties
Get = fun m ->
let vx = x.Get m
let vy = y.Get m
f vx vy }
With map2
we can define a zip
function that takes two Query
arguments and
combine their results as a pair. We will use this function in our builder.
let zip x y =
map2 (fun vx vy -> vx,vy) x y
Computation Expressions are created using types that implement specific
members corresponding to the different operations. For applicatives, we
need to implement BindReturn
with the following signature:
M<'a> * ('a -> 'b) -> M<'b>
Where M
in our case is Query
. You should spot that it's the same signature
as map
(with the function as the second argument).
The second one is MergeSources
and is used to zip parameter together:
M<'a> * M<'b> -> M<'a * 'b>
Here we will use the zip
function we defined before.
Here is the builder definition:
type QueryBuilder() =
member _.BindReturn(x : Query<'a>,f: 'a -> 'b) : Query<'b> =
Query.map f x
member _.MergeSources(x : Query<'a>,y: Query<'b>) : Query<'a * 'b> =
Query.zip x y
let query = QueryBuilder()
For our sample, use define a User and the basic properties defined by the service:
type User =
{ FullName: string
Age: int
FavoriteLanguage: string}
module Props =
let firstname = Query.prop "firstname"
let lastname = Query.prop "lastname"
let age = Query.prop "age" |> Query.map int
let favoriteLanguage = Query.prop "favoritelanguage"
Is is now possible to use the query computation expression to compute new derived properties. Here we define fullname that queries firstname and lastname and appends them together. When using this derived property, it will request both firstname and lastname properties from the service.
module DerivedProps =
let fullname =
query {
let! firstname = Props.firstname
and! lastname = Props.lastname
return firstname + " " + lastname
}
you can notice that we use let!
and and!
here.
The meaning of let!
is: Give this name (here firstname) to the value inside the structure on the right (the query).
Since we have a Query<string>
on the right, firstname will be a string
.
The and!
means: and at the same time, give this name to this value inside this other structure on the right.
This is at the same time extracting both values with zip. The actual code looks like this:
query.BindReturn(
query.MergeSources(Props.firstname, Props.lastname),
fun (firstname, lastname) -> firstname + " " + lastname)
We can then compose queries further by reusing derived properties inside new queries:
let user =
query {
let! fullname = DerivedProps.fullname
and! age = Props.age
and! favoriteLanguage = Props.favoriteLanguage
return
{ FullName = fullname
Age = age
FavoriteLanguage = favoriteLanguage }
}
callService user
Let's use it for async.
We define a BindReturn
and a MergeSources
member.
Using a type extension, it is not advised to use async {}
blocks in the implementation because it can go recursive...
I still put the equivalent construct as a comment:
type AsyncBuilder with
member _.BindReturn(x: 'a Async,f: 'a -> 'b) : 'b Async =
// this is the same as:
// async { return f v }
async.Bind(x, fun v -> async.Return (f v))
member _.MergeSources(x: 'a Async, y: 'b Async) : ('a * 'b) Async =
// this is the same as:
// async {
// let! xa = Async.StartChild x
// let! ya = Async.StartChild y
// let! xv = xa // wait x value
// let! yv = ya // wait y value
// return xv, yv // pair values
// }
async.Bind(Async.StartChild x,
fun xa ->
async.Bind(Async.StartChild y,
fun ya ->
async.Bind(xa, fun xv ->
async.Bind(ya, fun yv ->
async.Return (xv,yv)
)
)
)
)
The zippopotam.us service returns informations about zip codes. We will use the JsonProvider to load the data asynchronously and parse the result.
open FSharp.Data
type ZipCode = FSharp.Data.JsonProvider<"http://api.zippopotam.us/GB/EC1">
/// Gets latitude/longitude for a returned zip info
let coord (zip: ZipCode.Root) =
zip.Places.[0].Latitude, zip.Places.[0].Longitude
We use The pythagorean theorem to compute the distance given the latitude and longitude of two points:
let dist (lata: decimal,longa: decimal) (latb: decimal, longb: decimal) =
let x = float (longb - longa) * cos (double (latb + lata) / 2. * Math.PI / 360.)
let y = float (latb - lata)
let z = sqrt (x*x + y*y)
z * 1.852 * 60. |> decimal
Now using let!
and!
we fetch and compute the coordinates of Paris and London
in parallel and then use both results to get the distance:
async {
let! parisCoords =
async {
let! paris = ZipCode.AsyncLoad "http://api.zippopotam.us/fr/75020"
return coord paris }
and! londonCoords =
async {
let! london = ZipCode.AsyncLoad "http://api.zippopotam.us/GB/EC1"
return coord london }
return dist parisCoords londonCoords
}
|> Async.RunSynchronously
It's obviously possible to use both Computation Expressions and the approach with operators from the last post for more fun!
val string: value: 'T -> string
<summary>Converts the argument to a string using <c>ToString</c>.</summary>
<remarks>For standard integer and floating point values the and any type that implements <c>IFormattable</c><c>ToString</c> conversion uses <c>CultureInfo.InvariantCulture</c>. </remarks>
<param name="value">The input value.</param>
<returns>The converted string.</returns>
--------------------
type string = String
<summary>An abbreviation for the CLI type <see cref="T:System.String" />.</summary>
<category>Basic Types</category>
module Set from Microsoft.FSharp.Collections
<summary>Contains operations for working with values of type <see cref="T:Microsoft.FSharp.Collections.Set`1" />.</summary>
--------------------
type Set<'T (requires comparison)> = interface IReadOnlyCollection<'T> interface IComparable interface IEnumerable interface IEnumerable<'T> interface ICollection<'T> new: elements: seq<'T> -> Set<'T> member Add: value: 'T -> Set<'T> member Contains: value: 'T -> bool override Equals: obj -> bool member IsProperSubsetOf: otherSet: Set<'T> -> bool ...
<summary>Immutable sets based on binary trees, where elements are ordered by F# generic comparison. By default comparison is the F# structural comparison function or uses implementations of the IComparable interface on element values.</summary>
<remarks>See the <see cref="T:Microsoft.FSharp.Collections.SetModule" /> module for further operations on sets. All members of this class are thread-safe and may be used concurrently from multiple threads.</remarks>
--------------------
new: elements: seq<'T> -> Set<'T>
module Map from Microsoft.FSharp.Collections
<summary>Contains operations for working with values of type <see cref="T:Microsoft.FSharp.Collections.Map`2" />.</summary>
--------------------
type Map<'Key,'Value (requires comparison)> = interface IReadOnlyDictionary<'Key,'Value> interface IReadOnlyCollection<KeyValuePair<'Key,'Value>> interface IEnumerable interface IComparable interface IEnumerable<KeyValuePair<'Key,'Value>> interface ICollection<KeyValuePair<'Key,'Value>> interface IDictionary<'Key,'Value> new: elements: seq<'Key * 'Value> -> Map<'Key,'Value> member Add: key: 'Key * value: 'Value -> Map<'Key,'Value> member Change: key: 'Key * f: ('Value option -> 'Value option) -> Map<'Key,'Value> ...
<summary>Immutable maps based on binary trees, where keys are ordered by F# generic comparison. By default comparison is the F# structural comparison function or uses implementations of the IComparable interface on key values.</summary>
<remarks>See the <see cref="T:Microsoft.FSharp.Collections.MapModule" /> module for further operations on maps. All members of this class are thread-safe and may be used concurrently from multiple threads.</remarks>
--------------------
new: elements: seq<'Key * 'Value> -> Map<'Key,'Value>
<summary>Returns a new map made from the given bindings.</summary>
<param name="elements">The input list of key/value pairs.</param>
<returns>The resulting map.</returns>
<summary>Evaluates to "true" if the given element is in the given set.</summary>
<param name="element">The element to test.</param>
<param name="set">The input set.</param>
<returns>True if <c>element</c> is in <c>set</c>.</returns>
<summary>The set containing the given element.</summary>
<param name="value">The value for the set to contain.</param>
<returns>The set containing <c>value</c>.</returns>
type QueryBuilder = new: unit -> QueryBuilder member BindReturn: x: Query<'a> * f: ('a -> 'b) -> Query<'b> member MergeSources: x: Query<'a> * y: Query<'b> -> Query<'a * 'b>
--------------------
new: unit -> QueryBuilder
module Query from 2020-10-07-applicative-computation-expressions
--------------------
type Query<'t> = { Properties: Set<string> Get: Map<string,string> -> 't }
val int: value: 'T -> int (requires member op_Explicit)
<summary>Converts the argument to signed 32-bit integer. This is a direct conversion for all primitive numeric types. For strings, the input is converted using <c>Int32.Parse()</c> with InvariantCulture settings. Otherwise the operation requires an appropriate static conversion method on the input type.</summary>
<param name="value">The input value.</param>
<returns>The converted int</returns>
--------------------
[<Struct>] type int = int32
<summary>An abbreviation for the CLI type <see cref="T:System.Int32" />.</summary>
<category>Basic Types</category>
--------------------
type int<'Measure> = int
<summary>The type of 32-bit signed integer numbers, annotated with a unit of measure. The unit of measure is erased in compiled code and when values of this type are analyzed using reflection. The type is representationally equivalent to <see cref="T:System.Int32" />.</summary>
<category>Basic Types with Units of Measure</category>
<summary>The type of the <c>async</c> operator, used to build workflows for asynchronous computations.</summary>
<category index="1">Async Programming</category>
type Async = static member AsBeginEnd: computation: ('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit) static member AwaitEvent: event: IEvent<'Del,'T> * ?cancelAction: (unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate) static member AwaitIAsyncResult: iar: IAsyncResult * ?millisecondsTimeout: int -> Async<bool> static member AwaitTask: task: Task<'T> -> Async<'T> + 1 overload static member AwaitWaitHandle: waitHandle: WaitHandle * ?millisecondsTimeout: int -> Async<bool> static member CancelDefaultToken: unit -> unit static member Catch: computation: Async<'T> -> Async<Choice<'T,exn>> static member Choice: computations: seq<Async<'T option>> -> Async<'T option> static member FromBeginEnd: beginAction: (AsyncCallback * obj -> IAsyncResult) * endAction: (IAsyncResult -> 'T) * ?cancelAction: (unit -> unit) -> Async<'T> + 3 overloads static member FromContinuations: callback: (('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T> ...
<summary>Holds static members for creating and manipulating asynchronous computations.</summary>
<remarks> See also <a href="https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/asynchronous-workflows">F# Language Guide - Async Workflows</a>. </remarks>
<category index="1">Async Programming</category>
--------------------
type Async<'T>
<summary> An asynchronous computation, which, when run, will eventually produce a value of type T, or else raises an exception. </summary>
<remarks> This type has no members. Asynchronous computations are normally specified either by using an async expression or the static methods in the <see cref="T:Microsoft.FSharp.Control.Async" /> type. See also <a href="https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/asynchronous-workflows">F# Language Guide - Async Workflows</a>. </remarks>
<namespacedoc><summary> Library functionality for asynchronous programming, events and agents. See also <a href="https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/asynchronous-workflows">Asynchronous Programming</a>, <a href="https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/members/events">Events</a> and <a href="https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/lazy-expressions">Lazy Expressions</a> in the F# Language Guide. </summary></namespacedoc>
<category index="1">Async Programming</category>
<summary>Builds an asynchronous workflow using computation expression syntax.</summary>
namespace FSharp
--------------------
namespace Microsoft.FSharp
namespace FSharp.Data
--------------------
namespace Microsoft.FSharp.Data
<summary>Typed representation of a JSON document.</summary> <param name='Sample'>Location of a JSON sample file or a string containing a sample JSON document.</param> <param name='SampleIsList'>If true, sample should be a list of individual samples for the inference.</param> <param name='RootName'>The name to be used to the root type. Defaults to `Root`.</param> <param name='Culture'>The culture used for parsing numbers and dates. Defaults to the invariant culture.</param> <param name='Encoding'>The encoding used to read the sample. You can specify either the character set name or the codepage number. Defaults to UTF8 for files, and to ISO-8859-1 the for HTTP requests, unless `charset` is specified in the `Content-Type` response header.</param> <param name='ResolutionFolder'>A directory that is used when resolving relative file references (at design time and in hosted execution).</param> <param name='EmbeddedResource'>When specified, the type provider first attempts to load the sample from the specified resource (e.g. 'MyCompany.MyAssembly, resource_name.json'). This is useful when exposing types generated by the type provider.</param> <param name='InferTypesFromValues'>If true, turns on additional type inference from values. (e.g. type inference infers string values such as "123" as ints and values constrained to 0 and 1 as booleans.)</param>
Gets latitude/longitude for a returned zip info
val decimal: value: 'T -> decimal (requires member op_Explicit)
<summary>Converts the argument to System.Decimal using a direct conversion for all primitive numeric types. For strings, the input is converted using <c>UInt64.Parse()</c> with InvariantCulture settings. Otherwise the operation requires an appropriate static conversion method on the input type.</summary>
<param name="value">The input value.</param>
<returns>The converted decimal.</returns>
--------------------
[<Struct>] type decimal = Decimal
<summary>An abbreviation for the CLI type <see cref="T:System.Decimal" />.</summary>
<category>Basic Types</category>
--------------------
type decimal<'Measure> = decimal
<summary>The type of decimal numbers, annotated with a unit of measure. The unit of measure is erased in compiled code and when values of this type are analyzed using reflection. The type is representationally equivalent to <see cref="T:System.Decimal" />.</summary>
<category>Basic Types with Units of Measure</category>
val float: value: 'T -> float (requires member op_Explicit)
<summary>Converts the argument to 64-bit float. This is a direct conversion for all primitive numeric types. For strings, the input is converted using <c>Double.Parse()</c> with InvariantCulture settings. Otherwise the operation requires an appropriate static conversion method on the input type.</summary>
<param name="value">The input value.</param>
<returns>The converted float</returns>
--------------------
[<Struct>] type float = Double
<summary>An abbreviation for the CLI type <see cref="T:System.Double" />.</summary>
<category>Basic Types</category>
--------------------
type float<'Measure> = float
<summary>The type of double-precision floating point numbers, annotated with a unit of measure. The unit of measure is erased in compiled code and when values of this type are analyzed using reflection. The type is representationally equivalent to <see cref="T:System.Double" />.</summary>
<category index="6">Basic Types with Units of Measure</category>
<summary>Cosine of the given number</summary>
<param name="value">The input value.</param>
<returns>The cosine of the input.</returns>
val double: value: 'T -> double (requires member op_Explicit)
<summary>Converts the argument to 64-bit float.</summary>
<remarks>This is a direct conversion for all primitive numeric types. For strings, the input is converted using <c>Double.Parse()</c> with InvariantCulture settings. Otherwise the operation requires and invokes a <c>ToDouble</c> method on the input type.</remarks>
--------------------
[<Struct>] type double = Double
<summary>An abbreviation for the CLI type <see cref="T:System.Double" />. Identical to <see cref="T:Microsoft.FSharp.Core.float" />.</summary>
<category>Basic Types</category>
--------------------
type double<'Measure> = float<'Measure>
<summary>The type of double-precision floating point numbers, annotated with a unit of measure. The unit of measure is erased in compiled code and when values of this type are analyzed using reflection. The type is representationally equivalent to <see cref="T:System.Double" />.</summary>
<category index="6">Basic Types with Units of Measure</category>
<summary>Provides constants and static methods for trigonometric, logarithmic, and other common mathematical functions.</summary>
<summary>Square root of the given number</summary>
<param name="value">The input value.</param>
<returns>The square root of the input.</returns>
Loads JSON from the specified uri