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 })The concrete Elm Platform.Program type produced by Flow.element,
Flow.document, and Flow.application.
Build a standard Browser.element program whose messages are Flow
computations.
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
}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
}Suspend this Flow pipeline, open a Channel, 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 }))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 }))
myChannelLike 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 myChannelInstead 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 });
-- }
-- });Lift a plain value into Flow without performing any effects.
Flow.pure 42
|> Flow.map (\n -> n + 1)
-- produces 43, state unchangedLift 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 handleResponseLift 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)Read the current model.
Flow.get
|> Flow.andThen (\model -> Flow.pure model.count)Replace the model entirely.
Flow.set { count = 0 }Apply a function to transform the model.
Flow.modify (\m -> { m | count = m.count + 1 })Transform the value produced by a Flow without changing state.
Flow.pure 3
|> Flow.map (\n -> n * 2)
-- produces 6Chain two Flow computations. The second receives the value produced by the first.
Flow.get
|> Flow.andThen (\m -> Flow.pure m.count)Flatten a nested Flow s (Flow s a) into Flow s a.
Flow.pure (Flow.pure 42)
|> Flow.join
-- same as Flow.pure 42Apply a function wrapped in Flow to a value wrapped in Flow.
Flow.pure (\n -> n + 1)
|> Flow.ap (Flow.pure 41)
-- produces 42Flipped ap — apply a value to a function, both wrapped in Flow.
Flow.pure 41
|> Flow.flap (Flow.pure (\n -> n + 1))
-- produces 42Compose two Kleisli arrows (functions returning Flow).
handleResponse : Response -> Flow Model ()
handleResponse =
Flow.compose persistToDisk fetchDataRun 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!"Map over a list with a function that returns Flow, collecting the results in order.
Flow.traverse fetchUser userIds
|> Flow.andThen renderAllSequence a list of Flow computations, collecting all results.
Equivalent to traverse identity.
Flow.mapM [ step1, step2, step3 ]
|> Flow.andThen finalizeRe-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 via and the other optics helpers are built on.
Use via in normal code; reach for replace only when you need a custom
traversal that optics cannot express.
The empty computation — terminates the current branch without producing a value or changing state. Use this as the natural end-point of a pipeline.
Flow.get
|> Flow.andThen
(\m ->
if m.ready then
doWork
else
Flow.none
)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 heavyComputationWrap 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 ]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, 6Run 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.
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)Continue only if the value is Just, terminate with none otherwise.
fetchUser userId
|> Flow.assertJust
|> Flow.andThen renderUserContinue only if the result is Ok, silently terminate on Err.
decodeResponse body
|> Flow.assertOk
|> Flow.andThen handleDataContinue only if the predicate holds for the produced value, terminate
with none otherwise.
Flow.get
|> Flow.assertCondition (\m -> m.count > 0)
|> Flow.andThen doSomethingWithPositiveCountUnwrap 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)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)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 MyModel.selectedItem
(\maybeItem ->
case maybeItem of
Just item ->
renderItem item
Nothing ->
showPlaceholder
)Read a value through an optic, terminating with none if the optic does
not match.
Flow.forAll MyModel.selectedItem
(\item -> renderItem item)Read all values targeted by an optic (useful for traversals).
Flow.getAll MyModel.allItems
(\items -> Flow.pure (List.length items))Modify the value(s) targeted by an optic.
Flow.over MyModel.count (\n -> n + 1)Set the value(s) targeted by an optic.
Flow.setAll MyModel.status LoadingRun a Flow a x in the context of a larger model s, using an optic to
zoom in and out. State reads and writes inside the sub-flow are applied through
the optic.
Flow.via MyModel.editingRecord
(Flow.modify (\r -> { r | name = newName }))Run a Flow only when the condition is True, otherwise produce ().
Flow.when model.isLoggedIn savePreferencesRun a Flow for its effects, then produce a fixed value a regardless of
what the flow returned.
saveRecord
|> Flow.return "saved"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 nextStepAcquire a resource, run a computation, then release the resource —
even if the computation terminates early with none.
Flow.bracket_
(Flow.modify (\m -> { m | loading = True }))
(Flow.modify (\m -> { m | loading = False }))
fetchDataSet a Bool field to True for the duration of a computation, then
restore it to False. Useful for loading/busy flags.
Flow.setting MyModel.isLoading fetchDataRun 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 MyModel.isSaving saveRecord
A description of a computation that can read and write state
sand eventually produces a value of typea.Flowis a free monad: values of this type are data structures that the runtime interprets, rather than functions that run immediately. This makes it possible to batch, sequence, and transform computations before any side-effects take place.