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())
|
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()
|
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())
|
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))
|
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.
val int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int
val decimal: value: 'T -> decimal (requires member op_Explicit)
--------------------
type decimal = System.Decimal
--------------------
type decimal<'Measure> = decimal
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>
module Result from Microsoft.FSharp.Core
--------------------
type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
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 -> string abstract WriteLine: string -> unit type IConsoleProvider = abstract Console: IConsole module Console = let readLine () = Effect.Create(fun (p: #IConsoleProvider) -> p.Console.ReadLine()) let writeLine line = Effect.Create(fun (p: #IConsoleProvider) -> 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 () |> 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 -> 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>
module OrderApi from Effect-sequences
--------------------
type OrderApi = interface IOrderApi new: ?failOnPage: int -> OrderApi
--------------------
new: ?failOnPage: int -> OrderApi
type Env = interface IOrderApiProvider new: api: IOrderApi -> Env
--------------------
new: api: IOrderApi -> Env
<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>
<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>
<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 |> Effect.run env with | Ok id -> printfn $"Placed {id}" | Error err -> printfn $"Failed: {err}" } </code></example>
<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>
Orsak