Logo Orsak

Effect sequences

An Effect produces one result. Some work produces many, over time: the pages of an API, the rows of a query, the messages of a queue. An EffSeq<'r, 'a, 'e> is an effect for that: like an effect, it requires an environment 'r to start, and can fail with 'e, but it produces a sequence of 'a, as an async enumerable, one item at a time. Like an effect, it is cold: nothing happens until it is enumerated.

Effect sequences are written with the effSeq computation expression, and read with a for loop in eff.

This page is a script: it runs with dotnet fsi docs/effect-sequences.fsx once Orsak is built, and the output shown is from such a run.

A paged API

The examples read orders from an API that returns them a page at a time, with the number of the next page, if any. It is described by an interface, with a provider interface and a module creating the effect:

open System.Threading.Tasks
open Orsak

type Order = { Id: int; Total: decimal }

type OrderPage = { Orders: Order list; NextPage: int option }

type ApiError = ServiceUnavailable of page: int

type IOrderApi =
    abstract GetPage: page: int -> Task<Result<OrderPage, ApiError>>

type IOrderApiProvider =
    abstract Orders: IOrderApi

module OrderApi =
    let getPage page =
        Effect.Create(fun (p: #IOrderApiProvider) -> p.Orders.GetPage page)

The implementation here has three pages of two orders each, prints every page it is asked for, and can be made to fail on a given page:

type OrderApi(?failOnPage: int) =
    interface IOrderApi with
        member _.GetPage page = task {
            printfn $"  fetching page {page}"

            if Some page = failOnPage then
                return Error(ServiceUnavailable page)
            else
                let orders = [ for i in 1..2 -> { Id = (page - 1) * 2 + i; Total = decimal (page * 100 + i * 10) } ]
                return Ok { Orders = orders; NextPage = if page < 3 then Some(page + 1) else None }
        }

type Env(api: IOrderApi) =
    interface IOrderApiProvider with
        member _.Orders = api

Writing a sequence

effSeq works like eff: let! binds effects, tasks and results, and loops work as usual. In addition, yield produces an item, and yield! all the items of a list, another sequence, or an async enumerable. This sequence reads every order, one page after another:

let allOrders () = effSeq {
    let mutable page = Some 1

    while page.IsSome do
        let! result = OrderApi.getPage page.Value
        yield! result.Orders
        page <- result.NextPage
}

Its type is inferred as EffSeq<#IOrderApiProvider, Order, ApiError>: it needs an environment that provides the API, like the effect it uses.

Reading a sequence

A for loop in eff reads a sequence, with the environment of the surrounding effect:

let totalOfAllOrders () = eff {
    let mutable total = 0m

    for order in allOrders () do
        printfn $"order {order.Id}: {order.Total}"
        total <- total + order.Total

    return total
}

let run (effect: Effect<Env, 'a, ApiError>) (api: IOrderApi) =
    let result = effect |> Effect.run (Env api)
    printfn $"result: %A{result.AsTask().Result}"

run (totalOfAllOrders ()) (OrderApi())
  fetching page 1
order 1: 110
order 2: 120
  fetching page 2
order 3: 210
order 4: 220
  fetching page 3
order 5: 310
order 6: 320
result: Ok 1290M

The pages are fetched as the loop gets to them, not up front: the sequence only does work when the next item is asked for.

Reading part of a sequence

Since the sequence is lazy, reading only the beginning does only the work needed for it. Invoke starts the sequence with an environment, and returns it as an IAsyncEnumerable of results, for use with other libraries, such as FSharp.Control.TaskSeq, or as here by hand:

let firstOrders count (api: IOrderApi) = task {
    let enumerator = (allOrders ()).Invoke(Env api).GetAsyncEnumerator()
    let mutable remaining = count

    while remaining > 0 do
        let! hasNext = enumerator.MoveNextAsync()

        if hasNext then
            match enumerator.Current with
            | Ok order -> printfn $"order {order.Id}: {order.Total}"
            | Error error -> printfn $"error: %A{error}"

            remaining <- remaining - 1
        else
            remaining <- 0

    do! enumerator.DisposeAsync()
}

(firstOrders 3 (OrderApi())).Wait()
  fetching page 1
order 1: 110
order 2: 120
  fetching page 2
order 3: 210

Only the two pages holding the first three orders were fetched.

Building sequences from sequences

A for loop in effSeq reads another sequence, so sequences can be filtered and transformed with the usual constructs:

let largeOrders () = effSeq {
    for order in allOrders () do
        if order.Total > 200m then
            yield order
}

let printLargeOrders () = eff {
    for order in largeOrders () do
        printfn $"large order {order.Id}: {order.Total}"
}

run (printLargeOrders ()) (OrderApi())
  fetching page 1
  fetching page 2
large order 3: 210
large order 4: 220
  fetching page 3
large order 5: 310
large order 6: 320
result: Ok ()

Errors

When an effect in a sequence fails, the sequence produces the error as its last item, and ends. The for loop in eff then fails the surrounding effect with it, after the items that came before:

run (totalOfAllOrders ()) (OrderApi(failOnPage = 3))
  fetching page 1
order 1: 110
order 2: 120
  fetching page 2
order 3: 210
order 4: 220
  fetching page 3
result: Error (ServiceUnavailable 3)

As with effects, errors are values: code that reads the sequence with Invoke gets them as Error items, and decides itself what to do with them.

namespace System
namespace System.Threading
namespace System.Threading.Tasks
namespace Orsak
type Order = { Id: int Total: decimal }
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

--------------------
type int = int32

--------------------
type int<'Measure> = int
Multiple items
val decimal: value: 'T -> decimal (requires member op_Explicit)

--------------------
type decimal = System.Decimal

--------------------
type decimal<'Measure> = decimal
type OrderPage = { Orders: Order list NextPage: int option }
type 'T list = List<'T>
type 'T option = Option<'T>
type ApiError = | ServiceUnavailable of page: int
type IOrderApi = abstract GetPage: page: int -> Task<Result<OrderPage,ApiError>>
Multiple items
type Task = interface IAsyncResult interface IDisposable new: action: Action -> unit + 7 overloads member ConfigureAwait: continueOnCapturedContext: bool -> ConfiguredTaskAwaitable + 1 overload member ContinueWith: continuationAction: Action<Task,obj> * state: obj -> Task + 19 overloads member Dispose: unit -> unit member GetAwaiter: unit -> TaskAwaiter member RunSynchronously: unit -> unit + 1 overload member Start: unit -> unit + 1 overload member Wait: unit -> unit + 5 overloads ...
<summary>Represents an asynchronous operation.</summary>

--------------------
type Task<'TResult> = inherit Task new: ``function`` : Func<obj,'TResult> * state: obj -> unit + 7 overloads member ConfigureAwait: continueOnCapturedContext: bool -> ConfiguredTaskAwaitable<'TResult> + 1 overload member ContinueWith: continuationAction: Action<Task<'TResult>,obj> * state: obj -> Task + 19 overloads member GetAwaiter: unit -> TaskAwaiter<'TResult> member WaitAsync: cancellationToken: CancellationToken -> Task<'TResult> + 4 overloads member Result: 'TResult static member Factory: TaskFactory<'TResult>
<summary>Represents an asynchronous operation that can return a value.</summary>
<typeparam name="TResult">The type of the result produced by this <see cref="T:System.Threading.Tasks.Task`1" />.</typeparam>


--------------------
Task(action: System.Action) : Task
Task(action: System.Action, cancellationToken: System.Threading.CancellationToken) : Task
Task(action: System.Action, creationOptions: TaskCreationOptions) : Task
Task(action: System.Action<obj>, state: obj) : Task
Task(action: System.Action, cancellationToken: System.Threading.CancellationToken, creationOptions: TaskCreationOptions) : Task
Task(action: System.Action<obj>, state: obj, cancellationToken: System.Threading.CancellationToken) : Task
Task(action: System.Action<obj>, state: obj, creationOptions: TaskCreationOptions) : Task
Task(action: System.Action<obj>, state: obj, cancellationToken: System.Threading.CancellationToken, creationOptions: TaskCreationOptions) : Task

--------------------
Task(``function`` : System.Func<'TResult>) : Task<'TResult>
Task(``function`` : System.Func<obj,'TResult>, state: obj) : Task<'TResult>
Task(``function`` : System.Func<'TResult>, cancellationToken: System.Threading.CancellationToken) : Task<'TResult>
Task(``function`` : System.Func<'TResult>, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : System.Func<obj,'TResult>, state: obj, cancellationToken: System.Threading.CancellationToken) : Task<'TResult>
Task(``function`` : System.Func<obj,'TResult>, state: obj, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : System.Func<'TResult>, cancellationToken: System.Threading.CancellationToken, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : System.Func<obj,'TResult>, state: obj, cancellationToken: System.Threading.CancellationToken, creationOptions: TaskCreationOptions) : Task<'TResult>
Multiple items
module Result from Microsoft.FSharp.Core

--------------------
type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
type IOrderApiProvider = abstract Orders: IOrderApi
val getPage: page: int -> Effect<#IOrderApiProvider,OrderPage,ApiError>
val page: int
Multiple items
union case Effect.Effect: EffectDelegate<'r,'a,'e> -> Effect<'r,'a,'e>

--------------------
module Effect from Orsak
<summary> Functions for running, combining, recovering and repeating effects. </summary>

--------------------
type Effect = static member Create: f: ('a -> Task<Result<'b,'e>>) -> Effect<'a,'b,'e> + 2 overloads static member Error: error: 'e -> Effect<'a,'b,'e>
<summary> Creates effects. <c>Effect.Create</c> turns a function of an environment into an effect, and is how effects are made from the interfaces that describe side effects. The function may return a plain value, a <c>Result</c>, a <c>Task</c>, a <c>ValueTask</c> or an <c>Async</c>, of a value or of a <c>Result</c>; returning a <c>Result</c> lets the effect fail. </summary>
<example> The usual pattern: an interface describing the side effect, a provider interface exposing it, and a function creating the effect, which requires any environment that implements the provider (<c>#IConsoleProvider</c>). <code lang="fsharp"> type IConsole = abstract ReadLine: unit -&gt; string abstract WriteLine: string -&gt; unit type IConsoleProvider = abstract Console: IConsole module Console = let readLine () = Effect.Create(fun (p: #IConsoleProvider) -&gt; p.Console.ReadLine()) let writeLine line = Effect.Create(fun (p: #IConsoleProvider) -&gt; p.Console.WriteLine line) </code> The Orsak.Myriad generators can write the functions of such a module from the interface. </example>


--------------------
type Effect<'r,'a,'e> = | Effect of EffectDelegate<'r,'a,'e> member Run: r: 'r -> AsyncResult<'a,'e> member RunOrFail: r: 'r -> Task<'a> static member (<*>)<'b,'a (requires member (+))> : f: Effect<'r,('b -> 'a),'a0> * e: Effect<'r,'b,'a0> -> Effect<'r,'a,'a0> (requires member (+)) static member (>>=) : h: Effect<'r,'a,'e> * f: ('a -> Effect<'r,'b,'e>) -> Effect<'r,'b,'e> static member Map: h: Effect<'r,'a,'e> * f: ('a -> 'b) -> Effect<'r,'b,'e> static member Return<'a,'b,'c> : a: 'a0 -> Effect<'b,'a0,'c> static member ap<'r,'a,'b,'e> : applicative: Effect<'r,('b -> 'a),'e> -> e: Effect<'r,'b,'e> -> Effect<'r,'a,'e>
<summary> Describes an effect that can either succeed with <typeparamref name="'a" />, or fail with <typeparamref name="'e" />. The effect is 'cold', and only starts when run with an <typeparamref name="'r" />. </summary>
<remarks> Effects are usually written with the <c>eff</c> computation expression, and created from interfaces that describe side effects with <c>Effect.Create</c>. The environment <typeparamref name="'r" /> is inferred from what the effect uses, and supplied when the effect is run. </remarks>
<example><code lang="fsharp"> let greet () = eff { let! name = Console.readLine () do! Console.writeLine $"Hello {name}" } // at the composition root, with an environment providing the console effects: let! result = greet () |&gt; Effect.run env </code></example>
<typeparam name="'r"> The environment required to run the effect </typeparam>
<typeparam name="'a"> The resulting type when the effect runs successfully </typeparam>
<typeparam name="'e"> The resulting type when the effect fails</typeparam>
static member Effect.Create: f: ('a -> 'b) -> Effect<'a,'b,'e>
static member Effect.Create: f: ('a -> Async<'b>) -> Effect<'a,'b,'a0>
static member Effect.Create: f: ('a -> Result<'b,'e>) -> Effect<'a,'b,'e>
static member Effect.Create: f: ('a -> Task<'b>) -> Effect<'a,'b,'a0>
static member Effect.Create: f: ('a -> ValueTask<'b>) -> Effect<'a,'b,'a0>
static member Effect.Create: f: ('a -> Async<Result<'b,'e>>) -> Effect<'a,'b,'e>
static member Effect.Create: f: ('a -> ValueTask<Result<'b,'e>>) -> Effect<'a,'b,'e>
static member Effect.Create: f: ('a -> Task<Result<'b,'e>>) -> Effect<'a,'b,'e>
val p: #IOrderApiProvider
property IOrderApiProvider.Orders: IOrderApi with get
abstract IOrderApi.GetPage: page: int -> Task<Result<OrderPage,ApiError>>
Multiple items
module OrderApi from Effect-sequences

--------------------
type OrderApi = interface IOrderApi new: ?failOnPage: int -> OrderApi

--------------------
new: ?failOnPage: int -> OrderApi
val failOnPage: int option
val task: TaskBuilder
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
union case Option.Some: Value: 'T -> Option<'T>
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
union case ApiError.ServiceUnavailable: page: int -> ApiError
val orders: Order list
val i: int
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
union case Option.None: Option<'T>
Multiple items
type Env = interface IOrderApiProvider new: api: IOrderApi -> Env

--------------------
new: api: IOrderApi -> Env
val api: IOrderApi
val allOrders: unit -> EffSeq<#IOrderApiProvider,Order,ApiError>
val effSeq: EffSeqBuilder
<summary> The computation expression for writing effect sequences, <see cref="T:Orsak.EffSeq`3" />. Like <c>eff</c>, it can bind effects, tasks and results; in addition, <c>yield</c> produces an item, and <c>yield!</c> the items of another sequence. </summary>
<example><code lang="fsharp"> let orderLines (orderIds: int list) = effSeq { for id in orderIds do let! order = Orders.load id yield! order.Lines } </code></example>
val mutable page: int option
property Option.IsSome: bool with get
val result: OrderPage
property Option.Value: int with get
OrderPage.Orders: Order list
OrderPage.NextPage: int option
val totalOfAllOrders: unit -> Effect<#IOrderApiProvider,decimal,ApiError>
val eff: EffBuilder
<summary> The computation expression for writing effects. Binding (<c>let!</c>, <c>do!</c>, <c>return!</c>) works with other effects, and with <c>Task</c>, <c>ValueTask</c>, <c>Async</c> and <c>Result</c>, so .NET APIs can be used directly. It supports <c>for</c>, <c>while</c>, <c>use</c>, <c>try/with</c> and <c>try/finally</c>, and <c>and!</c> runs effects concurrently. A failed effect stops the rest of the expression, with its error. </summary>
<example><code lang="fsharp"> let placeOrder (order: Order) = eff { let! id = GuidGenerator.genGuid () let! now = Time.utcNow () do! Orders.save { order with Id = id; Placed = now } let! customer = Customers.load order.CustomerId and! stock = Stock.reserve order.Lines return id } </code></example>
val mutable total: decimal
val order: Order
Order.Id: int
Order.Total: decimal
val run: effect: Effect<Env,'a,ApiError> -> api: IOrderApi -> unit
val effect: Effect<Env,'a,ApiError>
'a
val result: AsyncResult<'a,ApiError>
val run: env: 'r -> e: Effect<'r,'a,'e> -> AsyncResult<'a,'e>
<summary> Starts the effect. </summary>
<typeparam name="'r"> The environment required to run the effect </typeparam>
<typeparam name="'a"> The resulting type when the effect runs successfully </typeparam>
<typeparam name="'e"> The resulting type when the effect fails</typeparam>
<param name="env">The environment needed to start the effect</param>
<param name="e">The effect to run</param>
<example><code lang="fsharp"> task { match! placeOrder order |&gt; Effect.run env with | Ok id -&gt; printfn $"Placed {id}" | Error err -&gt; printfn $"Failed: {err}" } </code></example>
ValueTask.AsTask() : Task<Result<'a,ApiError>>
val firstOrders: count: int -> api: IOrderApi -> Task<unit>
val count: int
val enumerator: System.Collections.Generic.IAsyncEnumerator<Result<Order,ApiError>>
val mutable remaining: int
val hasNext: bool
System.Collections.Generic.IAsyncEnumerator.MoveNextAsync() : ValueTask<bool>
property System.Collections.Generic.IAsyncEnumerator.Current: Result<Order,ApiError> with get
<summary>Gets the element in the collection at the current position of the enumerator.</summary>
<returns>The element in the collection at the current position of the enumerator.</returns>
val error: ApiError
System.IAsyncDisposable.DisposeAsync() : ValueTask
val largeOrders: unit -> EffSeq<#IOrderApiProvider,Order,ApiError>
val printLargeOrders: unit -> Effect<#IOrderApiProvider,unit,ApiError>

Type something to start searching.