// thinkbeforecoding

Applicative Computation Expressions - 2

2020-10-08T08:18:10 / jeremie chassaing

In the last post we used BindReturnand MergeSources to implement the applicative. The advantage of implementing them is that it will work for any number of and!.

When using let! with a single and!, it makes one call to MergeSources to pair arguments and one call to BindReturn to pass values to the function.

When using let! with two and!, it makes two calls to MergeSources to tuple arguments and one call to BindReturn to pass values to the function.

This can be expensive, so you can provide specific implementations for given numbers of parameters to reduce the number of calls.

For two arguments, you can implement Bind2Return:

type AsyncBuilder with
    member this.Bind2Return(x: 'a Async,y: 'b Async,f: 'a * 'b -> 'c) : 'c Async =
        this.Bind(Async.StartChild x, fun xa ->
            this.Bind(Async.StartChild y, fun ya ->
                this.Bind(xa, fun xv ->
                    this.Bind(ya, fun yv -> this.Return (f(xv, yv)))
                    )
                )
            
            )

its signature is:

M<'a> * M<'b> * ('a * 'b -> 'c) -> M<'c>

Notice that it is similar to map2 signature.

We can check that it's working as expected:

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

To handle a let! and! and! case you have to provide a Bind3Return member that takes 3 values and a function. You can also provide a Bind4Return, Bind5Return... and from my tests there is no limit in the number of arguments you can test. F# would happily call a Bind265Return if it had to.

Those additional functions are occasions to optimize the implementation by reducing the number of intermediate allocations and function calls.

Obviously, it's advised to provide only a few special cases and fall back to BindReturn and MergeSources for the rest.

Other options for implementation is to provide MergeSources3, MergeSources4 to reduce the number of intermediate tuples. The signature will be for MergeSources3:

M<'a> * M<'b> * M<'c> -> M<'a * 'b * 'c>

It's also possible to define Bind2, Bind3 with the following signatures:

M<'a> * M<'b> * ('a * 'b -> M<'c>) -> M<'c>

This looks like the Bind2Return except that the function already returns a wrapped value.

You can find the complete documentation in the RFC

namespace System
Multiple items
namespace FSharp

--------------------
namespace Microsoft.FSharp
Multiple items
namespace FSharp.Data

--------------------
namespace Microsoft.FSharp.Data
type ZipCode = JsonProvider<...>
type JsonProvider
<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>
val dist: lata: decimal * longa: decimal -> latb: decimal * longb: decimal -> decimal
val lata: decimal
Multiple items
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 longa: decimal
val latb: decimal
val longb: decimal
val x: float
Multiple items
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>
val cos: value: 'T -> 'T (requires member Cos)
<summary>Cosine of the given number</summary>
<param name="value">The input value.</param>
<returns>The cosine of the input.</returns>
Multiple items
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>
type Math = static member Abs: value: decimal -> decimal + 7 overloads static member Acos: d: float -> float static member Acosh: d: float -> float static member Asin: d: float -> float static member Asinh: d: float -> float static member Atan: d: float -> float static member Atan2: y: float * x: float -> float static member Atanh: d: float -> float static member BigMul: a: int * b: int -> int64 + 2 overloads static member BitDecrement: x: float -> float ...
<summary>Provides constants and static methods for trigonometric, logarithmic, and other common mathematical functions.</summary>
field Math.PI: float = 3.14159265359
val y: float
val z: float
val sqrt: value: 'T -> 'U (requires member Sqrt)
<summary>Square root of the given number</summary>
<param name="value">The input value.</param>
<returns>The square root of the input.</returns>
val coord: zip: JsonProvider<...>.Root -> decimal * decimal
 Gets latitude/longitude for a returned zip info
val zip: JsonProvider<...>.Root
type Root = inherit IJsonDocument new: postCode: string * country: string * countryAbbreviation: string * places: Placis[] -> Root + 1 overload member Country: string member CountryAbbreviation: string member Places: Placis[] member PostCode: string
property JsonProvider<...>.Root.Places: JsonProvider<...>.Placis[] with get
type AsyncBuilder = member Bind: computation: Async<'T> * binder: ('T -> Async<'U>) -> Async<'U> member Combine: computation1: Async<unit> * computation2: Async<'T> -> Async<'T> member Delay: generator: (unit -> Async<'T>) -> Async<'T> member For: sequence: seq<'T> * body: ('T -> Async<unit>) -> Async<unit> member Return: value: 'T -> Async<'T> member ReturnFrom: computation: Async<'T> -> Async<'T> member TryFinally: computation: Async<'T> * compensation: (unit -> unit) -> Async<'T> member TryWith: computation: Async<'T> * catchHandler: (exn -> Async<'T>) -> Async<'T> member Using: resource: 'T * binder: ('T -> Async<'U>) -> Async<'U> (requires 'T :> IDisposable) member While: guard: (unit -> bool) * computation: Async<unit> -> Async<unit> ...
<summary>The type of the <c>async</c> operator, used to build workflows for asynchronous computations.</summary>
<category index="1">Async Programming</category>
val this: AsyncBuilder
val x: Async<'a>
Multiple items
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>
val y: Async<'b>
val f: ('a * 'b -> 'c)
member AsyncBuilder.Bind: computation: Async<'T> * binder: ('T -> Async<'U>) -> Async<'U>
static member Async.StartChild: computation: Async<'T> * ?millisecondsTimeout: int -> Async<Async<'T>>
val xa: Async<'a>
val ya: Async<'b>
val xv: 'a
val yv: 'b
member AsyncBuilder.Return: value: 'T -> Async<'T>
val async: AsyncBuilder
<summary>Builds an asynchronous workflow using computation expression syntax.</summary>
val parisCoords: decimal * decimal
val paris: JsonProvider<...>.Root
JsonProvider<...>.AsyncLoad(uri: string) : Async<JsonProvider<...>.Root>
Loads JSON from the specified uri
val london: JsonProvider<...>.Root
static member Async.RunSynchronously: computation: Async<'T> * ?timeout: int * ?cancellationToken: Threading.CancellationToken -> 'T