Testing
An effect doesn't do anything until it is run, and everything it needs comes from the environment it is run with. Testing an effect is therefore a matter of running it with an environment of fakes, and checking its result. There is no container to configure and nothing to mock by reflection: a test environment is an ordinary type, or an object expression, implementing the provider interfaces the effect asks for.
This page tests a small feature that uses Orsak's built-in effects for ids, randomness and time. It is a script: it
runs with dotnet fsi docs/testing.fsx once Orsak is built, and the output shown is from such a run. The tests are
plain functions here; in a test project, they would be the bodies of tests in the framework of your choice.
The feature
A shop gives its customers discount vouchers, each with a unique code, a random discount of 5 to 15 percent, and an expiry 30 days out. A customer can hold at most three. The vouchers are kept in a store, described in the usual way:
open System
open System.Collections.Generic
open System.Threading.Tasks
open Microsoft.Extensions.Time.Testing
open Orsak
type Voucher = {
Code: Guid
Customer: string
Percent: int
Expires: DateTimeOffset
}
type VoucherError =
| TooManyVouchers
| StoreUnavailable
type IVoucherStore =
abstract member CountFor: customer: string -> Task<int>
abstract member Save: Voucher -> Task<Result<unit, VoucherError>>
type IVoucherStoreProvider =
abstract member Vouchers: IVoucherStore
module Vouchers =
let countFor customer =
Effect.Create(fun (p: #IVoucherStoreProvider) -> p.Vouchers.CountFor customer)
let save voucher =
Effect.Create(fun (p: #IVoucherStoreProvider) -> p.Vouchers.Save voucher)
Issuing a voucher uses the store, and three of Orsak's built-in effects: GuidGenerator.genGuid, Random.next and
Time.utcNow (.NET 8 and later):
let issueVoucher customer = eff {
let! existing = Vouchers.countFor customer
if existing >= 3 then
return! Error TooManyVouchers
else
let! code = GuidGenerator.genGuid ()
let! percent = Random.next 5 16
let! now = Time.utcNow ()
let voucher = {
Code = code
Customer = customer
Percent = percent
Expires = now.AddDays 30.
}
do! Vouchers.save voucher
return voucher
}
let isValid voucher = eff {
let! now = Time.utcNow ()
return now < voucher.Expires
}
The type of issueVoucher is inferred as
string -> Effect<#IVoucherStoreProvider & #IGuidGenProvider & #IRandomProvider & #ITimeProvider, Voucher, VoucherError>
which is a complete list of what a test has to provide, checked by the compiler: an environment that misses one of
them is a compile error, not a failure at runtime. isValid needs only a clock.
A test environment
The fakes make every source of variation fixed. The store keeps its vouchers in a list, so tests can look at what
was saved. The id generator hands out known ids, the random numbers are always the lowest possible, and the clock is
a FakeTimeProvider, from the Microsoft.Extensions.TimeProvider.Testing package, which only moves when told to:
type InMemoryStore() =
let saved = List<Voucher>()
member _.Saved = List.ofSeq saved
interface IVoucherStore with
member _.CountFor customer =
Task.FromResult(saved |> Seq.filter (fun v -> v.Customer = customer) |> Seq.length)
member _.Save voucher =
saved.Add voucher
Task.FromResult(Ok())
type SequentialGuids() =
let mutable next = 0
interface IGuidGenerator with
member _.GenGuid() =
next <- next + 1
Guid($"00000000-0000-0000-0000-%012d{next}")
type LowestRandom() =
interface IRandomGenerator with
member _.Next((minValue, _)) = minValue
member _.NextDouble() = 0.0
type TestEnv(store: IVoucherStore) =
let guids = SequentialGuids()
let random = LowestRandom()
let clock = FakeTimeProvider(DateTimeOffset(2026, 1, 1, 12, 0, 0, TimeSpan.Zero))
member _.Clock = clock
interface IVoucherStoreProvider with
member _.Vouchers = store
interface IGuidGenProvider with
member _.GuidGenerator = guids
interface IRandomProvider with
member _.Effect = random
interface ITimeProvider with
member _.Clock = clock
Each test creates its own environment, so tests don't share state and can run in parallel.
Testing the result
Effect.run returns the result as a value, to be matched on. The tests use a small helper to fail with a message:
let expect condition message =
if not condition then
failwith message
let ``issues a voucher expiring in 30 days`` () = task {
let store = InMemoryStore()
let! result = issueVoucher "ada" |> Effect.run (TestEnv store)
match result with
| Ok voucher ->
expect (voucher.Code = Guid "00000000-0000-0000-0000-000000000001") "the first id"
expect (voucher.Percent = 5) "the lowest discount"
expect (voucher.Expires = DateTimeOffset(2026, 1, 31, 12, 0, 0, TimeSpan.Zero)) "30 days out"
expect (store.Saved = [ voucher ]) "the voucher is saved"
| Error e -> failwith $"failed with {e}"
}
When a test only cares about success, Effect.runOrFail is shorter: it returns the value, and throws when the effect
fails, which fails the test with the error in its message:
let ``a fourth voucher is refused`` () = task {
let env = TestEnv(InMemoryStore())
for _ in 1..3 do
let! _ = issueVoucher "ada" |> Effect.runOrFail env
()
let! fourth = issueVoucher "ada" |> Effect.run env
expect (fourth = Error TooManyVouchers) "the fourth is refused"
}
Testing failures
Failures of dependencies are faked like anything else. Here the store is down, and an object expression is enough to say so:
let ``a store failure is the result`` () = task {
let downStore =
{ new IVoucherStore with
member _.CountFor _ = Task.FromResult 0
member _.Save _ = Task.FromResult(Error StoreUnavailable)
}
let! result = issueVoucher "ada" |> Effect.run (TestEnv downStore)
expect (result = Error StoreUnavailable) "the store's error"
}
Testing time
Since the clock is part of the environment, a test can move it. A voucher is valid until it expires:
let ``a voucher expires after 30 days`` () = task {
let env = TestEnv(InMemoryStore())
let! voucher = issueVoucher "ada" |> Effect.runOrFail env
env.Clock.Advance(TimeSpan.FromDays 29.)
let! validAfter29 = isValid voucher |> Effect.runOrFail env
env.Clock.Advance(TimeSpan.FromDays 2.)
let! validAfter31 = isValid voucher |> Effect.runOrFail env
expect validAfter29 "valid after 29 days"
expect (not validAfter31) "expired after 31 days"
}
isValid only needs a clock, so it can also be tested with an environment that only has one:
let ``a voucher is valid before it expires`` () = task {
let clockOnly =
{ new ITimeProvider with
member _.Clock = FakeTimeProvider(DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero))
}
let voucher = {
Code = Guid.Empty
Customer = "ada"
Percent = 5
Expires = DateTimeOffset(2026, 2, 1, 0, 0, 0, TimeSpan.Zero)
}
let! valid = isValid voucher |> Effect.runOrFail clockOnly
expect valid "valid in January"
}
Running them all:
for name, test in
[ nameof ``issues a voucher expiring in 30 days``, ``issues a voucher expiring in 30 days``
nameof ``a fourth voucher is refused``, ``a fourth voucher is refused``
nameof ``a store failure is the result``, ``a store failure is the result``
nameof ``a voucher expires after 30 days``, ``a voucher expires after 30 days``
nameof ``a voucher is valid before it expires``, ``a voucher is valid before it expires`` ] do
test().Wait()
printfn $"passed: {name}"
|
Less boilerplate
A test environment type per test module is short, but it adds up. With Orsak.Myriad,
[<GenEnvironment>] on an interface inheriting the providers generates a create function taking an anonymous
record of effects, so a test can build its environment in place, and run and runOrFail functions that create it
and run an effect in one go.
type Guid = new: b: byte array -> unit + 6 overloads member CompareTo: value: Guid -> int + 1 overload member Equals: g: Guid -> bool + 1 overload member GetHashCode: unit -> int member ToByteArray: unit -> byte array + 1 overload member ToString: unit -> string + 2 overloads member TryFormat: utf8Destination: Span<byte> * bytesWritten: byref<int> * ?format: ReadOnlySpan<char> -> bool + 1 overload member TryWriteBytes: destination: Span<byte> -> bool + 1 overload static member (<) : left: Guid * right: Guid -> bool static member (<=) : left: Guid * right: Guid -> bool ...
<summary>Represents a globally unique identifier (GUID).</summary>
--------------------
Guid ()
Guid(b: byte array) : Guid
Guid(b: ReadOnlySpan<byte>) : Guid
Guid(g: string) : Guid
Guid(b: ReadOnlySpan<byte>, bigEndian: bool) : Guid
Guid(a: int, b: int16, c: int16, d: byte array) : Guid
Guid(a: int, b: int16, c: int16, d: byte, e: byte, f: byte, g: byte, h: byte, i: byte, j: byte, k: byte) : Guid
Guid(a: uint32, b: uint16, c: uint16, d: byte, e: byte, f: byte, g: byte, h: byte, i: byte, j: byte, k: byte) : Guid
val string: value: 'T -> string
--------------------
type string = String
val int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int
type DateTimeOffset = new: date: DateOnly * time: TimeOnly * offset: TimeSpan -> unit + 8 overloads member Add: timeSpan: TimeSpan -> DateTimeOffset member AddDays: days: float -> DateTimeOffset member AddHours: hours: float -> DateTimeOffset member AddMicroseconds: microseconds: float -> DateTimeOffset member AddMilliseconds: milliseconds: float -> DateTimeOffset member AddMinutes: minutes: float -> DateTimeOffset member AddMonths: months: int -> DateTimeOffset member AddSeconds: seconds: float -> DateTimeOffset member AddTicks: ticks: int64 -> DateTimeOffset ...
<summary>Represents a point in time, typically expressed as a date and time of day, relative to Coordinated Universal Time (UTC).</summary>
--------------------
DateTimeOffset ()
DateTimeOffset(dateTime: DateTime) : DateTimeOffset
DateTimeOffset(dateTime: DateTime, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(ticks: int64, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(date: DateOnly, time: TimeOnly, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(year: int, month: int, day: int, hour: int, minute: int, second: int, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, calendar: Globalization.Calendar, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, microsecond: int, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, microsecond: int, calendar: Globalization.Calendar, offset: TimeSpan) : DateTimeOffset
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: Action) : Task
Task(action: Action, cancellationToken: Threading.CancellationToken) : Task
Task(action: Action, creationOptions: TaskCreationOptions) : Task
Task(action: Action<obj>, state: obj) : Task
Task(action: Action, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : Task
Task(action: Action<obj>, state: obj, cancellationToken: Threading.CancellationToken) : Task
Task(action: Action<obj>, state: obj, creationOptions: TaskCreationOptions) : Task
Task(action: Action<obj>, state: obj, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : Task
--------------------
Task(``function`` : Func<'TResult>) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj) : Task<'TResult>
Task(``function`` : Func<'TResult>, cancellationToken: Threading.CancellationToken) : Task<'TResult>
Task(``function`` : Func<'TResult>, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj, cancellationToken: Threading.CancellationToken) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : Func<'TResult>, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj, cancellationToken: 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>
<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> Effects for generating <see cref="T:System.Guid" />s through an <see cref="T:Orsak.IGuidGenProvider" />. </summary>
<summary> An effect that generates a new <see cref="T:System.Guid" /> with the environment's generator. </summary>
<example><code lang="fsharp"> let createOrder () = eff { let! id = GuidGenerator.genGuid () return { Id = id; Lines = [] } } </code></example>
type Random = new: unit -> unit + 1 overload member GetHexString: stringLength: int * ?lowercase: bool -> string + 1 overload member GetItems<'T> : choices: ReadOnlySpan<'T> * length: int -> 'T array + 2 overloads member GetString: choices: ReadOnlySpan<char> * length: int -> string member Next: unit -> int + 2 overloads member NextBytes: buffer: byte array -> unit + 1 overload member NextDouble: unit -> float member NextInt64: unit -> int64 + 2 overloads member NextSingle: unit -> float32 member Shuffle<'T> : values: Span<'T> -> unit + 1 overload ...
<summary>Represents a pseudo-random number generator, which is an algorithm that produces a sequence of numbers that meet certain statistical requirements for randomness.</summary>
--------------------
Random() : Random
Random(Seed: int) : Random
<summary> An effect that generates a random integer from <paramref name="min" />, inclusive, to <paramref name="max" />, exclusive. </summary>
<param name="min">The inclusive lower bound.</param>
<param name="max">The exclusive upper bound.</param>
<example><code lang="fsharp"> let rollDie () = Random.next 1 7 </code></example>
<summary> Effects for reading the time through an <see cref="T:Orsak.ITimeProvider" />. </summary>
<summary> An effect that reads the current UTC time from the environment's clock. </summary>
<example><code lang="fsharp"> let isExpired (expires: DateTimeOffset) = eff { let! now = Time.utcNow () return now > expires } </code></example>
val string: value: 'T -> string
--------------------
type string = System.String
type InMemoryStore = interface IVoucherStore new: unit -> InMemoryStore member Saved: Voucher list
--------------------
new: unit -> InMemoryStore
type List<'T> = interface ICollection<'T> interface seq<'T> interface IEnumerable interface IList<'T> interface IReadOnlyCollection<'T> interface IReadOnlyList<'T> interface ICollection interface IList new: unit -> unit + 2 overloads member Add: item: 'T -> unit ...
<summary>Represents a strongly typed list of objects that can be accessed by index. Provides methods to search, sort, and manipulate lists.</summary>
<typeparam name="T">The type of elements in the list.</typeparam>
--------------------
List() : List<'T>
List(collection: 'T seq) : List<'T>
List(capacity: int) : List<'T>
type SequentialGuids = interface IGuidGenerator new: unit -> SequentialGuids
--------------------
new: unit -> SequentialGuids
<summary> Generates <see cref="T:System.Guid" />s. An effect interface, so that code creating ids can be run with a deterministic generator in tests. </summary>
type LowestRandom = interface IRandomGenerator new: unit -> LowestRandom
--------------------
new: unit -> LowestRandom
<summary>An alias of <see cref="T:Orsak.IRandom" />.</summary>
type TestEnv = interface ITimeProvider interface IRandomProvider interface IGuidGenProvider interface IVoucherStoreProvider new: store: IVoucherStore -> TestEnv member Clock: FakeTimeProvider
--------------------
new: store: IVoucherStore -> TestEnv
type FakeTimeProvider = inherit TimeProvider new: unit -> unit + 1 overload member AdjustTime: value: DateTimeOffset -> unit member Advance: delta: TimeSpan -> unit member CreateTimer: callback: TimerCallback * state: obj * dueTime: TimeSpan * period: TimeSpan -> ITimer member GetTimestamp: unit -> int64 member GetUtcNow: unit -> DateTimeOffset member SetLocalTimeZone: localTimeZone: TimeZoneInfo -> unit member SetUtcNow: value: DateTimeOffset -> unit member ToString: unit -> string ...
<summary> Represents a synthetic time provider that can be used to enable deterministic behavior in tests. </summary>
--------------------
FakeTimeProvider() : FakeTimeProvider
FakeTimeProvider(startDateTime: DateTimeOffset) : FakeTimeProvider
type TimeSpan = new: hours: int * minutes: int * seconds: int -> unit + 4 overloads member Add: ts: TimeSpan -> TimeSpan member CompareTo: value: obj -> int + 1 overload member Divide: divisor: float -> TimeSpan + 1 overload member Duration: unit -> TimeSpan member Equals: value: obj -> bool + 2 overloads member GetHashCode: unit -> int member Multiply: factor: float -> TimeSpan member Negate: unit -> TimeSpan member Subtract: ts: TimeSpan -> TimeSpan ...
<summary>Represents a time interval.</summary>
--------------------
TimeSpan ()
TimeSpan(ticks: int64) : TimeSpan
TimeSpan(hours: int, minutes: int, seconds: int) : TimeSpan
TimeSpan(days: int, hours: int, minutes: int, seconds: int) : TimeSpan
TimeSpan(days: int, hours: int, minutes: int, seconds: int, milliseconds: int) : TimeSpan
TimeSpan(days: int, hours: int, minutes: int, seconds: int, milliseconds: int, microseconds: int) : TimeSpan
<summary>Represents the zero <see cref="T:System.TimeSpan" /> value. This field is read-only.</summary>
<summary> Provides an <see cref="T:Orsak.IGuidGenerator" /> to effects, as part of an environment. </summary>
<summary>An alias of <see cref="T:Orsak.IRandomGeneratorProvider" />.</summary>
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>
<summary> Provides a <see cref="T:System.TimeProvider" /> to effects, as part of an environment. Using <c>TimeProvider.System</c> in production and a fake time provider in tests makes time-dependent effects testable. </summary>
<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> Starts the effect, and raises an exception if it fails, with the error's string representation as its message. Prefer <c>Effect.run</c>, which returns the error as a value; this is meant for places where an exception is the only option, such as tests or scripts. </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>
TimeSpan.FromDays(value: float) : TimeSpan
TimeSpan.FromDays(days: int, ?hours: int, ?minutes: int64, ?seconds: int64, ?milliseconds: int64, ?microseconds: int64) : TimeSpan
Orsak