Basic usage
This is a library intended to make effectful code easier to manage. It allows the user to easily create and consume side-effects modelled as interfaces, by making use of computation expressions.
Imagine having the interface
type IButtonPusher =
abstract member PushButton: unit -> Task<unit>
describing some business-critical capability. To integrate that with the effect framework, we create the following helpers:
type IButtonPusherProvider =
abstract member ButtonPusher: IButtonPusher
open Orsak
module ButtonPusher =
let pushButton () =
Effect.Create(fun (provider: #IButtonPusherProvider) -> provider.ButtonPusher.PushButton())
Then, we can write code like
let work () : Effect<#IButtonPusherProvider, unit, _> = eff {
//code
do! ButtonPusher.pushButton ()
//more code
}
Which can be run with some type which implements IButtonPusherProvider.
The big win with effects is that they compose. By using a flexible constraint in the effect creation function, we can
simply add effects, which will constrain the effect runner type to implement both IButtonPusherProvider and ILogProvider
type ILogProvider =
abstract member Logger: ILogger
let logInformation (message: string) =
Effect.Create(fun (provider: #ILogProvider) -> provider.Logger.LogInformation(message))
let workThenLog () = eff {
do! work ()
do! logInformation "Did the work."
}
Then, in the composition root of the program, one could define:
type EffectRunner(buttonPusher: IButtonPusher) =
interface ILogProvider with
member _.Logger = NullLogger.Instance
interface IButtonPusherProvider with
member _.ButtonPusher = buttonPusher
And then run the effects:
let runner =
EffectRunner(
{ new IButtonPusher with
member this.PushButton() = Task.FromResult()
}
)
let result: ValueTask<Result<unit, string>> = workThenLog () |> Effect.run runner
Running an effect returns a ValueTask of a Result, whose error type is left open by work and chosen here.
Nothing happens until then, so the same effect can be run again, with the same runner or with another one, such as
one with fakes in a test.
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>
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>
<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>
type Logger<'T> = interface ILogger<'T> interface ILogger new: factory: ILoggerFactory -> unit
<summary> Delegates to a new <see cref="T:Microsoft.Extensions.Logging.ILogger" /> instance using the full name of the given type, created by the provided <see cref="T:Microsoft.Extensions.Logging.ILoggerFactory" />. </summary>
<typeparam name="T">The type.</typeparam>
--------------------
Logger(factory: ILoggerFactory) : Logger<'T>
type ILogger = override BeginScope<'TState> : state: 'TState -> IDisposable override IsEnabled: logLevel: LogLevel -> bool override Log<'TState> : logLevel: LogLevel * eventId: EventId * state: 'TState * ``exception`` : exn * formatter: Func<'TState,exn,string> -> unit
<summary> Represents a type used to perform logging. </summary>
<remarks>Aggregates most logging patterns to a single method.</remarks>
--------------------
type ILogger<'TCategoryName> = inherit ILogger
<summary> A generic interface for logging where the category name is derived from the specified <typeparamref name="TCategoryName" /> type name. Generally used to enable activation of a named <see cref="T:Microsoft.Extensions.Logging.ILogger" /> from dependency injection. </summary>
<typeparam name="TCategoryName">The type whose name is used for the logger category name.</typeparam>
val string: value: 'T -> string
--------------------
type string = System.String
(extension) ILogger.LogInformation(eventId: EventId, message: string, args: obj array) : unit
(extension) ILogger.LogInformation(``exception`` : exn, message: string, args: obj array) : unit
(extension) ILogger.LogInformation(eventId: EventId, ``exception`` : exn, message: string, args: obj array) : unit
type EffectRunner = interface IButtonPusherProvider interface ILogProvider new: buttonPusher: IButtonPusher -> EffectRunner
--------------------
new: buttonPusher: IButtonPusher -> EffectRunner
type NullLogger = interface ILogger member BeginScope<'TState> : state: 'TState -> IDisposable member IsEnabled: logLevel: LogLevel -> bool member Log<'TState> : logLevel: LogLevel * eventId: EventId * state: 'TState * ``exception`` : exn * formatter: Func<'TState,exn,string> -> unit static member Instance: NullLogger
<summary> Minimalistic logger that does nothing. </summary>
--------------------
type NullLogger<'T> = interface ILogger<'T> interface ILogger new: unit -> unit member BeginScope<'TState> : state: 'TState -> IDisposable member IsEnabled: logLevel: LogLevel -> bool member Log<'TState> : logLevel: LogLevel * eventId: EventId * state: 'TState * ``exception`` : exn * formatter: Func<'TState,exn,string> -> unit static val Instance: NullLogger<'T>
<summary> Minimalistic logger that does nothing. </summary>
--------------------
NullLogger() : NullLogger<'T>
field NullLogger<'T>.Instance: NullLogger<'T>
<summary> Returns an instance of <see cref="T:Microsoft.Extensions.Logging.Abstractions.NullLogger`1" />. </summary>
<returns>An instance of <see cref="T:Microsoft.Extensions.Logging.Abstractions.NullLogger`1" />.</returns>
--------------------
property NullLogger.Instance: NullLogger with get
<summary> Returns the shared instance of <see cref="T:Microsoft.Extensions.Logging.Abstractions.NullLogger" />. </summary>
type ValueTask = new: source: IValueTaskSource * token: int16 -> unit + 1 overload member AsTask: unit -> Task member ConfigureAwait: continueOnCapturedContext: bool -> ConfiguredValueTaskAwaitable member Equals: obj: obj -> bool + 1 overload member GetAwaiter: unit -> ValueTaskAwaiter member GetHashCode: unit -> int member Preserve: unit -> ValueTask static member (<>) : left: ValueTask * right: ValueTask -> bool static member (=) : left: ValueTask * right: ValueTask -> bool static member FromCanceled: cancellationToken: CancellationToken -> ValueTask + 1 overload ...
<summary>Provides an awaitable result of an asynchronous operation.</summary>
--------------------
type ValueTask<'TResult> = new: source: IValueTaskSource<'TResult> * token: int16 -> unit + 2 overloads member AsTask: unit -> Task<'TResult> member ConfigureAwait: continueOnCapturedContext: bool -> ConfiguredValueTaskAwaitable<'TResult> member Equals: obj: obj -> bool + 1 overload member GetAwaiter: unit -> ValueTaskAwaiter<'TResult> member GetHashCode: unit -> int member Preserve: unit -> ValueTask<'TResult> member ToString: unit -> string static member (<>) : left: ValueTask<'TResult> * right: ValueTask<'TResult> -> bool static member (=) : left: ValueTask<'TResult> * right: ValueTask<'TResult> -> bool ...
<summary>Provides a value type that wraps a <see cref="T:System.Threading.Tasks.Task`1" /> and a <typeparamref name="TResult" />, only one of which is used.</summary>
<typeparam name="TResult">The result.</typeparam>
--------------------
ValueTask ()
ValueTask(task: Task) : ValueTask
ValueTask(source: Sources.IValueTaskSource, token: int16) : ValueTask
--------------------
ValueTask ()
ValueTask(task: Task<'TResult>) : ValueTask<'TResult>
ValueTask(result: 'TResult) : ValueTask<'TResult>
ValueTask(source: Sources.IValueTaskSource<'TResult>, token: int16) : ValueTask<'TResult>
module Result from Microsoft.FSharp.Core
--------------------
type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
<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>
Orsak