Header menu logo IcedTasks

Why is there different binds?

Here be dragons

You may have browsed around the F# codebase for Resumable Tasks or this one have noticed this Bind method:

[<NoEagerConstraintApplication>]
member inline _.Bind< ^TaskLike, 'TResult1, 'TResult2, ^Awaiter, 'TOverall
    when ^TaskLike: (member GetAwaiter: unit -> ^Awaiter)
    and ^Awaiter :> ICriticalNotifyCompletion
    and ^Awaiter: (member get_IsCompleted: unit -> bool)
    and ^Awaiter: (member GetResult: unit -> 'TResult1)>
    (
        task: ^TaskLike,
        continuation: ('TResult1 -> TaskCode<'TOverall, 'TResult2>)
    ) : TaskCode<'TOverall, 'TResult2> =

What is going on here? Well to understand this, we need to understand a few other concepts first.

In programming, there's something called duck typing. Essentially if an object fits a certain shape, methods can be called. For instance, you don't need to implement IEnumerator on an object to use it in a foreach loop, you just need to implement the methods that IEnumerable requires, such as Current and MoveNext()

See this very good article describing this in detail. Things you might not know about CSharp - Duck Typing for a bit more details.

This is also true of async/await. As long as an object has GetAwaiter (known as an Awaitable), and returns an Awaiter which subsequently implements INotifyCompletion and has members IsCompleted, and OnCompleted, it can be used in an async method. (See article above for more details). But ok, why is this important? It's because this creates parity with C#. The most common example of an Awaitable that isn't a Task/Task<T>/ValueTask/ValueTask<T> is Task.Yield() which returns a YieldAwaitable.

Ok so how does this relate to the previous Bind method.

To handle duck typing F# has this concept known as Statically Resolved Type Parameters, allowing you to get shapes of objects at compile time. This allows for use to use that Awaitable/Awaiter concepts in the generic sense. This means as long as it fits the shape, like in the case of Task.Yield() it will work. Awesome!

However when you go to use a Task<T> you get this error message:

A unique overload for method 'GetAwaiter' could not be determined based on type information prior to this program point. A type annotation may be needed.

Known return type: Awaiter<^a,'b>

Candidates:
 - Task.GetAwaiter() : TaskAwaiter
 - Task.GetAwaiter() : TaskAwaiter<'TResult1>F# Compiler43

Why is this? Because Task<T> implements two GetAwaiter methods and the compiler can't figure out which one to use (one for the generic and non generic form of Task<T>and Task. So we need to create an overload that takes that Task<T> specifically. ValueTask<T> don't suffer from this as it was designed not to implement both the generic and non generic form. See this issue Provide conversion from ValueTask to ValueTask

So we have this overload:

member inline _.Bind
    (
        task: Task<'TResult1>,
        continuation: ('TResult1 -> TaskCode<'TOverall, 'TResult2>)
    ) : TaskCode<'TOverall, 'TResult2> =

In IcedTasks we use Source member for this.

member inline _.Source(task: Task<'T>) =
    (fun (ct: CancellationToken) -> task.GetAwaiter())

This is a very underdocumented feature of F# Computation Expressions. The best docs on it are on this StackOverflow post Why would you use Builder.Source() in a custom computation expression builder?. But this lets us not have to repeat Bind/ReturnFrom members for every type we want to support.

type unit = Unit
type bool = System.Boolean
val task: TaskBuilder

Type something to start searching.