DAG - An Immutable Spreadsheet Data Structure

In finance, data grids can be defined as a set of input fields and function fields that take other field values as parameters. Spreadsheets are often used to do this, but they have several limitations.

Recently, I've been working on ways of describing calculations, so they can just as easily be viewed in a desktop application, web report or spreadsheet.

One of the components required to do this is a functional calculation graph much like how a spreadsheet works. This blog aims to construct a functional directed acyclic graph (DAG) calculation data structure.

DAG code

We don't have to work very hard to ensure the graph is not circular or keep the cells in topological order. The API can be designed such that it is only possible to add function cells when the parameter cells already exist in the DAG. All tasks can be performed with a single pass of the cells in the order they were added.

The DAG data structure is made immutable by cloning any internal arrays when they need to be changed. Grids can keep the old version of the calculations or compare and switch to the new version when needed.

The DAG is fully type safe by use of an applicative functor builder in constructing function cells.

Cells are evaluated as lazy Task so calculations run as parallel as possible. Calculations are run only once, and results are reused even after further updates to the DAG.

The code can be downloaded here.

  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: 
 47: 
 48: 
 49: 
 50: 
 51: 
 52: 
 53: 
 54: 
 55: 
 56: 
 57: 
 58: 
 59: 
 60: 
 61: 
 62: 
 63: 
 64: 
 65: 
 66: 
 67: 
 68: 
 69: 
 70: 
 71: 
 72: 
 73: 
 74: 
 75: 
 76: 
 77: 
 78: 
 79: 
 80: 
 81: 
 82: 
 83: 
 84: 
 85: 
 86: 
 87: 
 88: 
 89: 
 90: 
 91: 
 92: 
 93: 
 94: 
 95: 
 96: 
 97: 
 98: 
 99: 
100: 
101: 
102: 
103: 
104: 
105: 
106: 
107: 
108: 
109: 
110: 
111: 
112: 
113: 
114: 
115: 
116: 
117: 
118: 
119: 
type Dag = private {
    InputValues : obj array
    FunctionInputs : (Set<int> * Set<int>) array
    FunctionFunctions : (Dag -> obj) array //obj is Task<'a>
    FunctionValues : Lazy<obj> array //obj is Task<'a>
}

module Dag =
    let private append a v =
        let mutable a = a
        Array.Resize(&a, Array.length a + 1)
        a.[Array.length a - 1] <- v
        a

    type Input = private | CellInput
    type Function = private | CellFunction

    type Cell<'a,'b> = private | Cell of int

    let empty = {
        InputValues = [||]
        FunctionInputs = [||]
        FunctionFunctions = [||]
        FunctionValues = [||]
    }

    let addInput (v:'a) (d:Dag) : Dag * Cell<'a, Input> =
        { d with
            InputValues = box v |> append d.InputValues
        }, Cell d.InputValues.Length

    let getValue (Cell i:Cell<'a,Input>) (d:Dag) : 'a =
        downcast d.InputValues.[i]

    let setInput (Cell i:Cell<'a,Input>) (a:'a) (d:Dag) : Dag =
        if downcast d.InputValues.[i] = a then d
        else
            let dirtyCalcs =
                Seq.fold (fun (j,s) (inputs,calcInputs) ->
                    if Set.contains i inputs ||
                       Set.intersect s calcInputs |> Set.isEmpty |> not then
                        j+1, Set.add j s
                    else
                        j+1, s
                ) (0,Set.empty) d.FunctionInputs
                |> snd

            let inputValues = Array.copy d.InputValues
            inputValues.[i] <- box a

            if Set.isEmpty dirtyCalcs then { d with InputValues = inputValues }
            else
                let functionValues = Array.copy d.FunctionValues
                let dag = {
                    d with
                        InputValues = inputValues
                        FunctionValues = functionValues
                }
                Set.iter (fun i ->
                    functionValues.[i] <- lazy d.FunctionFunctions.[i] dag
                ) dirtyCalcs
                dag

    let getValueTask (Cell i:Cell<'a,Function>) (d:Dag) : Task<'a> =
        downcast d.FunctionValues.[i].Value

    let changed (Cell i:Cell<'a,'t>) (before:Dag) (after:Dag) : bool =
        if typeof<'t> = typeof<Function> then
            before.FunctionValues.[i] <> after.FunctionValues.[i]
        else
            downcast before.InputValues.[i] <> downcast after.InputValues.[i]

    type 'a Builder = private {
        Dag : Dag
        Inputs : Set<int> * Set<int>
        Function : Dag -> Task<'a>
    }

    let buildFunction (d:Dag) f = {
        Dag = d
        Inputs = Set.empty, Set.empty
        Function = fun _ -> Task.FromResult f
    }

    let applyCell (Cell i:Cell<'a,'t>) {Dag=dag;Inputs=inI,inC;Function=bFn} =

        let inline taskMap f (t:Task<_>) =
            t.ContinueWith(fun (r:Task<_>) -> f r.Result)

        let isFunctionCell = typeof<'t> = typeof<Function>
        {
            Dag = dag
            Inputs =
                if isFunctionCell then inI, Set.add i inC
                                  else Set.add i inI, inC
            Function =
                if isFunctionCell then
                    fun d ->
                        let fTask = bFn d
                        ( downcast d.FunctionValues.[i].Value
                          |> taskMap (fun a -> taskMap (fun f -> f a) fTask)
                        ).Unwrap()
                else
                    fun d ->
                        bFn d |> taskMap (fun f ->
                            downcast d.InputValues.[i] |> f  )
        }

    let addFunction ({Dag=dag;Inputs=ips;Function=fn}:'a Builder) =
        let calc = fn >> box
        let d = {
            dag with
                FunctionInputs = append dag.FunctionInputs ips
                FunctionFunctions = append dag.FunctionFunctions calc
                FunctionValues = append dag.FunctionValues null
        }
        d.FunctionValues.[d.FunctionValues.Length-1] <- lazy calc d
        let cell : Cell<'a,Function> = Cell dag.FunctionValues.Length
        d, cell

Testing

The following tests demonstrate use of the DAG.

  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: 
 47: 
 48: 
 49: 
 50: 
 51: 
 52: 
 53: 
 54: 
 55: 
 56: 
 57: 
 58: 
 59: 
 60: 
 61: 
 62: 
 63: 
 64: 
 65: 
 66: 
 67: 
 68: 
 69: 
 70: 
 71: 
 72: 
 73: 
 74: 
 75: 
 76: 
 77: 
 78: 
 79: 
 80: 
 81: 
 82: 
 83: 
 84: 
 85: 
 86: 
 87: 
 88: 
 89: 
 90: 
 91: 
 92: 
 93: 
 94: 
 95: 
 96: 
 97: 
 98: 
 99: 
100: 
101: 
102: 
103: 
104: 
105: 
106: 
107: 
108: 
109: 
110: 
111: 
112: 
113: 
114: 
115: 
116: 
117: 
118: 
119: 
120: 
121: 
122: 
123: 
124: 
125: 
126: 
127: 
128: 
129: 
130: 
131: 
132: 
133: 
134: 
135: 
136: 
137: 
138: 
139: 
140: 
141: 
142: 
143: 
144: 
145: 
146: 
147: 
148: 
149: 
150: 
151: 
152: 
153: 
154: 
155: 
156: 
157: 
158: 
159: 
160: 
161: 
162: 
163: 
164: 
165: 
166: 
167: 
let tests =
    testList "dag tests" [

        testAsync "one cell" {
            let dag, cell1 = Dag.addInput 7 Dag.empty
            Expect.equal 7 (Dag.getValue cell1 dag) "one cell"
        }

        testAsync "two cell" {
            let dag, cell1 = Dag.addInput 8 Dag.empty
            let dag, cell2 = Dag.addInput 9 dag
            Expect.equal 8 (Dag.getValue cell1 dag) "first 8"
            Expect.equal 9 (Dag.getValue cell2 dag) "second 9"
        }

        testAsync "one function" {
            let dag, cell1 = Dag.addInput 42 Dag.empty
            let dag, cell2 =
                Dag.buildFunction dag (fun x -> x * 10)
                |> Dag.applyCell cell1
                |> Dag.addFunction
            let! result = Dag.getValueTask cell2 dag |> Async.AwaitTask
            Expect.equal 420 result "42 * 10 = 420"
        }

        testAsync "one function with set" {
            let dag, cell1 = Dag.addInput 13 Dag.empty
            let dag, cell2 =
                Dag.buildFunction dag (fun x -> x * 10)
                |> Dag.applyCell cell1
                |> Dag.addFunction
            let dag = Dag.setInput cell1 43 dag
            let! result = Dag.getValueTask cell2 dag |> Async.AwaitTask
            Expect.equal 430 result "43 * 10 = 430"
        }

        testAsync "one function with set twice" {
            let dag, cell1 = Dag.addInput 15 Dag.empty
            let dag, cell2 =
                Dag.buildFunction dag (fun x -> x * 10)
                |> Dag.applyCell cell1
                |> Dag.addFunction
            let dag = Dag.setInput cell1 43 dag
            let dag = Dag.setInput cell1 44 dag
            let! result = Dag.getValueTask cell2 dag |> Async.AwaitTask
            Expect.equal 440 result "44 * 10 = 440"
        }

        testAsync "not changed input" {
            let dagBefore, cell1 = Dag.addInput 42 Dag.empty
            let dagAfter,_ = Dag.addInput 45 dagBefore
            Expect.isFalse (Dag.changed cell1 dagBefore dagAfter) "no change"
        }

        testAsync "changed input" {
            let dagBefore, cell1 = Dag.addInput 42 Dag.empty
            let dagAfter = Dag.setInput cell1 45 dagBefore
            Expect.isTrue (Dag.changed cell1 dagBefore dagAfter) "changed"
        }

        testAsync "not changed function" {
            let dag, cell1 = Dag.addInput 42 Dag.empty
            let dagBefore, cell2 =
                Dag.buildFunction dag (fun x -> x * 10)
                |> Dag.applyCell cell1
                |> Dag.addFunction
            let dagAfter,_ = Dag.addInput 45 dagBefore
            Expect.isFalse (Dag.changed cell2 dagBefore dagAfter) "no change"
        }

        testAsync "changed function" {
            let dag, cell1 = Dag.addInput 17 Dag.empty
            let dagBefore, cell2 =
                Dag.buildFunction dag (fun x -> x * 10)
                |> Dag.applyCell cell1
                |> Dag.addFunction
            let dagAfter = Dag.setInput cell1 45 dagBefore
            Expect.isTrue (Dag.changed cell2 dagBefore dagAfter) "changed"
            let! result = Dag.getValueTask cell2 dagAfter |> Async.AwaitTask
            Expect.equal 450 result "45 * 10 = 450"
            let! result = Dag.getValueTask cell2 dagBefore |> Async.AwaitTask
            Expect.equal 170 result "17 * 10 = 170"
        }

        testAsync "chained functions" {
            let dag, cell1 = Dag.addInput 18 Dag.empty
            let dag, cell2 =
                Dag.buildFunction dag (fun x -> x * 10)
                |> Dag.applyCell cell1
                |> Dag.addFunction
            let dagBefore, cell3 =
                Dag.buildFunction dag (fun x -> x + 1)
                |> Dag.applyCell cell2
                |> Dag.addFunction
            let dagAfter = Dag.setInput cell1 23 dagBefore
            let! result = Dag.getValueTask cell3 dagAfter |> Async.AwaitTask
            Expect.equal 231 result "231"
            let! result = Dag.getValueTask cell3 dagBefore |> Async.AwaitTask
            Expect.equal 181 result "181"
        }

        testAsync "two function" {
            let dag, cell1 = Dag.addInput "z" Dag.empty
            let dag, cell2 = Dag.addInput 7 dag
            let dag, cell3 =
                Dag.buildFunction dag (fun s (n:int) -> s + string n)
                |> Dag.applyCell cell1
                |> Dag.applyCell cell2
                |> Dag.addFunction
            let! result = Dag.getValueTask cell3 dag |> Async.AwaitTask
            Expect.equal "z7" result "z7"
        }

        testAsync "three function with set" {
            let dag, cell1 = Dag.addInput "f" Dag.empty
            let dag, cell2 = Dag.addInput 8 dag
            let dag, cell3 = Dag.addInput 1.5 dag
            let dag, cell4 =
                Dag.buildFunction dag
                    (fun s (n:int) (f:float) -> s + string n + "-" + string f)
                |> Dag.applyCell cell1
                |> Dag.applyCell cell2
                |> Dag.applyCell cell3
                |> Dag.addFunction
            let! result = Dag.getValueTask cell4 dag |> Async.AwaitTask
            Expect.equal "f8-1.5" result "f8-1.5"
            let dag = Dag.setInput cell1 "w" dag
            let! result = Dag.getValueTask cell4 dag |> Async.AwaitTask
            Expect.equal "w8-1.5" result "w8-1.5"
        }
        
        testAsync "chained functions multi" {
            let dag, cell1 = Dag.addInput "a" Dag.empty
            let dag, cell2 = Dag.addInput 1 dag
            let dag, cell3 =
                Dag.buildFunction dag (fun s (n:int) -> "x:" + s + string n)
                |> Dag.applyCell cell1
                |> Dag.applyCell cell2
                |> Dag.addFunction
            let dag, cell4 = Dag.addInput "b" dag
            let dag, cell5 = Dag.addInput 2 dag
            let dag, cell6 =
                Dag.buildFunction dag (fun s (n:int) -> "y:" + s + string n)
                |> Dag.applyCell cell4
                |> Dag.applyCell cell5
                |> Dag.addFunction
            let dag, cell7 = Dag.addInput "c" dag
            let dag, cell8 = Dag.addInput 3 dag
            let dag, cell9 =
                Dag.buildFunction dag (fun s (n:int) -> "z:" + s + string n)
                |> Dag.applyCell cell7
                |> Dag.applyCell cell8
                |> Dag.addFunction
            let dagBefore, cell10 =
                Dag.buildFunction dag
                    (fun s1 s2 s3 -> String.concat "|" [s1;s2;s3])
                |> Dag.applyCell cell3
                |> Dag.applyCell cell6
                |> Dag.applyCell cell9
                |> Dag.addFunction
            let dagAfter = Dag.setInput cell5 4 dagBefore
            let! result = Dag.getValueTask cell10 dagAfter |> Async.AwaitTask
            Expect.equal "x:a1|y:b4|z:c3" result "x:a1|y:b4|z:c3"
            let! result = Dag.getValueTask cell10 dagBefore |> Async.AwaitTask
            Expect.equal "x:a1|y:b2|z:c3" result "x:a1|y:b2|z:c3"
        }
    ]

Conclusion

This has been a very successful experiment. The DAG has some nice features while keeping type safety.

The way immutability has been implemented means it is probably not best suited to fast realtime updates or very fine-grained calculations. For more coarse-grained calculations like grids of dependent fields, where each cell represents a column of values and summaries, it could be ideal.

namespace System
namespace System.Threading
namespace System.Threading.Tasks
type Dag =
  private {InputValues: obj array;
           FunctionInputs: (Set<int> * Set<int>) array;
           FunctionFunctions: (Dag -> obj) array;
           FunctionValues: Lazy<obj> array;}

Full name: DagBlog.Dag
Dag.InputValues: obj array
type obj = Object

Full name: Microsoft.FSharp.Core.obj
type 'T array = 'T []

Full name: Microsoft.FSharp.Core.array<_>
Dag.FunctionInputs: (Set<int> * Set<int>) array
Multiple items
module Set

from Microsoft.FSharp.Collections

--------------------
type Set<'T (requires comparison)> =
  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
  member IsProperSupersetOf : otherSet:Set<'T> -> bool
  ...

Full name: Microsoft.FSharp.Collections.Set<_>

--------------------
new : elements:seq<'T> -> Set<'T>
Multiple items
val int : value:'T -> int (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.int

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

Full name: Microsoft.FSharp.Core.int

--------------------
type int<'Measure> = int

Full name: Microsoft.FSharp.Core.int<_>
Dag.FunctionFunctions: (Dag -> obj) array
Dag.FunctionValues: Lazy<obj> array
Multiple items
active recognizer Lazy: Lazy<'T> -> 'T

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.( |Lazy| )

--------------------
type Lazy<'T> =
  new : unit -> Lazy<'T> + 5 overloads
  member IsValueCreated : bool
  member ToString : unit -> string
  member Value : 'T

Full name: System.Lazy<_>

--------------------
Lazy() : unit
Lazy(valueFactory: Func<'T>) : unit
Lazy(isThreadSafe: bool) : unit
Lazy(mode: Threading.LazyThreadSafetyMode) : unit
Lazy(valueFactory: Func<'T>, isThreadSafe: bool) : unit
Lazy(valueFactory: Func<'T>, mode: Threading.LazyThreadSafetyMode) : unit
type Array =
  member Clone : unit -> obj
  member CopyTo : array:Array * index:int -> unit + 1 overload
  member GetEnumerator : unit -> IEnumerator
  member GetLength : dimension:int -> int
  member GetLongLength : dimension:int -> int64
  member GetLowerBound : dimension:int -> int
  member GetUpperBound : dimension:int -> int
  member GetValue : [<ParamArray>] indices:int[] -> obj + 7 overloads
  member Initialize : unit -> unit
  member IsFixedSize : bool
  ...

Full name: System.Array
Array.Resize<'T>(array: byref<'T []>, newSize: int) : unit
val length : array:'T [] -> int

Full name: Microsoft.FSharp.Collections.Array.length
val box : value:'T -> obj

Full name: Microsoft.FSharp.Core.Operators.box
module Seq

from Microsoft.FSharp.Collections
val fold : folder:('State -> 'T -> 'State) -> state:'State -> source:seq<'T> -> 'State

Full name: Microsoft.FSharp.Collections.Seq.fold
val contains : element:'T -> set:Set<'T> -> bool (requires comparison)

Full name: Microsoft.FSharp.Collections.Set.contains
val intersect : set1:Set<'T> -> set2:Set<'T> -> Set<'T> (requires comparison)

Full name: Microsoft.FSharp.Collections.Set.intersect
val isEmpty : set:Set<'T> -> bool (requires comparison)

Full name: Microsoft.FSharp.Collections.Set.isEmpty
val not : value:bool -> bool

Full name: Microsoft.FSharp.Core.Operators.not
val add : value:'T -> set:Set<'T> -> Set<'T> (requires comparison)

Full name: Microsoft.FSharp.Collections.Set.add
val empty<'T (requires comparison)> : Set<'T> (requires comparison)

Full name: Microsoft.FSharp.Collections.Set.empty
val snd : tuple:('T1 * 'T2) -> 'T2

Full name: Microsoft.FSharp.Core.Operators.snd
val copy : array:'T [] -> 'T []

Full name: Microsoft.FSharp.Collections.Array.copy
val iter : action:('T -> unit) -> set:Set<'T> -> unit (requires comparison)

Full name: Microsoft.FSharp.Collections.Set.iter
Multiple items
type Task =
  new : action:Action -> Task + 7 overloads
  member AsyncState : obj
  member ContinueWith : continuationAction:Action<Task> -> Task + 9 overloads
  member CreationOptions : TaskCreationOptions
  member Dispose : unit -> unit
  member Exception : AggregateException
  member Id : int
  member IsCanceled : bool
  member IsCompleted : bool
  member IsFaulted : bool
  ...

Full name: System.Threading.Tasks.Task

--------------------
type Task<'TResult> =
  inherit Task
  new : function:Func<'TResult> -> Task<'TResult> + 7 overloads
  member ContinueWith : continuationAction:Action<Task<'TResult>> -> Task + 9 overloads
  member Result : 'TResult with get, set
  static member Factory : TaskFactory<'TResult>

Full name: System.Threading.Tasks.Task<_>

--------------------
Task(action: Action) : unit
Task(action: Action, cancellationToken: Threading.CancellationToken) : unit
Task(action: Action, creationOptions: TaskCreationOptions) : unit
Task(action: Action<obj>, state: obj) : unit
Task(action: Action, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : unit
Task(action: Action<obj>, state: obj, cancellationToken: Threading.CancellationToken) : unit
Task(action: Action<obj>, state: obj, creationOptions: TaskCreationOptions) : unit
Task(action: Action<obj>, state: obj, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : unit

--------------------
Task(function: Func<'TResult>) : unit
Task(function: Func<'TResult>, cancellationToken: Threading.CancellationToken) : unit
Task(function: Func<'TResult>, creationOptions: TaskCreationOptions) : unit
Task(function: Func<obj,'TResult>, state: obj) : unit
Task(function: Func<'TResult>, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : unit
Task(function: Func<obj,'TResult>, state: obj, cancellationToken: Threading.CancellationToken) : unit
Task(function: Func<obj,'TResult>, state: obj, creationOptions: TaskCreationOptions) : unit
Task(function: Func<obj,'TResult>, state: obj, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : unit
type bool = Boolean

Full name: Microsoft.FSharp.Core.bool
val typeof<'T> : Type

Full name: Microsoft.FSharp.Core.Operators.typeof
val x : int

Full name: DagBlog.Gen.x
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 -> Async<unit>
static member AwaitTask : task:Task<'T> -> Async<'T>
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 FromBeginEnd : beginAction:(AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg:'Arg1 * beginAction:('Arg1 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * beginAction:('Arg1 * 'Arg2 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * arg3:'Arg3 * beginAction:('Arg1 * 'Arg2 * 'Arg3 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromContinuations : callback:(('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T>
static member Ignore : computation:Async<'T> -> Async<unit>
static member OnCancel : interruption:(unit -> unit) -> Async<IDisposable>
static member Parallel : computations:seq<Async<'T>> -> Async<'T []>
static member RunSynchronously : computation:Async<'T> * ?timeout:int * ?cancellationToken:CancellationToken -> 'T
static member Sleep : millisecondsDueTime:int -> Async<unit>
static member Start : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions * ?cancellationToken:CancellationToken -> Task<'T>
static member StartChild : computation:Async<'T> * ?millisecondsTimeout:int -> Async<Async<'T>>
static member StartChildAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions -> Async<Task<'T>>
static member StartImmediate : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartWithContinuations : computation:Async<'T> * continuation:('T -> unit) * exceptionContinuation:(exn -> unit) * cancellationContinuation:(OperationCanceledException -> unit) * ?cancellationToken:CancellationToken -> unit
static member SwitchToContext : syncContext:SynchronizationContext -> Async<unit>
static member SwitchToNewThread : unit -> Async<unit>
static member SwitchToThreadPool : unit -> Async<unit>
static member TryCancelled : computation:Async<'T> * compensation:(OperationCanceledException -> unit) -> Async<'T>
static member CancellationToken : Async<CancellationToken>
static member DefaultCancellationToken : CancellationToken

Full name: Microsoft.FSharp.Control.Async

--------------------
type Async<'T>

Full name: Microsoft.FSharp.Control.Async<_>
static member Async.AwaitTask : task:Task -> Async<unit>
static member Async.AwaitTask : task:Task<'T> -> Async<'T>
Multiple items
val string : value:'T -> string

Full name: Microsoft.FSharp.Core.Operators.string

--------------------
type string = String

Full name: Microsoft.FSharp.Core.string
Multiple items
val float : value:'T -> float (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.float

--------------------
type float = Double

Full name: Microsoft.FSharp.Core.float

--------------------
type float<'Measure> = float

Full name: Microsoft.FSharp.Core.float<_>
Multiple items
type String =
  new : value:char -> string + 7 overloads
  member Chars : int -> char
  member Clone : unit -> obj
  member CompareTo : value:obj -> int + 1 overload
  member Contains : value:string -> bool
  member CopyTo : sourceIndex:int * destination:char[] * destinationIndex:int * count:int -> unit
  member EndsWith : value:string -> bool + 2 overloads
  member Equals : obj:obj -> bool + 2 overloads
  member GetEnumerator : unit -> CharEnumerator
  member GetHashCode : unit -> int
  ...

Full name: System.String

--------------------
String(value: nativeptr<char>) : unit
String(value: nativeptr<sbyte>) : unit
String(value: char []) : unit
String(c: char, count: int) : unit
String(value: nativeptr<char>, startIndex: int, length: int) : unit
String(value: nativeptr<sbyte>, startIndex: int, length: int) : unit
String(value: char [], startIndex: int, length: int) : unit
String(value: nativeptr<sbyte>, startIndex: int, length: int, enc: Text.Encoding) : unit
val concat : sep:string -> strings:seq<string> -> string

Full name: Microsoft.FSharp.Core.String.concat