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
--------------------
type string = String
module Set from Microsoft.FSharp.Collections
--------------------
type Set<'T (requires comparison)> = interface IReadOnlyCollection<'T> interface IStructuralEquatable interface IComparable interface IEnumerable interface IEnumerable<'T> interface ICollection<'T> new: elements: 'T seq -> Set<'T> member Add: value: 'T -> Set<'T> member Contains: value: 'T -> bool override Equals: objnull -> bool ...
--------------------
new: elements: 'T seq -> Set<'T>
module Map from Microsoft.FSharp.Collections
--------------------
type Map<'Key,'Value (requires comparison)> = interface IReadOnlyDictionary<'Key,'Value> interface IReadOnlyCollection<KeyValuePair<'Key,'Value>> interface IEnumerable interface IStructuralEquatable interface IComparable interface IEnumerable<KeyValuePair<'Key,'Value>> interface ICollection<KeyValuePair<'Key,'Value>> interface IDictionary<'Key,'Value> new: elements: ('Key * 'Value) seq -> Map<'Key,'Value> member Add: key: 'Key * value: 'Value -> Map<'Key,'Value> ...
--------------------
new: elements: ('Key * 'Value) seq -> Map<'Key,'Value>
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)
--------------------
type int = int32
--------------------
type int<'Measure> = int
type Async = static member AsBeginEnd: computation: ('Arg -> Async<'T>) -> ('Arg * AsyncCallback * objnull -> 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: Async<'T option> seq -> Async<'T option> static member FromBeginEnd: beginAction: (AsyncCallback * objnull -> IAsyncResult) * endAction: (IAsyncResult -> 'T) * ?cancelAction: (unit -> unit) -> Async<'T> + 3 overloads static member FromContinuations: callback: (('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T> ...
--------------------
type Async<'T>
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'> This parameter is deprecated. Please use InferenceMode instead. 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> <param name='PreferDictionaries'>If true, json records are interpreted as dictionaries when the names of all the fields are inferred (by type inference rules) into the same non-string primitive type.</param> <param name='InferenceMode'>Possible values: | NoInference -> Inference is disabled. All values are inferred as the most basic type permitted for the value (i.e. string or number or bool). | ValuesOnly -> Types of values are inferred from the Sample. Inline schema support is disabled. This is the default. | ValuesAndInlineSchemasHints -> Types of values are inferred from both values and inline schemas. Inline schemas are special string values that can define a type and/or unit of measure. Supported syntax: typeof<type> or typeof{type} or typeof<type<measure>> or typeof{type{measure}}. Valid measures are the default SI units, and valid types are <c>int</c>, <c>int64</c>, <c>bool</c>, <c>float</c>, <c>decimal</c>, <c>date</c>, <c>datetimeoffset</c>, <c>timespan</c>, <c>guid</c> and <c>string</c>. | ValuesAndInlineSchemasOverrides -> Same as ValuesAndInlineSchemasHints, but value inferred types are ignored when an inline schema is present. </param>
Gets latitude/longitude for a returned zip info
val decimal: value: 'T -> decimal (requires member op_Explicit)
--------------------
type decimal = Decimal
--------------------
type decimal<'Measure> = decimal
val float: value: 'T -> float (requires member op_Explicit)
--------------------
type float = Double
--------------------
type float<'Measure> = float
val double: value: 'T -> double (requires member op_Explicit)
--------------------
type double = Double
--------------------
type double<'Measure> = float<'Measure>
<summary>Provides constants and static methods for trigonometric, logarithmic, and other common mathematical functions.</summary>
Loads JSON from the specified uri