-
Notifications
You must be signed in to change notification settings - Fork 842
Description
I am after some guidance on implementing computation expressions (CEs) that build upon the new built in task computation expression(s). The code can be found here. I will offer two examples and the hurdles that I ran into trying to implement them in the way i had envisaged. I had hoped that I would be able to implement the below CEs in a way that takes advantage of the performance characteristics and stacktrace benefits of the new task CEs. I know that I can use the task CE directly to implement them, but it is my understanding that task.Run would then be called for each function in the builder.
The API
I thought it might be useful to summarize the TaskBuilder api here to make it easier to refer to, it is more complicated in reality but essentially boils down to:
member inline _.Delay(generator : unit -> TaskCode<'TOverall, 'T>) : TaskCode<'TOverall, 'T>
member inline _.Zero() : TaskCode<'TOverall, unit>
member inline _.Return (value: 'T) : TaskCode<'T, 'T>
member inline _.Combine(task1: TaskCode<'TOverall, unit>, task2: TaskCode<'TOverall, 'T>) : TaskCode<'TOverall, 'T>
member inline _.While ([<InlineIfLambda>] condition : unit -> bool, body : TaskCode<'TOverall, unit>) : TaskCode<'TOverall, unit>
member inline _.TryWith (body: TaskCode<'TOverall, 'T>, catch: exn -> TaskCode<'TOverall, 'T>) : TaskCode<'TOverall, 'T>
member inline _.TryFinally (body: TaskCode<'TOverall, 'T>, [<InlineIfLambda>] compensation : unit -> unit) : TaskCode<'TOverall, 'T>
member inline _.For (sequence : seq<'T>, body : 'T -> TaskCode<'TOverall, unit>) : TaskCode<'TOverall, unit>
static member _.BindDynamic (sm: byref<_>, task: Task<'TResult1>, continuation: ('TResult1 -> TaskCode<'TOverall, 'TResult2>)) : bool
member inline _.Bind (task: Task<'TResult1>, continuation: ('TResult1 -> TaskCode<'TOverall, 'TResult2>)) : TaskCode<'TOverall, 'TResult2>
static member _.RunDynamic(code: TaskCode<'T, 'T>) : Task<'T>
member inline _.Run(code : TaskCode<'T, 'T>) : Task<'T>Some relevant types for TaskCode are:
type ResumableCode<'Data, 'T> = delegate of byref<ResumableStateMachine<'Data>> -> bool
[<Struct; NoComparison; NoEquality>]
type TaskStateMachineData<'T> =
[<DefaultValue(false)>]
val mutable Result : 'T
[<DefaultValue(false)>]
val mutable MethodBuilder : AsyncTaskMethodBuilder<'T>
and TaskStateMachine<'TOverall> = ResumableStateMachine<TaskStateMachineData<'TOverall>>
and TaskResumptionFunc<'TOverall> = ResumptionFunc<TaskStateMachineData<'TOverall>>
and TaskResumptionDynamicInfo<'TOverall> = ResumptionDynamicInfo<TaskStateMachineData<'TOverall>>
and TaskCode<'TOverall, 'T> = ResumableCode<TaskStateMachineData<'TOverall>, 'T>Porting Expecto's TestTaskBuilder:
The implementation that I was attempting to port can be found here. It is essentially a simple wrapper around a Task Builder, with some Expecto specific logic for Run. I started with the exact code linked to above (but with a dependency on the built in task {...} and went from there:
-
Issue 1: Compiler error
error FS3501: Invalid resumable code. Any method of function accepting or returning resumable code must be marked 'inline'- Resolution: Mark everything except
Runas inline. It is possible inlineRunas well, ifnameandfocusStateare made available publicly. - Question: Why is it legal to leave
Runas standard member (not inline), given it takes aTaskCodeas a parameter.
- Resolution: Mark everything except
-
Issue 2: Compiler error
error FS3511: This state machine is not statically compilable. The resumable code value(s) 'f' does not have a definition. An alternative dynamic implementation will be used, which may be slower. Consider adjusting your code to ensure this state machine is statically compilable, or else suppress this warning.- Resolution: It appears that calling
task.Runwith theTaskCodepassed in always produces this warning, I could not find a way to get around it, the only way I could get it to compile was to suppress the error with#nowarn "3511"
- Resolution: It appears that calling
Next I tried to annotate the TestTaskBuilder with types.
- Issue 3: Compiler error due to using
TaskCodein the type annotations.This construct is for use by compiled F# code and should not be used directly.- Resolution: I was able to suppress this with
#nowarn "1204", but I was pretty surprised that using this type is so discouraged.
- Resolution: I was able to suppress this with
Implementing a TaskResultBuilder:
We have a TaskResultBuilder that depends on TaskBuilder.fs. I wanted to port it over to depend on the new built in TaskBuilder, it is quite similar to the one provided in FsToolkit.ErrorHandling which depends on Ply and it's intermediatry Ply<'u> type and uply CE, so I will link to it for additional context.
The TaskResult type:
type TaskResult<'TResult, 'Error> = TaskResult of Task<Result<'TResult, 'Error>>- Issue 1:
Bind
I had hoped to create aBindfunction with the signature, similar to thisHowever there is no obvious (to me anyway) way to create amember inline _.Bind ( tr: TaskResult<'TResult1, 'Error>, f: 'TResult1 -> TaskCode<Result<'TOverall, 'Error>, Result<'TResult2, 'Error>> ) : TaskCode<Result<'TOverall, 'Error>, Result<'TResult2, 'Error>>
TaskCode<Result<'TOverall, 'Error>, Result<'TResult2, 'Error>>when the result is an error:let binder r = match r with | Ok x -> f x | Error e -> // How do we create the `TaskCode` here?
- Issue 2:
While
I had hoped to create aWhilefunction with the signature, similar to this:However there is no obvious way to retrieve the result, calling body will only give you amember inline _.While ( condition : unit -> bool, body : TaskCode<Result<'TOverall, 'Error>, Result<unit, 'Error>> ) : TaskCode<Result<'TOverall, 'Error>, Result<unit, 'Error>>
boolback that is related to the resumable code flow:let mutable finished = false let mutable result = Ok() while not finished && condition () do let resultOfBody = // How do we retrieve the `Result` from `TaskCode` here match resultOfBody with | Ok x -> x | Error _ as e -> result <- e finished <- true // How do we create the `TaskCode` to return here?
I'm not sure the best way to resolve the two issues above, but I am hoping to utilize TaskBuilder and not have to write the low level resumable code myself. I know that when creating a TaskCode you provide a function that has a TaskStateMachineData parameter, however the Result property of that type restricts the 'TOverall type parameter not the 'T type parameter
General questions:
- Am I misunderstanding the general approach I should be taking?
- Are there some combinators, or some higher level abstractions that I can build on that I am missing?
- Is this approach supported at all? If not what approach is recommended?
Any advice or insight would be greatly appreciated. If there is a more appropriate place to ask/post this question let me know and I will do so.