Resumable monad, idempotence and cloud services
October 18th, 2015, by William Blum
Abstract
We define a new monad and its associated F# syntactic sugar resumable { ... }
to express computations that can be interrupted at specified control points
and resumed in subsequent executions while carrying along state from
the previous execution.
Resumable expresssions make it simpler to write idempotent and resumable code which is often necessary when writing cloud services code.
Motivating example
Suppose that we are building a service that creates virtual machines. The first step is to obtain the name of the machine to be created. The second step is to defer the actual virtual machine provisioning to an actual cloud provider like Amazon or Azure through an asynchronous API call. The external provider returns a request ID used to check status of the request. The final step is to poll the request ID until the request succeeds or fails (e.g., the virtual machine is provisioned or an error occurred).
Let's first define helper functions to model this environment.
1:
|
|
First we need a function called by our service to retrieve the details of the request (such as machine name, type of the machine, ...) For simplicity here we will assume it's just a machine name.
1: 2: 3: 4: |
|
Now let's define a simple model of the cloud service API used to provision new virtual machines. The function below is just a mockup for the real API: it returns a random number representing the request ID created by the cloud VM provider.
1: 2: 3: 4: 5: |
|
Last we model the cloud API that checks the status of a previously issued request. To simulate the processing time normally required for such operation to complete we count how many times the function was called and after 5 attempts we return 'success'.
1: 2: 3: 4: 5: 6: 7: |
|
Now that our environment is defined we can implement our service operation operationally as follows.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: |
|
The logic is straightforward: we get the machine name from the client who made the request, we then forward the request to the cloud and then poll until the cloud request completes.
This works very well except that, typically, service running in the cloud (e.g., as a worker role or a micro service) can be interrupted at any moment. The machine hosting the service can be restarted, upgraded, or rescaled. In the example above the operation can be interrupted at any point. Suppose for instance that it is stopped right after sending the VM provisioning request to the cloud. What happens next? Typically the infrastructure on which we run our service will detect failure to complete the request after a certain timeout and will schedule a new request. At some point this new request will be picked up by another instance of our service. When this happens we want the operation to resume where it left off instead of restarting from scratch. If we don't handle this situation we may end up with an orphan virtual machine, having to pay for two virtual machines instead of one!
To achieve this we need a mechanism to define resumable control points in our implementation. There should be one resumable point each time an important unit of work is completed. The set of resumable points implicitly defines a global progress state for our operation. Such state can be saved somewhere (for instance on a cloud blob or queue) so that when our function is called again it can read the state and starts where it left off.
Such code transformation can be done manually but it's a tedious error-prone task: it basically boils down to converting your code into a state machine. This requires you to first identify the resumable points in your code, identify the intermediate states at each resumable points, and implement some state serialization/deserialization logic.
What if all this boiler plate code could be generated automatically? What if we could just implement our service operation with almost identical code as above and have the state machine logic generated for us?
Here is how we would like to write it:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: |
|
Resumable computational expression
We need a data type to encode the state of the computation. The state will be the
sequence of all results returned at each resumable control point in the computation.
We thus introduce the type Cell<'t>
to hold the result of type 't
returned at one individual resumable control point:
1: 2: 3: |
|
There is one cell for each resumable control point in the computation. Before the corresponding operation is executed
the cell contains NotExecuted
, once it has been executed the cell holds the
result yielded at the control point.
A resumable control point can then be encoded as a function taking
the current trace of type 'h
as parameter and returning the new trace
together with the new value produced at this control point:
1:
|
|
Finally here comes the meaty part: the definition of the monadic operators. My monadic skills being rather rusty it took me a weekend to figure out the implementation details... The result is deceptively simple:
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: |
|
The idea in the above definition is to decompose a trace of execution as a triple of the form (u,a,v)
:
- The
u
part represents a prefix of the trace. - The
a
part represents the cell in context. -
The
v
part represents the suffix of the trace, that is the result returned at each resumable control point following the cell in context.
I've tried several other encodings before I came up with this one.
It may seem more obvious for instance to encode traces with pairs of the form
(prefix,last)
where prefix
is the list of past executed operations and last
represents the
last operation to execute. Unfortunately defining monadic operators based on such encodings becomes
an insurmountable task. The triple decomposition turns out to be necessary to deal with compositionality in the
implementation of the Bind
monadic operator.
A much better encoding would be to define the trace as a heterogeneous list of elements but the fairly limited type system provided by F# (compared to Haskell for instance) prevents you from doing that. You would have to give up type safety and define your trace as an array of
obj
and appeal to .Net reflection to make this all work. Of course I am not willing to give up type safety so I did not even go there...
We can now define syntactic sugar in F# for the monadic operators above defined:
1:
|
|
Let's make use of it on a simple example first: a computation that creates the pair (1,2)
in two resumable steps:
1: 2: 3: 4: 5: 6: |
|
The type returned by the resumable
construct is a function taking the current state of the
trace as a parameter and returns both the new trace and the overall return value.
In order to call this function we need to pass the inital value of the trace as a parameter
(we will get back to this later in more details).
1: 2: 3: 4: |
|
Each call to example
advance the computation by one step. The first call produces 1
,
the second call produces 2
while the last call returns the expect result of Result(1,2)
.
At each step the current trace state is returned and can be serialized to some external storage.
If the computer running our code fails or restarts the state can be deserialized and resumed
where it left off.
Generating the initial trace state
This works all very well but in practice to actually execute a resumable expression you need to construct the initial state (or in monadic parlance the zero value of the type) yourself. One thing to notice is that the type of the trace is automatically generated by the monadic syntax. In the example above it is:
1: 2: 3: |
|
The initial value for this trace type is therefore
1:
|
|
The challenge is that this type gets bigger as the number of resumable points increases in your resumable expression, and so does the F# expression encoding the initial state value. The more control points you have in your computation the more complex the overall type gets.
So we need a better way to generate the initial value of the trace type for larger computations.
How can we do that? One way is to define a helper function with a phantom type 'v
:
1:
|
|
We can then equivalently define z0
as:
1:
|
|
We can simplify this further: since the value z0 is passed as a parameter to the resumable computation later in the program the type of each cell is automatically inferred therefore we can omit all type parameters:
1: 2: |
|
In fact F# let's you just write:
1: 2: |
|
More generally, if your computation as n
resumable control points the initial trace value is defined as above with instead n
repetitions of the operator z
.
OK, that's pretty neat but you still need to manually count how many resumable points you have in your expression to be able to automatically generate the initial trace value. Worse: the trick actually stops working when dealing with nested resumable expressions!
Generating the initial trace through reflection
I've spent another weekend battling with the F# type system looking for a type-safe method to define the initial trace value, to no avail. It's once again limitations of the F# type system that makes it impossible to define the terminal element of the trace type in a type-safe manner. The core of the issue boils down to the impossibility of pattern matching on types in F#. Instead the solution I came up with involves arcane incantations to the .Net and F# type reflection APIs.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: |
|
Here are some examples of use:
1: 2: 3: 4: 5: 6: 7: 8: 9: |
|
Now that we have the helpers defined we can define the initial trace for the above example as follows
1:
|
|
The phantom type parameter of getZeroType
is automatically inferred and the zero value automatically generated.
Now let's get back to our motivating example for provisioning virtual machines in the cloud:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: |
|
Does this look familiar? The actual implementation is almost identical to the original non-resumable one. We've just sprinkled the resumable keyword in strategic places where we expect the computation to be interrupted. If you have played with F# asynchronous workflows before this concept should be very familiar.
To run the operation we can just execute each individual step as follows:
1: 2: 3: 4: 5: |
|
Each call returns a new state holding the current trace of execution. Executing each step of the computation one at a time is tedious and not very practical. Instead of manually calling the function to advance by a single step we can use the following helper to run the entire operation through completion:
1: 2: 3: 4: 5: 6: 7: 8: |
|
Our service API can then be called in a single command:
1:
|
|
Idempotence
One important property of runThroughCompletion
is that it is idempotent.
This means that executing it once, twice or more times consecutively yields the exact same result.
Mathematically this can be expressed as:
1:
|
|
for all state x.
This is a desirable property for services which can be interrupted for various reasons like outages, updates, scaling or migrations. When such event happens the service typically fails over to a redudant instance of the service that will attempt to execute the same operation again. Making the operation idempotent guarantees that the second execution yields a deterministic outcome regardless of whether the first execution completed or not.
Cancellability and Resumability
Another desirable property is cancellability. Meaning that if the system interrupts
the operation it should leave itself in a well-defined resumable state. In other words it should be possible
to interrupt execution of runThroughCompletion
without bringing the system to a corrupt state.
Note Here I want to highlight an important limitation of the resumable monad: it guarantees cancellability at resumable points only. If the execution is interrupted between those resumable points (for instance after the call to the virtual machine provisioning API but before the call returns with the requestId) then the state of the system will be undefined!
To achieve resumability one needs to identify the possible side-effects that the operation has on the environment. In our example those side-effects are:
- A machine name was requested from the user;
- A request to provision a VM was issued to an external cloud service;
- The polling has completed and the cloud service has honoured the request.
The resumable monad let's you easily capture those side effects and control points using the resumable { ... }
construct.
Execution engine
The other thing we need is an execution engine that takes advantage of those control points to cancel and resume the computation.
We can achieve that by adding persistence to our implementation of run
. Each time we advance the computation to a resumable point we can
save the current trace of execution and persist it to external storage (blob, queue,...). Next time we run the operation
we read the saved state from disk and continue were we left off.
Here is a possible implementation using Json file serialization. (This codes requires the NewtonSoft Json nuget package.):
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: |
|
Now let's excute the service API and simulate a service interruption using an async timeout:
1: 2: 3: 4: |
|
This first run prompts you to enter the machine name, it then submits the request to the cloud and starts polling for completion before failing due to the forced timeouts of 10ms. If you now run the same command once again:
1:
|
|
Not only you are not prompted a machine name but the machine name and request Id are restored from the previous run and the VM provisioning request completes successfully!
Conclusion
I hope you enjoyed this article. For the sake of succinctness I am not going to repeat in the conclusion what I already discussed in this article so please refer to the abstract for that :-)
Possible improvements include support for other monadic operators
(TryFinally
, TryWith
, Using
) and integration with the async
monad
to enable definition of asynchronous resumable computations.
References
There are plenty of references on monads on the internet. You can easily find them with your favourite search engine. One related topic in the monadic world is the so-called "state monad". Also I am sure somebody else already came up with something similar to the resuamble monad. I did not look in the literature myself, if you have a reference please send it to me through github and I'll add the reference here.
Thanks
To Tomas Petricek for his marvelous literate programming package for F# that was used to typeset this HTML page.
Full name: TheResumableMonad.Environment.getMachineName
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
static member BackgroundColor : ConsoleColor with get, set
static member Beep : unit -> unit + 1 overload
static member BufferHeight : int with get, set
static member BufferWidth : int with get, set
static member CapsLock : bool
static member Clear : unit -> unit
static member CursorLeft : int with get, set
static member CursorSize : int with get, set
static member CursorTop : int with get, set
static member CursorVisible : bool with get, set
...
Full name: System.Console
Full name: TheResumableMonad.Environment.provisionVM
type Random =
new : unit -> Random + 1 overload
member Next : unit -> int + 2 overloads
member NextBytes : buffer:byte[] -> unit
member NextDouble : unit -> float
Full name: System.Random
--------------------
System.Random() : unit
System.Random(Seed: int) : unit
Full name: TheResumableMonad.Environment.vmRequestSucceeded
val ref : value:'T -> 'T ref
Full name: Microsoft.FSharp.Core.Operators.ref
--------------------
type 'T ref = Ref<'T>
Full name: Microsoft.FSharp.Core.ref<_>
Full name: TheResumableMonad.MyService.myServiceApi
from TheResumableMonad
Full name: Microsoft.FSharp.Core.Operators.not
type Thread =
inherit CriticalFinalizerObject
new : start:ThreadStart -> Thread + 3 overloads
member Abort : unit -> unit + 1 overload
member ApartmentState : ApartmentState with get, set
member CurrentCulture : CultureInfo with get, set
member CurrentUICulture : CultureInfo with get, set
member DisableComObjectEagerCleanup : unit -> unit
member ExecutionContext : ExecutionContext
member GetApartmentState : unit -> ApartmentState
member GetCompressedStack : unit -> CompressedStack
member GetHashCode : unit -> int
...
Full name: System.Threading.Thread
--------------------
System.Threading.Thread(start: System.Threading.ThreadStart) : unit
System.Threading.Thread(start: System.Threading.ParameterizedThreadStart) : unit
System.Threading.Thread(start: System.Threading.ThreadStart, maxStackSize: int) : unit
System.Threading.Thread(start: System.Threading.ParameterizedThreadStart, maxStackSize: int) : unit
System.Threading.Thread.Sleep(millisecondsTimeout: int) : unit
| NotExecuted
| Result of 't
Full name: TheResumableMonad.Cell<_>
Full name: TheResumableMonad.Resumable<_,_>
type ResumableBuilder =
new : unit -> ResumableBuilder
member Bind : f:Resumable<'u,'a> * g:('a -> Resumable<'v,'b>) -> Resumable<('u * Cell<'a> * 'v),'b>
member Combine : p1:Resumable<'u,unit> * p2:Resumable<'v,'b> -> Resumable<('u * Cell<unit> * 'v),'b>
member Delay : generator:(unit -> Resumable<'u,'a>) -> ('u -> 'u * Cell<'a>)
member Return : x:'t -> (unit -> unit * Cell<'t>)
member ReturnFrom : x:'a -> 'a
member While : gd:(unit -> bool) * prog:Resumable<unit,unit> -> Resumable<unit,unit>
member Zero : unit -> (unit -> unit * Cell<unit>)
Full name: TheResumableMonad.ResumableBuilder
--------------------
new : unit -> ResumableBuilder
Full name: TheResumableMonad.ResumableBuilder.Zero
Full name: TheResumableMonad.ResumableBuilder.Return
Full name: TheResumableMonad.ResumableBuilder.ReturnFrom
Full name: TheResumableMonad.ResumableBuilder.Delay
Full name: Microsoft.FSharp.Core.unit
Full name: TheResumableMonad.ResumableBuilder.Bind
Full name: TheResumableMonad.ResumableBuilder.Combine
Full name: TheResumableMonad.ResumableBuilder.While
Full name: TheResumableMonad.resumable
Full name: TheResumableMonad.Example.example
Full name: TheResumableMonad.Example.s0
Full name: TheResumableMonad.Example.s1
Full name: TheResumableMonad.Example.r1
Full name: TheResumableMonad.Example.s2
Full name: TheResumableMonad.Example.r2
Full name: TheResumableMonad.Example.s3
Full name: TheResumableMonad.Example.r3
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<_>
Full name: TheResumableMonad.Example.z0
Full name: TheResumableMonad.Example.z
Full name: TheResumableMonad.Example.z0_b
Full name: TheResumableMonad.Example.z0_c
Full name: TheResumableMonad.Example.z0_d
Full name: TheResumableMonad.Zero.getZeroUntyped
inherit MemberInfo
member Assembly : Assembly
member AssemblyQualifiedName : string
member Attributes : TypeAttributes
member BaseType : Type
member ContainsGenericParameters : bool
member DeclaringMethod : MethodBase
member DeclaringType : Type
member Equals : o:obj -> bool + 1 overload
member FindInterfaces : filter:TypeFilter * filterCriteria:obj -> Type[]
member FindMembers : memberType:MemberTypes * bindingAttr:BindingFlags * filter:MemberFilter * filterCriteria:obj -> MemberInfo[]
...
Full name: System.Type
Full name: Microsoft.FSharp.Core.Operators.typeof
Full name: Microsoft.FSharp.Core.Operators.box
Full name: Microsoft.FSharp.Core.Operators.typedefof
static member GetExceptionFields : exn:obj * ?bindingFlags:BindingFlags -> obj []
static member GetRecordField : record:obj * info:PropertyInfo -> obj
static member GetRecordFields : record:obj * ?bindingFlags:BindingFlags -> obj []
static member GetTupleField : tuple:obj * index:int -> obj
static member GetTupleFields : tuple:obj -> obj []
static member GetUnionFields : value:obj * unionType:Type * ?bindingFlags:BindingFlags -> UnionCaseInfo * obj []
static member MakeFunction : functionType:Type * implementation:(obj -> obj) -> obj
static member MakeRecord : recordType:Type * values:obj [] * ?bindingFlags:BindingFlags -> obj
static member MakeTuple : tupleElements:obj [] * tupleType:Type -> obj
static member MakeUnion : unionCase:UnionCaseInfo * args:obj [] * ?bindingFlags:BindingFlags -> obj
...
Full name: Microsoft.FSharp.Reflection.FSharpValue
static member FSharpValue.MakeUnion : unionCase:UnionCaseInfo * args:obj [] * ?bindingFlags:System.Reflection.BindingFlags -> obj
static member GetExceptionFields : exceptionType:Type * ?bindingFlags:BindingFlags -> PropertyInfo []
static member GetFunctionElements : functionType:Type -> Type * Type
static member GetRecordFields : recordType:Type * ?bindingFlags:BindingFlags -> PropertyInfo []
static member GetTupleElements : tupleType:Type -> Type []
static member GetUnionCases : unionType:Type * ?bindingFlags:BindingFlags -> UnionCaseInfo []
static member IsExceptionRepresentation : exceptionType:Type * ?bindingFlags:BindingFlags -> bool
static member IsFunction : typ:Type -> bool
static member IsModule : typ:Type -> bool
static member IsRecord : typ:Type * ?bindingFlags:BindingFlags -> bool
static member IsTuple : typ:Type -> bool
...
Full name: Microsoft.FSharp.Reflection.FSharpType
static member FSharpType.GetUnionCases : unionType:System.Type * ?bindingFlags:System.Reflection.BindingFlags -> UnionCaseInfo []
from Microsoft.FSharp.Collections
Full name: Microsoft.FSharp.Collections.Array.map
Full name: Microsoft.FSharp.Core.Operators.invalidArg
Full name: TheResumableMonad.Zero.getZeroTyped
Full name: Microsoft.FSharp.Core.Operators.unbox
from TheResumableMonad.Zero
val string : value:'T -> string
Full name: Microsoft.FSharp.Core.Operators.string
--------------------
type string = System.String
Full name: Microsoft.FSharp.Core.string
Full name: TheResumableMonad.Zero.Tests.y
Full name: Microsoft.FSharp.Core.bool
Full name: TheResumableMonad.MyResumableService.myServiceApi
Full name: TheResumableMonad.MyResumableService.s1
Full name: TheResumableMonad.MyResumableService.r1
from TheResumableMonad
Full name: TheResumableMonad.MyResumableService.s2
Full name: TheResumableMonad.MyResumableService.r2
Full name: TheResumableMonad.MyResumableService.s3
Full name: TheResumableMonad.MyResumableService.r3
Full name: TheResumableMonad.MyResumableService.s4
Full name: TheResumableMonad.MyResumableService.r4
Full name: TheResumableMonad.MyResumableService.s5
Full name: TheResumableMonad.MyResumableService.r5
Full name: TheResumableMonad.runThroughCompletion
Full name: TheResumableMonad.run
from TheResumableMonad
Full name: TheResumableMonad.runResumable
static member AppendAllLines : path:string * contents:IEnumerable<string> -> unit + 1 overload
static member AppendAllText : path:string * contents:string -> unit + 1 overload
static member AppendText : path:string -> StreamWriter
static member Copy : sourceFileName:string * destFileName:string -> unit + 1 overload
static member Create : path:string -> FileStream + 3 overloads
static member CreateText : path:string -> StreamWriter
static member Decrypt : path:string -> unit
static member Delete : path:string -> unit
static member Encrypt : path:string -> unit
static member Exists : path:string -> bool
...
Full name: System.IO.File
System.IO.File.WriteAllText(path: string, contents: string, encoding: System.Text.Encoding) : unit
System.IO.File.ReadAllText(path: string, encoding: System.Text.Encoding) : string
Full name: TheResumableMonad.p
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.async
Full name: TheResumableMonad.t
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<_>
Full name: TheResumableMonad.t2
Full name: TheResumableMonad.WhileTest.x
Full name: TheResumableMonad.WhileTest.m2
Full name: Microsoft.FSharp.Core.Operators.incr
Full name: TheResumableMonad.WhileTest.s1
Full name: TheResumableMonad.WhileTest.r1
Full name: TheResumableMonad.WhileTest.s2
Full name: TheResumableMonad.WhileTest.r2
Full name: TheResumableMonad.WhileTest.s3
Full name: TheResumableMonad.WhileTest.r3
Full name: TheResumableMonad.WhileTest.s4
Full name: TheResumableMonad.WhileTest.r4
Full name: TheResumableMonad.WhileTest.s5
Full name: TheResumableMonad.WhileTest.r5
from TheResumableMonad
Full name: TheResumableMonad.IfTest.y
Full name: TheResumableMonad.IfTest.iftest
Full name: TheResumableMonad.IfTest.s1
Full name: TheResumableMonad.IfTest.r1
Full name: TheResumableMonad.IfTest.s2
Full name: TheResumableMonad.IfTest.r2
Full name: TheResumableMonad.IfTest.s3
Full name: TheResumableMonad.IfTest.r3
Full name: TheResumableMonad.IfTest.s4
Full name: TheResumableMonad.IfTest.r4
Full name: TheResumableMonad.IfTest.s5
Full name: TheResumableMonad.IfTest.r5