// thinkbeforecoding

Applicatives IRL

2020-10-03T16:33:28 / jeremie chassaing

This is the content of the presentation of the same name I did at Open FSharp (San Francisco) and NewCrafts (Paris)

Monads are all the rage in FP land... but Applicatives are also very useful and quite easy to get. We'll go through 3 real use cases to discover what they're up to.

Examples will be in F#. This is of course applicable in other languages with currying.

Options

We start with a simple structure: Option<'t>. As a reminder, its definition is

1: 
2: 
3: 
 type Option<'t> =
     | Some of 't
     | None

It's a value that either contains a value of type 't (the Some case) or doesn't contain a value (None)

map

We can easily write a map function for this structure.

map takes a 'a -> 'b function and applies it to the inner value of the option if any. If the option is None, it simply returns None.

1: 
2: 
3: 
4: 
 let map (f: 'a -> 'b) (x: Option<'a>) : Option<'b> =
     match x with
     | Some value -> Some (f value)
     | None -> None

when looking at map as a function taking a single parameter its signature is

1: 
('a -> 'b) -> (Option<'a> -> Option<'b>)

map takes a ('a -> 'b) function and change it in a (Option<'a> -> Option<'b>) function.

map2

map takes a single argument function f and make it Option aware. But what about a function taking two arguments ?

We can define a map2 function with the following signature:

1: 
('a -> 'b -> 'c) -> (Option<'a> -> Option<'b> -> Option<'c>)

It takes the function and calls it with the value of the two other Option parameter if they both have a value. It returns None if any is None.

1: 
2: 
3: 
4: 
 let map2 (f: 'a -> 'b -> 'c) (x: Option<'a>) (y: Option<'b>) : Option<'c> =
     match x,y with
     | Some xvalue, Some yvalue -> Some (f xvalue yvalue)
     | _ -> None

map3, map4...

For a 3 argument function, a 4 argument function we'd then have to write extra map functions. That would be doable but tedious. And where should we stop ?

Let's take a simple two argument function and se what happens when we use map on it:

1: 
2: 
3: 
 let add x y = x + y

 let mappedAdd = map add

Notice first that it compiles because add can be seen as a one argument function returning another 1 argument function:

1: 
2: 
let add x =
    fun y -> x + y

So its signature can be read as (int -> (int -> int))

Using map, it will become:

1: 
Option<int> -> Option<int -> int>

If you call it with the value Some 3, the result will be Some function that take an int and add 3 to it. Called with None, you get no function.

Lets take the most basic function application:

1: 
 let basicApply f x = f x

basicApply is a function that take a function f, a value v, and pass the value v to the function f (this is the F# <| operator).

we can make it work with option using map2:

1: 
 let apply f x = map2 basicApply f x

basicApply signature is

1: 
('a -> 'b) -> 'a -> 'b

and we can look at it as a 2 argument function:

1: 
(('a -> 'b) -> 'a) -> 'b

And map2 will change it to:

1: 
(Option<'a -> 'b> -> Option<'a>) -> Option<'b>

This is a function that take an optional function as first argument and an optional value as a second argument. If both have a value, the value is passed to the function. If any is None the result is None.

1: 
2: 
 let partialAdd = map add (Some 3) // Some(fun y -> 3 + Y)
 apply partialAdd (Some 5) // Some(3 + 5) => Some 8
Some 8

The nice thing is that it works for function with more than 2 arguments since any function can be considered as a function of 1 arguement.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
 type User =
     { FirstName: string
       LastName: string
       Age: int
       FavoriteLanguage: string }
 let user firstname lastname age favoriteLang =
     { FirstName = firstname
       LastName = lastname
       Age = age
       FavoriteLanguage = favoriteLang }

 let user' = map user (Some "John") // Option<string -> int -> string -> User>
 let user'' = apply user' (Some "Doe") // Option<int -> string -> User>
 let user''' = apply user'' (Some 42) // Option<string -> User>
 apply user''' (Some "F#") // Option<User>
Some { FirstName = "John"
       LastName = "Doe"
       Age = 42
       FavoriteLanguage = "F#" }

Hopefully we can simplify the writing by defining two infix operators for map and apply.

1: 
2: 
 let (<!>) = map
 let (<*>) = apply

Now we can write

1: 
2: 
3: 
4: 
5: 
 user
 <!> Some "John"
 <*> Some "Doe"
 <*> Some 42
 <*> Some "F#"

this is very usefull with validation, and you can easily do the same thing for Result<'a,string> type, a type that has either a value of type 'a, or an error message. map is already defined and we have to write a map2:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 
29: 
30: 
31: 
32: 
33: 
34: 
35: 
36: 
37: 
38: 
39: 
40: 
41: 
42: 
43: 
44: 
45: 
46: 
 let map2 (f: 'a -> 'b -> 'c) 
          (xresult: Result<'a, string>)
          (yresult: Result<'b, string>) 
          : Result<'c, string> =
    match xresult, yresult with
    | Ok x, Ok y -> Ok (f x y)
    | Error fe, Ok _ -> Error fe
    | Ok _, Error xe -> Error xe
    | Error fe, Error xe -> Error (fe + "\n" + xe)

 let apply f x = map2 basicApply f x

 let (<!>) = Result.map
 let (<*>) = apply

 open System

 let notEmpty prop value =
     if String.IsNullOrEmpty value then
         Error (sprintf "%s should not be empty" prop)
     else
         Ok value
 
 let tryParse prop input =
    match Int32.TryParse(input: string) with
    | true, value -> Ok value
    | false, _ -> Error (sprintf "%s should be an number" prop)

  // define a user as previously
 type User =
     { FirstName: string
       LastName: string
       Age: int
       FavoriteLanguage: string }
 let user firstname lastname age favoriteLang =
     { FirstName = firstname
       LastName = lastname
       Age = age
       FavoriteLanguage = favoriteLang }


 user
 <!> notEmpty "firstname" "John"
 <*> notEmpty "lastname" "Doe"
 <*> tryParse "age" "42"
 <*> notEmpty "favorite language" "F#"

Now try to play with the input values and look at the result. Instead of getting Ok with a typed user, you'll get an error with a message describing why the user could ne be constructed. Neat!

Series

Applicatives can be defined on anything on which we can write a map2 functions. Any construct that is zippable.

A good example of zippable, non trivial stucture is time series. Let's define one:

1: 
 type Series<'a> = Series of 'a * (DateTime * 'a) list

It has a initial value of type 'a and a list of changes consisting of the date of change and the new value.

For instance we can easily build constant series like this:

1: 
 let always x = Series(x, [])

it defines a Series with initial value x that never change.

We can also define a map:

1: 
2: 
 let map f (Series(initial, changes)) =
    Series( f initial, List.map (fun (d,v) -> d, f v) changes)

It applies the function f to the initial value and every change values.

map2 is a bit more convoluted. The two series start with an initial value that can be fed to the given function. After that, one of the series change, and we call f with the initial value of the one that didn't change an the new value of the other one.. each time a value change, we use this value and the last known value of the other one. We use a recursive function for this.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 
 let map2 f (Series(ix, cx)) (Series(iy, cy)) =
    let rec zip lastx lasty changesx changesy =
        [ match changesx, changesy with
          | [],[] -> () // we're done
          | [], _ ->
             // x won't change anymore
             for d,y in changesy do 
                d, f lastx y
          | _, [] -> 
             // y won't change anymore
             for d,x in changesx do 
                d, f x lasty
          | (dx,x) :: tailx , (dy,y) :: taily ->
            if dx < dy then
                // x changes before y
                dx, f x lasty
                yield! zip x lasty tailx changesy
            elif dy < dx then
                // y changes before x
                dy, f lastx y
                yield! zip lastx y changesx taily
            else
                // x and y change at the same time
                // dx is equal to dy
                dx, f x y
                yield! zip x y tailx taily
        ]
    Series(f ix iy, zip ix iy cx cy)

using map2 we can add two series:

1: 
2: 
3: 
4: 
5: 
6: 
 let add x y = x + y
 map2 add
        (Series(0, [DateTime(2020,10,03), 1
                    DateTime(2020,10,05), 5 ]))
        (Series(2, [DateTime(2020,10,03), 2 
                    DateTime(2020,10,04), 3]))
Series
  (2,
   [(03/10/2020 00:00:00, 3); (04/10/2020 00:00:00, 4); (05/10/2020 00:00:00, 8)])

Here is a graphical representation:

1: 
2: 
3: 
4: 
         Initial   2020-10-3    4   5
x:          0              1    -   5
y:          2              2    3   -
result:     2              3    4   8

We define apply and the two operators as we did for option:

1: 
2: 
3: 
 let apply f x = map2 basicApply f x
 let (<!>) = map
 let (<*>) = apply

And we can use it with a user

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
 type User =
     { FirstName: string
       LastName: string
       Age: int
       FavoriteLanguage: string }
 let user firstname lastname age favoriteLang =
     { FirstName = firstname
       LastName = lastname
       Age = age
       FavoriteLanguage = favoriteLang } 

 let birthDate = DateTime(1978,10,01) 
 user
    <!> always "John"
    <*> Series("Dow", [ DateTime(2010,08,17), "Snow"]) // married
    <*> Series(0, [for y in 1 .. 42 -> birthDate.AddYears(y), y ])
    <*> Series("C#", [DateTime(2005,05,01), "F#"])

The result is a Series of User with the new values on each change.

We can now take any function that work on non-Series values, and apply it to Series of values. This is especially useful whith hotels data. Hotels define prices, availability, closures and all other properties for their rooms for each night in a calendar. Each property can be modeled as a Series.

Let's compute the potential sells for a room:

1: 
2: 
3: 
4: 
5: 
 let potentialSells availability price closed =
     if closed then
         0m
     else
         decimal availability * price

This function has no notion of time and Series.

Now we want to apply it for every nights:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
 let availability = 
    Series(0, [ DateTime(2020,10,03), 5
                DateTime(2020,10,07), 3
                DateTime(2020,10,09), 0])
 let price =
    Series(100m, [ DateTime(2020,10,04), 110m
                   DateTime(2020,10,06), 100m ])
 let closed =
    Series(false, [DateTime(2020,10,5), true
                   DateTime(2020,10,6), false])
 potentialSells
 <!> availability
 <*> price
 <*> closed
Series
  (0M,
   [(03/10/2020 00:00:00, 500M); (04/10/2020 00:00:00, 550M);
    (05/10/2020 00:00:00, 0M); (06/10/2020 00:00:00, 500M);
    (07/10/2020 00:00:00, 300M); (09/10/2020 00:00:00, 0M)])

Just imagine the headache if we did not have this applicative..

Queries

The first two examples were structures that "contain" data. For this third case, we'll consider a different problem.

We have a document store - like elastic search - where we can query documents and request specific properties for the returned document. For instance in the query for a user we ask for the "firstname", "lastname" and "age" properties and we get these properties in the result.

For the demo we'll use a simple function:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
 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 true one would take a document id and actually call the document database.

Despite being very useful (we get only the properties we're interested in), this kind of interface often leads to irritating bugs. When accessing the result, we must be sure that all the properties we use have been correctly requested. And when we stop using a property, we should not forget to remove it from the query.

That would be awsome if we could just use the properties and the query would magically know which one to fetch.

Let's introduce the Query type:

1: 
2: 
3: 
 type Query<'t> =
    { Properties: string Set
      Get: Map<string,string> -> 't }

This type is exactly here to do what we want. It contains a list of properties to query and a function that use the result to extract a value from it. We have to create simple ways to build it safely.

The first thing is to be able to query a single property

1: 
2: 
3: 
 let prop name : Query<string> =
      { Properties = set [ name ]
        Get = fun response -> response.[name] }

This function takes a property name and builds a Query that:

  • has the expected property name in Properties
  • can extract this property from the result

We can define stock properties like using the prop function:

1: 
2: 
3: 
 let firstname = prop "firstname"
 let lastname = prop "lastname"
 let favoriteLanguage = prop "favoritelanguage"

We can fetch them with the following function:

1: 
2: 
3: 
 let callService (query: Query<'t>) : 't =
    let response = queryService query.Properties
    query.Get response

For age, we'd like to convert it to an int. This is easily done with a map function that will change the result using a given function

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
 let map (f: 'a -> 'b) (q: Query<'a>) : Query<'b> =
     { Properties = q.Properties
       Get = fun response ->
               let v = q.Get response
               f v }

 let (<!>) = map

 let age = int <!> prop "age"

Building a full name could be done like that (it can be more complicated):

1: 
2: 
 let fullname firstname lastname =
     firstname + " " + lastname

We need to retrieve both first name and last name and pass it to the function. But we can also make a map2:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
 let map2 (f: 'a -> 'b -> 'c) (x: Query<'a>) (y: Query<'b>) : Query<'c> =
    { Properties = x.Properties + y.Properties
      Get = fun response ->
               let xvalue = x.Get response
               let yvalue = y.Get response
               f xvalue yvalue 
    }

The result is a Query that:

  • has the union of the properties of both queries
  • get values from both and pass them to the given function

With a map2, we can define apply and the opertor:

1: 
2: 
 let apply f x = map2 basicApply f x
 let (<*>) = apply

With this we can query a user:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
 type User =
     { FirstName: string
       LastName: string
       Age: int
       FavoriteLanguage: string }
 let user firstname lastname age favoriteLang =
     { FirstName = firstname
       LastName = lastname
       Age = age
       FavoriteLanguage = favoriteLang } 
 
 let userQuery =
     user
     <!> firstname
     <*> lastname
     <*> age
     <*> favoriteLanguage

 callService userQuery

using userQuery, we just composed basic properties to form a query for a larger structure, so we know that we cannot use a property without requesting it in the query.

Other applicatives

Applicatives can be found it a lot of places. To zip lists, execute async computations in parallel.

It can also be used to create formlets. A Formlet<'t> is a UX form to fill a 't structure. The simples formlet are input fields. A text input is a Formlet<string>, a checkbox a Formlet<bool>. A label function is a (string -> Formlet<'a> -> Formlet<'a>) function that adds a label to an existing formlet. A map function can change Formlet<string> to Formlet<Address> given a (string -> Addess) function. And we use map2 and apply to take several formlets and compose them in a Formlet<User> for instance.

Once you start to see it, you'll spot them everywhere.

Be careful to not abuse it. For a single use, it's often better to compute the result directly.

But when you have many places that are impacted, especially if the code change often, it can reduce code complexity by a fair amount.

Don't hesitate to ping me on twitter if you find nice uses of applicatives!

As suggested by Chet Husk, I'll soon post about the new Applicatives support in Computation Expressions.

val basicApply : f:('a -> 'b) -> x:'a -> 'b
val f : ('a -> 'b)
val x : 'a
Multiple items
module Option

from Microsoft.FSharp.Core

--------------------
type Option<'t> =
  | Some of 't
  | None
union case Option.Some: 't -> Option<'t>
union case Option.None: Option<'t>
val map : f:('a -> 'b) -> x:Option<'a> -> Option<'b>
val x : Option<'a>
val value : 'a
module Option

from Microsoft.FSharp.Core
val map2 : f:('a -> 'b -> 'c) -> x:Option<'a> -> y:Option<'b> -> Option<'c>
val f : ('a -> 'b -> 'c)
val y : Option<'b>
val xvalue : 'a
val yvalue : 'b
val add : x:int -> y:int -> int
val x : int
val y : int
val mappedAdd : (Option<int> -> Option<(int -> int)>)
Multiple items
val int : value:'T -> int (requires member op_Explicit)

--------------------
type int = int32

--------------------
type int<'Measure> = int
val apply : f:Option<('a -> 'b)> -> x:Option<'a> -> Option<'b>
val f : Option<('a -> 'b)>
val partialAdd : Option<(int -> int)>
type User =
  { FirstName: string
    LastName: string
    Age: int
    FavoriteLanguage: string }
User.FirstName: string
Multiple items
val string : value:'T -> string

--------------------
type string = System.String
User.LastName: string
User.Age: int
User.FavoriteLanguage: string
val user : firstname:string -> lastname:string -> age:int -> favoriteLang:string -> User
val firstname : string
val lastname : string
val age : int
val favoriteLang : string
val user' : Option<(string -> int -> string -> User)>
val user'' : Option<(int -> string -> User)>
val user''' : Option<(string -> User)>
val map2 : f:('a -> 'b -> 'c) -> xresult:Result<'a,string> -> yresult:Result<'b,string> -> Result<'c,string>
val xresult : Result<'a,string>
Multiple items
module Result

from Microsoft.FSharp.Core

--------------------
[<Struct>]
type Result<'T,'TError> =
  | Ok of ResultValue: 'T
  | Error of ErrorValue: 'TError
val yresult : Result<'b,string>
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
val y : 'b
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
val fe : string
val xe : string
val apply : f:Result<('a -> 'b),string> -> x:Result<'a,string> -> Result<'b,string>
val f : Result<('a -> 'b),string>
val x : Result<'a,string>
val map : mapping:('T -> 'U) -> result:Result<'T,'TError> -> Result<'U,'TError>
namespace System
val notEmpty : prop:string -> value:string -> Result<string,string>
val prop : string
val value : string
Multiple items
type String =
  new : value:char[] -> string + 8 overloads
  member Chars : int -> char
  member Clone : unit -> obj
  member CompareTo : value:obj -> int + 1 overload
  member Contains : value:string -> bool + 3 overloads
  member CopyTo : sourceIndex:int * destination:char[] * destinationIndex:int * count:int -> unit
  member EndsWith : value:string -> bool + 3 overloads
  member EnumerateRunes : unit -> StringRuneEnumerator
  member Equals : obj:obj -> bool + 2 overloads
  member GetEnumerator : unit -> CharEnumerator
  ...

--------------------
String(value: char []) : String
String(value: nativeptr<char>) : String
String(value: nativeptr<sbyte>) : String
String(value: ReadOnlySpan<char>) : String
String(c: char, count: int) : String
String(value: char [], startIndex: int, length: int) : String
String(value: nativeptr<char>, startIndex: int, length: int) : String
String(value: nativeptr<sbyte>, startIndex: int, length: int) : String
String(value: nativeptr<sbyte>, startIndex: int, length: int, enc: Text.Encoding) : String
String.IsNullOrEmpty(value: string) : bool
val sprintf : format:Printf.StringFormat<'T> -> 'T
val tryParse : prop:string -> input:string -> Result<int,string>
val input : string
type Int32 =
  struct
    member CompareTo : value:obj -> int + 1 overload
    member Equals : obj:obj -> bool + 1 overload
    member GetHashCode : unit -> int
    member GetTypeCode : unit -> TypeCode
    member ToString : unit -> string + 3 overloads
    member TryFormat : destination:Span<char> * charsWritten:int * ?format:ReadOnlySpan<char> * ?provider:IFormatProvider -> bool
    static val MaxValue : int
    static val MinValue : int
    static member Parse : s:string -> int + 4 overloads
    static member TryParse : s:string * result:int -> bool + 3 overloads
  end
Int32.TryParse(s: ReadOnlySpan<char>, result: byref<int>) : bool
Int32.TryParse(s: string, result: byref<int>) : bool
Int32.TryParse(s: ReadOnlySpan<char>, style: Globalization.NumberStyles, provider: IFormatProvider, result: byref<int>) : bool
Int32.TryParse(s: string, style: Globalization.NumberStyles, provider: IFormatProvider, result: byref<int>) : bool
Multiple items
val string : value:'T -> string

--------------------
type string = String
val value : int
Multiple items
union case Series.Series: 'a * (DateTime * 'a) list -> Series<'a>

--------------------
type Series<'a> = | Series of 'a * (DateTime * 'a) list
Multiple items
type DateTime =
  struct
    new : ticks:int64 -> DateTime + 10 overloads
    member Add : value:TimeSpan -> DateTime
    member AddDays : value:float -> DateTime
    member AddHours : value:float -> DateTime
    member AddMilliseconds : value:float -> DateTime
    member AddMinutes : value:float -> DateTime
    member AddMonths : months:int -> DateTime
    member AddSeconds : value:float -> DateTime
    member AddTicks : value:int64 -> DateTime
    member AddYears : value:int -> DateTime
    ...
  end

--------------------
DateTime ()
   (+0 other overloads)
DateTime(ticks: int64) : DateTime
   (+0 other overloads)
DateTime(ticks: int64, kind: DateTimeKind) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, calendar: Globalization.Calendar) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, kind: DateTimeKind) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, calendar: Globalization.Calendar) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, kind: DateTimeKind) : DateTime
   (+0 other overloads)
type 'T list = List<'T>
val always : x:'a -> Series<'a>
val map : f:('a -> 'b) -> Series<'a> -> Series<'b>
val initial : 'a
val changes : (DateTime * 'a) list
Multiple items
module List

from Microsoft.FSharp.Collections

--------------------
type List<'T> =
  | ( [] )
  | ( :: ) of Head: 'T * Tail: 'T list
    interface IReadOnlyList<'T>
    interface IReadOnlyCollection<'T>
    interface IEnumerable
    interface IEnumerable<'T>
    member GetReverseIndex : rank:int * offset:int -> int
    member GetSlice : startIndex:int option * endIndex:int option -> 'T list
    member Head : 'T
    member IsEmpty : bool
    member Item : index:int -> 'T with get
    member Length : int
    ...
val map : mapping:('T -> 'U) -> list:'T list -> 'U list
val d : DateTime
val v : 'a
val map2 : f:('a -> 'b -> 'c) -> Series<'a> -> Series<'b> -> Series<'c>
val ix : 'a
val cx : (DateTime * 'a) list
val iy : 'b
val cy : (DateTime * 'b) list
val zip : ('a -> 'b -> ('d * 'a) list -> ('d * 'b) list -> ('d * 'c) list) (requires comparison)
val lastx : 'a
val lasty : 'b
val changesx : ('d * 'a) list (requires comparison)
val changesy : ('d * 'b) list (requires comparison)
val d : 'd (requires comparison)
val dx : 'd (requires comparison)
val tailx : ('d * 'a) list (requires comparison)
val dy : 'd (requires comparison)
val taily : ('d * 'b) list (requires comparison)
val apply : f:Series<('a -> 'b)> -> x:Series<'a> -> Series<'b>
val f : Series<('a -> 'b)>
val x : Series<'a>
val birthDate : DateTime
DateTime.AddYears(value: int) : DateTime
val potentialSells : availability:int -> price:decimal -> closed:bool -> decimal
val availability : int
val price : decimal
val closed : bool
Multiple items
val decimal : value:'T -> decimal (requires member op_Explicit)

--------------------
type decimal = Decimal

--------------------
type decimal<'Measure> = decimal
val availability : Series<int>
val price : Series<decimal>
val closed : Series<bool>
module Queries

from 2020-10-03-applicatives-irl
val queryService : properties:Set<string> -> Map<string,string>
val properties : Set<string>
Multiple items
module Set

from Microsoft.FSharp.Collections

--------------------
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
  ...

--------------------
new : elements:seq<'T> -> Set<'T>
Multiple items
module Map

from Microsoft.FSharp.Collections

--------------------
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 ContainsKey : key:'Key -> bool
  ...

--------------------
new : elements:seq<'Key * 'Value> -> Map<'Key,'Value>
val ofList : elements:('Key * 'T) list -> Map<'Key,'T> (requires comparison)
val contains : element:'T -> set:Set<'T> -> bool (requires comparison)
type Query<'t> =
  { Properties: Set<string>
    Get: Map<string,string> -> 't }
Query.Properties: Set<string>
Query.Get: Map<string,string> -> 't
val prop : name:string -> Query<string>
val name : string
val set : elements:seq<'T> -> Set<'T> (requires comparison)
val response : Map<string,string>
val firstname : Query<string>
val lastname : Query<string>
val favoriteLanguage : Query<string>
val callService : query:Query<'t> -> 't
val query : Query<'t>
val map : f:('a -> 'b) -> q:Query<'a> -> Query<'b>
val q : Query<'a>
Query.Get: Map<string,string> -> 'a
val age : Query<int>
val fullname : firstname:string -> lastname:string -> string
val map2 : f:('a -> 'b -> 'c) -> x:Query<'a> -> y:Query<'b> -> Query<'c>
val x : Query<'a>
val y : Query<'b>
Query.Get: Map<string,string> -> 'b
val apply : f:Query<('a -> 'b)> -> x:Query<'a> -> Query<'b>
val f : Query<('a -> 'b)>
val userQuery : Query<User>