421anon/elm-flow/5.0.0

NOTE: the latest version is 6.0.2

Flow

Write effectful Elm logic as composable steps - no Msg type, no update function.

Event handlers and subscriptions produce Flow values directly:

-- a button click
Html.button
    [ Html.Events.onClick (Flow.modify (\m -> { m | count = m.count + 1 })) ]
    [ Html.text "+" ]


-- an HTTP response
Flow.lift (Http.get { url = "/api/data", expect = Http.expectJson handleResult decoder })
type Flow s a

A description of a computation that can read and write state s and eventually produces a value of type a.

Composing flows

pure : a -> Flow s a

Lift a plain value into Flow without performing any effects.

Flow.pure 42
    |> Flow.map (\n -> n + 1)
-- produces 43, state unchanged
map : (a -> b) -> Flow s a -> Flow s b

Transform the value produced by a Flow without changing state.

Flow.pure 3
    |> Flow.map (\n -> n * 2)
-- produces 6
andThen : (a -> Flow s b) -> Flow s a -> Flow s b

Chain two Flow computations. The second receives the value produced by the first.

Flow.get
    |> Flow.andThen (\m -> Flow.pure m.count)
join : Flow s (Flow s a) -> Flow s a

Flatten a nested Flow s (Flow s a) into Flow s a.

Flow.pure (Flow.pure 42)
    |> Flow.join
-- same as Flow.pure 42
ap : Flow s (a -> b) -> Flow s a -> Flow s b

Apply a function wrapped in Flow to a value wrapped in Flow.

Flow.pure (\n -> n + 1)
    |> Flow.ap (Flow.pure 41)
-- produces 42
flap : Flow s a -> Flow s (a -> b) -> Flow s b

Flipped ap. Apply a value to a function, both wrapped in Flow.

Flow.pure 41
    |> Flow.flap (Flow.pure (\n -> n + 1))
-- produces 42
compose : (b -> Flow m c) -> (a -> Flow m b) -> a -> Flow m c

Compose two Kleisli arrows (functions returning Flow).

handleResponse : Response -> Flow Model ()
handleResponse =
    Flow.compose persistToDisk fetchData
seq : Flow s b -> Flow s a -> Flow s b

Run the piped Flow for its effects, discard its result, then run the argument Flow and return its result.

saveRecord
    |> Flow.seq (Flow.pure "saved!")
-- runs saveRecord, then produces "saved!"
return : a -> Flow s b -> Flow s a

Shorthand for `Flow.seq (Flow.pure a).

saveRecord
    |> Flow.return "saved"
traverse : (a -> Flow s b) -> List a -> Flow s (List b)

Map over a list with a function that returns Flow, collecting the results in order.

Flow.traverse fetchUser userIds
    |> Flow.andThen renderAll
mapM : List (Flow s a) -> Flow s (List a)

Sequence a list of Flow computations, collecting all results.

Equivalent to traverse identity.

Flow.mapM [ step1, step2, step3 ]
    |> Flow.andThen finalize

Control flow

if_ : Bool -> Flow s a -> Flow s a -> Flow s a

Branch on a Bool: run onTrue when the condition holds, onFalse otherwise.

Exactly one branch runs.

Flow.if_ model.isLoggedIn
    loadDashboard
    redirectToLogin
when : Bool -> Flow s () -> Flow s ()

Run a Flow only when the condition is True, otherwise produce ().

Flow.when model.isLoggedIn savePreferences
unless : Bool -> Flow s () -> Flow s ()

Run a Flow only when the condition is False, otherwise produce ().

Flow.unless model.isCached fetchFromServer
bracket_ : Flow s a -> Flow s b -> Flow s c -> Flow s c

Acquire a resource, run a computation, then release the resource.

WARNING: If the computation forks into multiple fibers, the release step will run once per fiber. This also means that if the computation is terminating (which is equivalent to forking into 0 fibers), then the release step will NOT run.

Flow.bracket_
    (Flow.modify (\m -> { m | loading = True }))
    (Flow.modify (\m -> { m | loading = False }))
    fetchData

Terminating a fiber

none : Flow s a

The empty computation. Terminates the current branch without producing a value or changing state.

WARNING: none is a 0-fiber program - it has no active fiber to carry a continuation forward. Chaining it with andThen, seq, etc. produces another 0-fiber program, so those continuations will never run.

Flow.get
    |> Flow.andThen
        (\m ->
            if m.ready then
                doWork


            else
                Flow.none
        )
assertJust : Flow s (Maybe a) -> Flow s a

Continue only if the value is Just, terminate with none otherwise.

fetchUser userId
    |> Flow.assertJust
    |> Flow.andThen renderUser
assertOk : Flow s (Result e a) -> Flow s a

Continue only if the result is Ok, silently terminate on Err.

decodeResponse body
    |> Flow.assertOk
    |> Flow.andThen handleData
assertCondition : (a -> Bool) -> Flow s a -> Flow s a

Continue only if the predicate holds for the produced value, terminate with none otherwise.

Flow.get
    |> Flow.assertCondition (\m -> m.count > 0)
    |> Flow.andThen doSomethingWithPositiveCount
fromMaybe : Maybe a -> (a -> Flow s b) -> Flow s b

Unwrap a Maybe and feed the inner value into a continuation, or terminate with none if the Maybe is Nothing.

Flow.fromMaybe model.selectedId
    (\id -> fetchItem id |> Flow.andThen renderItem)

Reading and writing the model

get : Flow s s

Read the current model.

Flow.get
    |> Flow.andThen (\model -> Flow.pure model.count)
set : s -> Flow s ()

Replace the model entirely.

Flow.set { count = 0 }
modify : (s -> s) -> Flow s ()

Apply a function to transform the model.

Flow.modify (\m -> { m | count = m.count + 1 })

Optics

try : An_Optic pr ls s a -> (Maybe a -> Flow s b) -> Flow s b

Read a value through an optic. Produces Just a if the optic matches, Nothing if it does not (e.g. a Prism pointing at the wrong variant).

Flow.try MyLenses.selectedItem
    (\maybeItem ->
        case maybeItem of
            Just item ->
                renderItem item


            Nothing ->
                showPlaceholder
    )
forAll : An_Optic pr ls s a -> (a -> Flow s b) -> Flow s b

Fork into n concurrent fibers for each matching value through an optic.

-- Only runs if a user is logged in
Flow.forAll MyLenses.currentUser
    (\user -> showWelcomeBanner user.name)
getAll : An_Optic pr ls s a -> (List a -> Flow s b) -> Flow s b

Read all values targeted by an optic (useful for traversals).

Flow.getAll MyLenses.allItems
    (\items -> Flow.pure (List.length items))
over : An_Optic pr ls s a -> (a -> a) -> Flow s ()

Modify the value(s) targeted by an optic.

Flow.over MyLenses.count (\n -> n + 1)
setAll : An_Optic pr ls s a -> a -> Flow s ()

Set the value(s) targeted by an optic.

Flow.setAll MyLenses.status Loading
ifHas : An_Optic pr ls s a -> (a -> Flow s b) -> Flow s b -> Flow s b

Branch on whether an optic matches: run onPresent with the matched value when the optic succeeds, onAbsent otherwise. Exactly one branch runs.

Flow.ifHas
    (currentProject << success)
    openStream
    closeStream
whenHas : An_Optic pr ls s a -> (a -> Flow s ()) -> Flow s ()

Run a Flow only when an optic matches. No match produces Flow.pure ().

Flow.whenHas (currentProject << success) openStream
unlessHas : An_Optic pr ls s a -> Flow s () -> Flow s ()

Run a Flow only when an optic does NOT match. A match produces Flow.pure ().

Flow.unlessHas (currentProject << success) handleMissingProject
setting : An_Optic pr ls s Bool -> Flow s a -> Flow s a

Set a Bool field to True for the duration of a computation, then restore it to False. Useful for loading/busy flags.

Flow.setting MyLenses.isLoading fetchData
locking : A_Lens pr s Bool -> Flow s () -> Flow s ()

Run a computation only if a Bool lens is currently False, and hold it at True for the duration. Subsequent calls while the lock is held are silently dropped.

Flow.locking MyLenses.isSaving saveRecord

Zooming into sub-models

viaLens : A_Lens pr outer inner -> Flow inner a -> Flow outer a

Embed a Flow inner a into a larger model outer through a lens. All state reads and writes inside the sub-flow are transparently routed through the lens.

This is the primary tool for reusable components: write a self-contained Flow against a sub-model type, then embed it anywhere that sub-model appears by passing in the appropriate lens.

-- SelectState is a reusable component with its own Flow logic
handleArrowDown : Flow SelectState ()
handleArrowDown =
    Flow.get
        |> Flow.map (get activeIndex)
        |> Flow.andThen (\i -> Flow.modify (set activeIndex (i + 1)))


-- Embed it into the parent model through a lens
Flow.viaLens MyLenses.selectState handleArrowDown
viaPrism : A_Prism ls outer inner -> Flow inner a -> Flow outer (Maybe a)

Embed a Flow inner a into a larger model outer through a prism.

When the prism matches, the sub-flow runs with its state routed through the prism and its result is returned as Just. When the prism does not match, the sub-flow is skipped and the result is Nothing. If the prism stops matching while the sub-flow is suspended (for example across await), the sub-flow stops there and produces Nothing.

-- Run a component only while the model holds its active variant
Flow.viaPrism MyLenses.activePage handlePageEvent
replace : Flow b a -> (a -> Flow b ()) -> Flow a x -> Flow b x

Re-interpret a Flow a x inside a larger context Flow b x by providing a getter (Flow b a) and a setter (a -> Flow b ()).

This is the primitive that viaLens is built on. Use viaLens in normal code; reach for replace only when you need a custom traversal that optics cannot express.

Commands and tasks

lift : Cmd a -> Flow s a

Lift an Elm Cmd into Flow. The command is executed and its result becomes the value produced by this Flow step.

Flow.lift (Http.get { url = "/ping", expect = Http.expectString identity })
    |> Flow.andThen handleResponse
liftUpdate : (s -> ( s, Cmd a )) -> Flow s a

Lift a classic TEA update-style function into Flow. Reads the current model, applies the function, writes the new model, and emits any commands.

Flow.liftUpdate (MyOldModule.update msg)
performTask : Task Never a -> Flow s a

Run an infallible Task and produce its result.

Flow.performTask (Task.succeed 42)
    |> Flow.andThen (\n -> Flow.modify (\m -> { m | count = n }))
attemptTask : Task e a -> Flow s ()

Fire a Task for its side-effects and ignore the result (both Ok and Err are discarded). Useful for fire-and-forget operations such as writing to localStorage via a port task.

Flow.attemptTask (Ports.saveToLocalStorage key value)
attemptTaskWith : (Result e a -> Flow s b) -> Task e a -> Flow s b

Run a Task and feed its Result into a continuation.

Flow.attemptTaskWith
    (\result ->
        case result of
            Ok data ->
                Flow.modify (\m -> { m | data = data })


            Err err ->
                Flow.modify (\m -> { m | error = Just err })
    )
    (Http.task { ... })

Concurrency

batch : List a -> Flow s a

Run several independent Flow branches concurrently, one per list element. Each branch shares the same initial state snapshot; state writes from earlier branches are visible to later ones (left-to-right evaluation order).

Flow.batch [ 1, 2, 3 ]
    |> Flow.map (\n -> n * 2)
-- forks into three branches producing 2, 4, 6
batchM : List (Flow s a) -> Flow s a

Run a list of Flow computations as concurrent branches. Each branch runs independently; the continuation (via andThen) is invoked once per branch with that branch's individual result.

WARNING: batchM multiplies the number of fibers by the number of continuations. If you wish to extend a multi-fiber Flow on a single branch, consider async.

Use mapM instead if you want the results collected into a List.

-- fire three notifications in parallel
Flow.batchM [ notifyAlice, notifyBob, notifyCarol ]


-- continuation runs once per branch, not once with a List
Flow.batchM [ fetchUserA, fetchUserB ]
    |> Flow.andThen (\user -> renderUser user)
async : Flow s a -> Flow s ()

Fire a Flow as a background branch and immediately continue with (). The background branch runs concurrently; its state writes are interleaved with the rest of the batch.

Flow.async (longRunningSync model)
    |> Flow.seq nextStep
gather : Codec a -> Flow s a -> Flow s (List a)

Join the branches of a batched Flow: wait until every branch has finished, then continue once with all of their results.

Results come in branch order (the order of the batch / batchM lists), not in completion order. Branches that terminate with none contribute nothing.

Results are stored outside the Flow while the other branches are still running, so they travel as JSON: provide a Codec for the result type. If its decoder rejects an encoded result, the joined Flow terminates.

WARNING: the join waits for every branch forked inside it, including background branches started with async or subscribe. A branch that never finishes keeps the join waiting forever.

-- fetch all users concurrently, then store them in input order
Flow.batch userIds
    |> Flow.andThen fetchUser
    |> Flow.gather (Flow.Codec encodeUser userDecoder)
    |> Flow.andThen (\users -> Flow.modify (\m -> { m | users = users }))
gather_ : Flow s a -> Flow s ()

Like gather, but discards the results: wait until every branch has finished, then continue once with (). Needs no Codec.

WARNING: like gather, it waits for every branch forked inside it, including background branches.

-- save every draft concurrently, then report once
Flow.batch drafts
    |> Flow.andThen saveDraft
    |> Flow.gather_
    |> Flow.seq (Flow.modify (\m -> { m | status = AllSaved }))
type Codec a
= Codec (a -> Value) (Decoder a)

How to turn a value into JSON and back: an encoder and a decoder that accepts whatever the encoder produces.

userCodec : Flow.Codec User
userCodec =
    Flow.Codec encodeUser userDecoder

Rendering

yield : a -> Flow s a

Yield a value back to the Elm runtime, allowing the browser to repaint, then continue with the value. Equivalent to Process.sleep 0.

-- ensure the UI reflects intermediate state before a heavy computation
Flow.set loadingModel
    |> Flow.seq (Flow.yield ())
    |> Flow.seq heavyComputation
forceRendering : Flow a b -> Flow a b

Wrap a Flow so that every state write inside it is followed by a render cycle, giving the browser a chance to repaint between steps.

Useful when you need the UI to visually reflect intermediate states during a multi-step computation rather than only showing the final result.

-- the UI repaints after each step, not just at the end
Flow.forceRendering <|
    Flow.mapM [ setStepOne, setStepTwo, setStepThree ]

Channels

Describe an event source with a ChannelSpec: use connect when you have both a subscription source and a command to kick it off, or listen when subscriptions alone are enough. Then consume events with await, subscribe, or subscribeWhile, each of which opens a live Channel from the spec.

A description of how to connect an external event source to a Flow pipeline. A spec does nothing by itself and can be opened any number of times, including concurrently; each opening is a separate Channel.

type Channel a

One live opening of a ChannelSpec. Receive further values from it with nextOnChannel.

type alias ChannelKey =
String

Key identifying a Channel.

Opening a spec creates a fresh key and passes it to the spec's command. The channel keeps that key for its whole lifetime, so the external source can tag its values with it and filterChannel can match them.

((a -> Maybe a) -> Sub (Maybe a))
-> (ChannelKey -> Cmd Never)

Build a spec from a subscription port and a command that opens or attaches it. The command fires each time the spec is opened, receiving the key of the new channel; nextOnChannel does not fire it again.

-- Port declarations:
--   port onMessage : (String -> msg) -> Sub msg
--   port subscribe : String -> Cmd msg
myChannel : ChannelSpec String
myChannel =
    Flow.connect Ports.onMessage (\_ -> Ports.subscribe "myTopic")

To drop or transform values before they reach the channel, wrap the port:

-- port onJson : (Json.Decode.Value -> msg) -> Sub msg
userChannel : ChannelSpec User
userChannel =
    Flow.listen
        (\toMsg -> Ports.onJson (Json.Decode.decodeValue userDecoder >> Result.toMaybe >> Maybe.andThen toMsg))
listen : ((a -> Maybe a) -> Sub (Maybe a)) -> ChannelSpec a

Build a spec from a subscription port when no startup command is needed.

-- Port declaration:
--   port onKeyDown : (String -> msg) -> Sub msg
keyChannel : ChannelSpec String
keyChannel =
    Flow.listen Ports.onKeyDown
filterChannel : (ChannelKey -> a -> Bool) -> ChannelSpec a -> ChannelSpec a

Filter events for a spec. Only events for which the predicate returns True are forwarded; others are silently dropped.

The predicate receives the ChannelKey of the channel and the event payload. Match on the key when the source tags values with it; otherwise you only need the payload:

Flow.filterChannel (\_ msg -> msg /= "") myChannel

Suspend this Flow pipeline, open a channel from the spec, and resume when a value arrives.

type alias Model =
    { latestMessage : String }


listenOnce : Flow Model ()
listenOnce =
    Flow.await myChannel
        |> Flow.andThen (\msg -> Flow.modify (\m -> { m | latestMessage = msg }))
subscribe : (a -> Flow s ()) -> ChannelSpec a -> Flow s a

Open a channel and run the given handler for every value it produces, indefinitely.

listenForever : Flow Model ()
listenForever =
    Flow.subscribe
        (\msg -> Flow.modify (\m -> { m | latestMessage = msg }))
        myChannel
subscribeWhile : (s -> a -> Bool) -> (a -> Flow s ()) -> ChannelSpec a -> Flow s a

Like subscribe, but stops when the predicate returns False. The predicate receives both the current model and the incoming value, so cancellation can be based on model state, the value itself, or both. Cancellation is lazy: the check runs on each incoming message, not immediately when the model changes.

-- stop based on model state alone
Flow.subscribeWhile (\model _ -> model.listening) handleMessage myChannel


-- stop on a specific sentinel value
Flow.subscribeWhile (\_ msg -> msg /= "disconnect") handleMessage myChannel
openChannel : ChannelSpec a -> (Channel a -> a -> Maybe (Flow s b)) -> Flow s b

Open a channel from a spec, firing the spec's command, and suspend until the first value arrives. The callback receives the opened Channel and each value: Just flow resumes with flow, Nothing ignores the value and keeps waiting.

This is the low-level primitive that await and subscribe are built on. Prefer those unless you need custom continuation logic.

nextOnChannel : Channel a -> (a -> Maybe (Flow s b)) -> Flow s b

Wait for the next value on an already opened channel, without firing the spec's command again.

ffi :
( { key : String
, fn : String
, value : Value
}
-> Cmd Never
)
-> ( ( { key : String
, value : Value
}
-> Maybe
{ key : String
, value : Value
}
)
-> Sub
(Maybe
{ key : String
, value : Value
}
)
)
-> String
-> (a -> Value)
-> Decoder b
-> a
-> Flow s b

Instead of creating a new port for every JavaScript interaction, you can use Flow.ffi to call many JS functions by name using a single pair of ports.

You provide the JS function name, an encoder for the outgoing data, and a decoder for the return value. Under the hood, this helper uses a unique string key to automatically match JavaScript responses back to the correct Flow request.

-- 1. Define one pair of ports






port ffiOut : { key : String, fn : String, value : Json.Encode.Value } -> Cmd msg


port ffiIn : ({ key : String, value : Json.Decode.Value } -> msg) -> Sub msg


-- 2. Wire them into a helper
callJs : String -> (a -> Json.Encode.Value) -> Json.Decode.Decoder b -> a -> Flow s b
callJs =
    Flow.ffi Ports.ffiOut Ports.ffiIn


-- 3. Call any JS function by name
fetchData : String -> Flow Model ()
fetchData id =
    callJs "fetchData" Json.Encode.string decodeData id
        |> Flow.andThen handleData


-- On the JavaScript side:
-- app.ports.ffiOut.subscribe(async (req) => {
--     if (req.fn === "fetchData") {
--         const data = await fetch("/api/data/" + req.value).then(r => r.json());
--         app.ports.ffiIn.send({ key: req.key, value: data });
--     }
-- });

Running a program

type alias Program flags s a =
Program flags ( s, Runtime s a ) (RuntimeMsg s a)

The concrete Elm Platform.Program type produced by Flow.element, Flow.document, and Flow.application.

{ init : flags -> ( s, Flow s a )
, view : s -> Html (Flow s a)
, subscriptions : s -> Sub (Flow s a)
}
-> Program flags s a

Build a standard Browser.element program whose messages are Flow computations.

{ init : flags -> ( s, Flow s a )
, view : s -> Document (Flow s a)
, subscriptions : s -> Sub (Flow s a)
}
-> Program flags s a

Like element but the view function returns a full Document, allowing you to control the page <title>.

main =
    Flow.document
        { init = \_ -> ( initialModel, loadData )
        , view = view -- returns { title = "…", body = […] }
        , subscriptions = \_ -> Sub.none
        }
{ init : flags -> Url -> Key -> ( s, Flow s a )
, view : s -> Document (Flow s a)
, subscriptions : s -> Sub (Flow s a)
, onUrlRequest : UrlRequest -> Flow s a
, onUrlChange : Url -> Flow s a
}
-> Program flags s a

Like document but also handles URL changes, for single-page applications.

main =
    Flow.application
        { init = \_ url key -> ( initialModel key, loadPage url )
        , view = view
        , subscriptions = \_ -> Sub.none
        , onUrlRequest = handleUrlRequest
        , onUrlChange = handleUrlChange
        }