Repository F# setup
open System
open System.IO
open System.Threading
open System.Threading.Tasks
open Axial
open Axial.Layers
open Axial.Console
open Axial.FileSystem
open Axial.Hosting
open Axial.Hosting.Browser
open Axial.Hosting.Node
open Axial.PlatformService
open Axial.State
open Axial.Telemetry
open Axial.Telemetry.JavaScriptCache and Memoize
When several callers ask for the same expensive value at once, each one starting its own load wastes work and can
overload the source. Cache and Flow.memoize run the load once and let every caller wait for that single result.
This is often called single-flight.
Memoize one computation
Flow.memoize turns a flow into a shared version of itself:
Shared setup
// Setup for the checked examples on this page.
open System
open System.IO
open System.Threading
open System.Threading.Tasks
open Axial
open Axial.Layers
open Axial.Console
open Axial.FileSystem
open Axial.Hosting
open Axial.Hosting.Browser
open Axial.Hosting.Node
open Axial.PlatformService
open Axial.State
open Axial.Telemetry
open Axial.Telemetry.JavaScript
/// Fails the docs test when an example's result differs from the value shown.
let shouldEqual expected actual =
if actual <> expected then failwithf "Expected %A but got %A" expected actual
SystemIOThreadingTasksAxialLayersConsoleFileSystemHostingBrowserNodePlatformServiceStateTelemetryJavaScriptshouldEqual: 'a -> 'a -> unitexpected: 'aactual: 'a(<>): 'T -> 'T -> boolStructural inequality The first parameter. The second parameter. The result of the comparison. 5 <> 5 // Evaluates to false 5 <> 6 // Evaluates to true [1; 2] <> [1; 2] // Evaluates to false
failwithf: Printf.StringFormat<'T,'Result> -> 'TPrint to a string buffer and raise an exception with the given result. Helper printers must return strings. The formatter. The formatted result. See Printf.failwithf (link: ) for examples.
let reads = ref 0
let readConfigFromDisk : Flow<ClockEnvironment, string, string> =
Flow.delay (fun () -> Flow.ok $"config read {Interlocked.Increment &reads.contents} time(s)")
let sameConfig : Flow<ClockEnvironment, string, bool> =
flow {
let! loadConfig = Flow.memoize readConfigFromDisk
let! a = loadConfig
let! b = loadConfig // the same value; the file is read once
return a = b
}
reads: int refref: 'T -> 'T refCreate a mutable reference cell The value to contain in the cell. The created reference cell. let count = ref 0 // Creates a reference cell object with a mutable Value property count.Value // Evaluates to 0 count.Value <- 1 // Updates the value count.Value // Evaluates to 1
readConfigFromDisk: Flow<ClockEnvironment,string,string>Axial.Flow`3Represents a cold workflow that reads an environment, returns a typed result, and is executed explicitly through one of its execution members such as ToTask, ToAsync, or RunSynchronously. The type of the environment dependency. The type of the failure value. The type of the success value.
Axial.ClockEnvironmentAn environment containing only a clock, for timed flows with no other services.
stringAn abbreviation for the CLI type . Basic Types
Axial.Flowdelay: (unit -> Flow<'env,'error,'value>) -> Flow<'env,'error,'value>Defers flow construction until execution time. A function that returns the flow to execute. A flow that lazily evaluates the factory when executed. let flow = Flow.delay (fun () -> Flow.succeed 42)
ok: 'value -> Flow<'env,'error,'value>Creates a successful synchronous flow. The value to wrap in a successful flow. A flow that always succeeds with the provided value.
sameConfig: Flow<ClockEnvironment,string,bool>boolAn abbreviation for the CLI type . Basic Types
flow: FlowBuilderThe universal flow { } computation expression.
loadConfig: Flow<ClockEnvironment,string,string>memoize: Flow<'env,'error,'value> -> Flow<'env,'none,Flow<'caller,'error,'value>>Returns a flow that runs at most once at a time and remembers its value. The first caller of the returned flow starts the computation; callers that arrive while it runs wait for the same result instead of starting another. A success is remembered for every later caller. A failure is not: the next caller starts a fresh attempt. The computation runs in the scope and environment where memoize ran, not in any caller's, so interrupting one caller never cancels the computation the others are waiting for. Closing that scope interrupts it. The computation to share. A flow that produces the memoized flow. let readConfig : Flow<unit, string, string> = Flow.delay (fun () -> Flow.ok "config") let sameConfig : Flow<unit, string, bool> = flow { let! loadConfig = Flow.memoize readConfig let! a = loadConfig let! b = loadConfig // readConfig ran once return a = b }
a: stringb: string(=): 'T -> 'T -> boolStructural equality The first parameter. The second parameter. The result of the comparison. 5 = 5 // Evaluates to true 5 = 6 // Evaluates to false [1; 2] = [1; 2] // Evaluates to true (1, 5) = (1, 6) // Evaluates to false
sameConfig |> Flow.run (ClockEnvironment Clock.live) |> shouldEqual (Exit.Success true)
reads.Value |> shouldEqual 1
sameConfig: Flow<ClockEnvironment,string,bool>(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Axial.Flowrun: 'env -> Flow<'env,'error,'value> -> Exit<'value,'error>Runs the workflow and blocks until the final exit is available. The environment used by the workflow. The workflow to run. The final workflow exit. let exit = workflow |> Flow.run environment
``.ctor``: IClock -> ClockEnvironmentAxial.PlatformService.ClockHelpers for the clock service.
live: IClockCreates a live clock backed by and a monotonic timer.
shouldEqual: 'a -> 'a -> unitAxial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
SuccessThe workflow completed successfully.
reads: int refValue: intThe current value of the reference cell
Cache by key
Cache.make takes a lookup function and returns a cache. Cache.get returns the value for a key, running the lookup
only when no success is cached and no lookup for that key is already running:
let lookups = ref 0
let loadUser (id: int) : Flow<ClockEnvironment, string, string> =
Flow.sleep (TimeSpan.FromMilliseconds 5.0)
|> Flow.map (fun () ->
Interlocked.Increment &lookups.contents |> ignore
$"user {id}")
let loadPages (userIds: int list) : Flow<ClockEnvironment, string, string list> =
flow {
let! users = Cache.make loadUser
let! pages = userIds |> Flow.traversePar (Parallelism.bounded 8) (fun id -> users |> Cache.get id)
return pages
}
lookups: int refref: 'T -> 'T refCreate a mutable reference cell The value to contain in the cell. The created reference cell. let count = ref 0 // Creates a reference cell object with a mutable Value property count.Value // Evaluates to 0 count.Value <- 1 // Updates the value count.Value // Evaluates to 1
loadUser: int -> Flow<ClockEnvironment,string,string>id: intintAn abbreviation for the CLI type . Basic Types
Axial.Flow`3Represents a cold workflow that reads an environment, returns a typed result, and is executed explicitly through one of its execution members such as ToTask, ToAsync, or RunSynchronously. The type of the environment dependency. The type of the failure value. The type of the success value.
Axial.ClockEnvironmentAn environment containing only a clock, for timed flows with no other services.
stringAn abbreviation for the CLI type . Basic Types
Axial.Flowsleep: TimeSpan -> Flow<'env,'error,unit>Suspends the flow for the specified duration, observing cancellation. The duration to sleep. A flow that completes after the specified delay, or is interrupted if cancelled first.
System.TimeSpanRepresents a time interval.
FromMilliseconds: float -> TimeSpanReturns a that represents a specified number of milliseconds. A number of milliseconds. An object that represents . is less than or greater than . -or- is . -or- is . is equal to .
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
map: ('value -> 'next) -> Flow<'env,'error,'value> -> Flow<'env,'error,'next>Transforms the successful value of a flow. If the source fails, the is not executed. The original failure cause is preserved, including typed failures, interruption, and defects. Use map for pure value transformations after an effect has succeeded. A function of type 'value -> 'next to transform the successful value. The source flow of type to transform. A new with the transformed success value of type 'next. let flow = Flow.succeed 1 |> Flow.map (fun x -> x + 1)
System.Threading.InterlockedProvides atomic operations for variables that are shared by multiple threads.
Increment: byref<int> -> intIncrements a specified variable and stores the result, as an atomic operation. The variable whose value is to be incremented. The incremented value. The address of is a null pointer.
(~&): 'T -> byref<'T>Address-of. Uses of this value may result in the generation of unverifiable code. The input object. The managed pointer.
contents: 'TThe current value of the reference cell
ignore: 'T -> unitIgnore the passed value. This is often used to throw away results of a computation. The value to ignore. ignore 55555 // Evaluates to ()
loadPages: int list -> Flow<ClockEnvironment,string,string list>userIds: int listlistThe type of immutable singly-linked lists. See the module for further operations related to lists. Use the constructors [] and :: (infix) to create values of this type, or the notation [1; 2; 3]. Use the values in the List module to manipulate values of this type, or pattern match against the values directly. See also F# Language Guide - Lists.
flow: FlowBuilderThe universal flow { } computation expression.
users: Cache<int,string,string>Axial.CacheModuleFunctions for creating and reading a single-flight .
make: ('key -> Flow<'env,'error,'value>) -> Flow<'env,'none,Cache<'key,'error,'value>>Creates a cache that computes missing values with . The cache captures the environment and scope of the flow that makes it. Lookups run there as detached fibers, and closing that scope interrupts any lookup still running. Make the cache where it should live, typically in a service's layer. Computes the value for a key. flow { let! users = Cache.make loadUser let! first = users |> Cache.get 42 let! again = users |> Cache.get 42 // no second load return first = again }
pages: string listtraversePar: Parallelism -> ('value -> Flow<'env,'error,'next>) -> 'value seq -> Flow<'env,'error,'next list>Maps values to flows and runs them with bounded concurrency, returning results in input order. At most mappings run at once; as each finishes, its worker starts the next value. The first failure interrupts the mappings still running and waits for their cleanup before the flow fails, so no sibling is left running in the background. Size CPU-bound work with Parallelism.ofProcessors. When each worker needs its own connection or handle, use traverseParUsing. The maximum number of mappings running at once. Maps each value to a flow. The values to map. A flow containing the mapped values in the order of . let! pages = urls |> Flow.traversePar (Parallelism.bounded 8) fetchPage
Axial.ParallelismModuleCreates bounds for parallel Flow and stream operators.
bounded: int -> ParallelismCreates a positive concurrency bound. Thrown when is not positive.
get: 'key -> Cache<'key,'error,'value> -> Flow<'env,'error,'value>Returns the value for a key, running the lookup if no success is cached and none is running. Interrupting this flow ends this caller's wait; the lookup keeps running for other callers. The key to read. The cache.
loadPages [ 1; 2; 1; 1; 2 ] |> Flow.run (ClockEnvironment Clock.live) |> shouldEqual (Exit.Success [ "user 1"; "user 2"; "user 1"; "user 1"; "user 2" ])
lookups.Value |> shouldEqual 2
loadPages: int list -> Flow<ClockEnvironment,string,string list>(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Axial.Flowrun: 'env -> Flow<'env,'error,'value> -> Exit<'value,'error>Runs the workflow and blocks until the final exit is available. The environment used by the workflow. The workflow to run. The final workflow exit. let exit = workflow |> Flow.run environment
``.ctor``: IClock -> ClockEnvironmentAxial.PlatformService.ClockHelpers for the clock service.
live: IClockCreates a live clock backed by and a monotonic timer.
shouldEqual: 'a -> 'a -> unitAxial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
SuccessThe workflow completed successfully.
lookups: int refValue: intThe current value of the reference cell
Five concurrent requests for two users ran the lookup twice.
Cache.invalidate forgets one key and Cache.invalidateAll forgets every key, so the next get looks up again.
Cache.count reports how many keys are cached or loading.
Rules
- Successes are kept; failures are not. A failed lookup is removed when it settles, so the next caller starts a fresh attempt instead of receiving a cached error.
- Interrupting a caller stops only its wait. Lookups run as detached fibers in the scope and environment where the cache (or memoized flow) was made, not in any caller's. A caller that is interrupted, for example because a user navigated away, never cancels a lookup that other callers are still waiting for.
- The owner's scope bounds the lookups. Closing the scope where the cache was made interrupts any lookup still running. Make the cache where it should live, typically in a service's layer.
- Values are kept until invalidated. There is no expiry; invalidate keys when their source changes.
The cache torture test interrupts callers, fails lookups, and invalidates keys while hundreds of callers share one cache, and checks each rule above.

