IcedTasks

repository·master·Indexed 41 days ago

https://github.com/theangrybyrd/icedtasks

A 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+.

Tokens
8.1K
Snippets
15
Records
32
Agent score
95%

What's inside IcedTasks

  1. What is IcedTasks?

    master
    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.
  2. Overview of IcedTasks computation expression builders

    master

    IcedTasks provides specialized F# computation expression builders for various asynchronous shapes that extend beyond the standard task expression in FSharp.Core. Use these builders depending on your performance, cancellation, and execution requirements:

    • Standard Tasks: task and backgroundTask.
    • ValueTask variants: valueTask, vTask, and pooling ValueTask variants (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).
  3. Use asyncEx via async shadowing

    master

    By opening IcedTasks.Polyfill.Async, the async { ... } syntax is shadowed to use the asyncEx builder instead of the standard F# async builder.

    This allows you to maintain standard F# Async<'T> semantics while gaining:

    • Direct binding of Task, ValueTask, and other awaitables.
    • IAsyncDisposable support.
    • IAsyncEnumerable<'T> iteration.
    • AsyncEx-specific task exception behavior.

    Recommendation: For most code, prefer importing IcedTasks.AsyncEx and using asyncEx { ... } explicitly to avoid confusing readers with shadowed names.

  4. Use ColdTask for lazy task evaluation

    master

    coldTask is an alias for unit -> Task<'T>. Unlike standard tasks that start executing immediately upon creation, ColdTask is '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 ""
    }
  5. When to use pooling builders vs standard builders

    master

    Pooling builders are specialized tools for performance optimization. You should follow this decision flow:

    1. Start with valueTask or cancellableValueTask: Use these when you are still defining your API shape or if the workload is not performance-critical.
    2. Switch to poolingValueTask or cancellablePoolingValueTask only 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.
  6. Understanding why IcedTasks uses different Bind and Source methods

    master

    IcedTasks provides specialized methods to handle the way F# interacts with different task types (like Task<'T> vs ValueTask<'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-generic GetAwaiter method. 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 Source member in its computation expression builder. This allows the library to support specific types like Task<'T> without needing to manually implement redundant Bind and ReturnFrom members for every possible task type.

  7. Use background builders to escape synchronization contexts

    master

    The 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 current SynchronizationContext or 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 background variant if needed.

  8. Use `and!` for independent operations in IcedTasks

    master

    In IcedTasks, the and! F# computation expression syntax is used for applicative composition. Use and! 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 a cancellableTask (or similar builders), both operations receive the same ambient CancellationToken and 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
        }
  9. Compare IcedTasks task vs FSharp.Core task

    master

    While FSharp.Core's task expression is the default for Task<'T> in F#, IcedTasks.Polyfill.Task.task provides 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 IAsyncDisposable using use and use!.
    • Async streams: Support for IAsyncEnumerable<'T> in for loops.
    • Awaitable binding: Binding supported awaitables through IcedTasks Source/awaiter machinery.

    Note that the resulting type remains Task<'T>.

  10. Use AsyncEx for enhanced async semantics

    master

    The asyncEx computation expression provides several improvements over standard F# async:

    1. IAsyncDisposable support: Allows using the use keyword with IAsyncDisposable objects.
    2. Awaitable interoperability: Allows let! and do! to work directly against Task<'T>, ValueTask<'T>, and any other Awaitable type.
    3. Improved Exception Handling: When Tasks throw exceptions, asyncEx avoids wrapping them in AggregateException, allowing you to catch the specific exception directly.
    4. IAsyncEnumerable support: Allows using the for keyword to iterate over IAsyncEnumerable<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
    }
  11. Adopt an import strategy for IcedTasks polyfills

    master

    When using polyfill namespaces, choose an import strategy that balances convenience with clarity. Consistency is key so that readers can quickly identify when task, backgroundTask, or async refers to an IcedTasks builder.

    PatternWhen to use it
    Local module openPreferred default. Keeps shadowing visible and contained to a specific scope.
    File-level openUseful when the entire file intentionally uses IcedTasks builders.
    Project-wide conventionReasonable only after the team has agreed that IcedTasks builders are the standard for the entire codebase.