IcedTasks
repository·master·Indexed 41 days ago
https://github.com/theangrybyrd/icedtasksA library providing specialized F# computation expressions for Task, ValueTask, and Async. It leverages Resumable Code features from F# 6.0 to offer improved performance, lazy evaluation (cold tasks), and seamless cancellation management. Key features include asyncEx for enhanced async semantics, support for IAsyncDisposable and IAsyncEnumerable, and pooling-backed ValueTask builders for allocation-sensitive code in .NET 6+.
What's inside IcedTasks
- IcedTasks is a library providing additional F# computation expressions (CEs) for the Task CE, leveraging the Resumable Code features introduced in F# 6.0. It extends asynchronous programming capabilities with specialized builders for performance, lazy evaluation, and cancellation management.
Overview of IcedTasks computation expression builders
masterIcedTasks provides specialized F# computation expression builders for various asynchronous shapes that extend beyond the standard
taskexpression inFSharp.Core. Use these builders depending on your performance, cancellation, and execution requirements:- Standard Tasks:
taskandbackgroundTask. - ValueTask variants:
valueTask,vTask, and poolingValueTaskvariants (optimized for reduced allocations). - Cold Tasks:
coldTask(for tasks that do not start execution until explicitly awaited). - Cancellable Tasks:
cancellableTask,cancellableValueTask, and cancellable pooling variants (designed for request-scoped cancellation). - Async Interop:
asyncEx(for improved .NET async interop). - Parallelism:
parallelAsync(for running operations in parallel).
- Standard Tasks:
Use asyncEx via async shadowing
masterBy opening
IcedTasks.Polyfill.Async, theasync { ... }syntax is shadowed to use theasyncExbuilder instead of the standard F#asyncbuilder.This allows you to maintain standard F#
Async<'T>semantics while gaining:- Direct binding of
Task,ValueTask, and other awaitables. IAsyncDisposablesupport.IAsyncEnumerable<'T>iteration.- AsyncEx-specific task exception behavior.
Recommendation: For most code, prefer importing
IcedTasks.AsyncExand usingasyncEx { ... }explicitly to avoid confusing readers with shadowed names.- Direct binding of
Use ColdTask for lazy task evaluation
mastercoldTaskis an alias forunit -> Task<'T>. Unlike standard tasks that start executing immediately upon creation,ColdTaskis 'cold', meaning it is only executed when the resulting function is called. This allows for deferred execution and re-runnable tasks.open IcedTasks let coldTask_dont_start_immediately = task { let mutable someValue = null let fooColdTask = coldTask { someValue <- 42 } do! Async.Sleep(100) // someValue is still null because fooColdTask hasn't been called Expect.equal someValue null "" // Calling the function starts execution do! fooColdTask () Expect.equal someValue 42 "" }Use backgroundTask to avoid caller context
masterTheIcedTasks.Polyfill.Tasknamespace provides a shadowedbackgroundTaskbuilder. This builder is intended to behave similarly toConfigureAwait(false), helping to avoid staying tied to the caller's synchronization context or non-default scheduler.When to use pooling builders vs standard builders
masterPooling builders are specialized tools for performance optimization. You should follow this decision flow:
- Start with
valueTaskorcancellableValueTask: Use these when you are still defining your API shape or if the workload is not performance-critical. - Switch to
poolingValueTaskorcancellablePoolingValueTaskonly when:- You are targeting .NET 6 or later.
- The API shape is already established as
ValueTask<'T>. - The code is running on a hot path.
- The code is highly allocation-sensitive.
- Start with
Understanding why IcedTasks uses different Bind and Source methods
masterIcedTasks provides specialized methods to handle the way F# interacts with different task types (like
Task<'T>vsValueTask<'T>).In F#, async/await relies on 'duck typing' via Statically Resolved Type Parameters. While this allows generic support for any 'Awaitable' object, certain types like
Task<'T>present an ambiguity: they implement both a generic and a non-genericGetAwaitermethod. This causes the F# compiler to fail with the error:A unique overload for method 'GetAwaiter' could not be determined based on type information prior to this program point.To resolve this ambiguity and maintain compatibility with both C# and F# async patterns, IcedTasks uses the
Sourcemember in its computation expression builder. This allows the library to support specific types likeTask<'T>without needing to manually implement redundantBindandReturnFrommembers for every possible task type.Use background builders to escape synchronization contexts
masterThe
background*variants (e.g.,backgroundTask,backgroundColdTask,backgroundCancellableTask) are used to switch execution to the thread pool. They are conceptually similar to using.ConfigureAwait(false)in C#. Use them when you want to ensure the continuation does not stay tied to the currentSynchronizationContextor scheduler (for example, to avoid blocking a UI thread).Note: You should first choose your desired async shape (task, coldTask, etc.) and then apply the
backgroundvariant if needed.Use `and!` for independent operations in IcedTasks
masterIn IcedTasks, the
and!F# computation expression syntax is used for applicative composition. Useand!when you have multiple operations that are independent of each other and you need the results of all of them to proceed.When using
and!within acancellableTask(or similar builders), both operations receive the same ambientCancellationTokenand are started concurrently before either result is awaited. This allows for concurrent execution rather than waiting for each step to finish sequentially.Key Rule:
- Use
and!for independent operations (where the second operation does not need the result of the first). - Use sequential
let!for dependent operations (where the second operation requires the result of the first).
let combined = cancellableTask { let! customer = loadCustomer customerId and! orders = loadOrders customerId return customer, orders }- Use
Compare IcedTasks task vs FSharp.Core task
masterWhile FSharp.Core's
taskexpression is the default forTask<'T>in F#,IcedTasks.Polyfill.Task.taskprovides an enhanced builder that participates in the IcedTasks interop surface.Use the IcedTasks builder when you need:
- Applicative composition: Using
and!with supported task-like operands. - Async disposal: Support for
IAsyncDisposableusinguseanduse!. - Async streams: Support for
IAsyncEnumerable<'T>inforloops. - Awaitable binding: Binding supported awaitables through IcedTasks
Source/awaiter machinery.
Note that the resulting type remains
Task<'T>.- Applicative composition: Using
Use AsyncEx for enhanced async semantics
masterThe
asyncExcomputation expression provides several improvements over standard F#async:- IAsyncDisposable support: Allows using the
usekeyword withIAsyncDisposableobjects. - Awaitable interoperability: Allows
let!anddo!to work directly againstTask<'T>,ValueTask<'T>, and any otherAwaitabletype. - Improved Exception Handling: When Tasks throw exceptions,
asyncExavoids wrapping them inAggregateException, allowing you to catch the specific exception directly. - IAsyncEnumerable support: Allows using the
forkeyword to iterate overIAsyncEnumerable<T>types.
open IcedTasks // 1. IAsyncDisposable let fakeDisposable () = { new IAsyncDisposable with member __.DisposeAsync() = ValueTask.CompletedTask } let myAsyncEx = asyncEx { use _ = fakeDisposable () return 42 } // 2. Awaiting Tasks/ValueTasks let myAsyncEx2 = asyncEx { let! _ = task { return 42 } // Task<T> let! _ = valueTask { return 42 } // ValueTask<T> return 42 } // 3. Exception handling let outer = asyncEx { try do! asyncEx { raise (ArgumentException "foo") } return () with | :? ArgumentException -> return () | ex -> return raise (Exception("Error", ex)) } // 4. IAsyncEnumerable let myAsyncEx3 = asyncEx { let items = taskSeq { // IAsyncEnumerable<T> yield 42 yield 1701 } let mutable sum = 0 for i in items do sum <- sum + i return sum }- IAsyncDisposable support: Allows using the
Adopt an import strategy for IcedTasks polyfills
masterWhen using polyfill namespaces, choose an import strategy that balances convenience with clarity. Consistency is key so that readers can quickly identify when
task,backgroundTask, orasyncrefers to an IcedTasks builder.Recommended Patterns
Pattern When to use it Local module open Preferred default. Keeps shadowing visible and contained to a specific scope. File-level open Useful when the entire file intentionally uses IcedTasks builders. Project-wide convention Reasonable only after the team has agreed that IcedTasks builders are the standard for the entire codebase.