421anon/elm-flow/2.1.0

NOTE: the latest version is 6.0.2

Rerouted (retagged, needs approval). The tag now points at a different commit than when this version was published. The Lamdera package server serves a copy recovered from https://github.com/421anon/elm-flow.

What we checked: Tag 2.1.0 is still on github.com/421anon/elm-flow; its commit a0f12ea9a25f is from 2026-03-20 12:05 UTC, after the version was published (2026-03-20 11:50 UTC); elm.json is byte-identical to package.elm-lang.org's; the exposed modules match docs.json; the zip's SHA-1 is not the published one and nothing else confirms its contents.

We could not verify this copy against what was published, so the Lamdera compiler asks before using it (y/n). Without a terminal, as in CI, it stops with an error unless approved:

  • lamdera make --auto-approve-package-rerouting approves rerouted versions for that run,
  • LAMDERA_AUTO_APPROVE_PACKAGE_REROUTING=1 does the same for every run in that environment,
  • LAMDERA_CACHE=off skips the Lamdera package server altogether: packages come from package.elm-lang.org and GitHub only, as with elm, and a rerouted version then fails to download.

Details · package.elm-lang.org's zip: https://github.com/421anon/elm-flow/zipball/2.1.0/

Flow

A monadic interface for The Elm Architecture, bridging synchronous state modifications with asynchronous subscriptions via Channels.

Note: This library merges concepts from two community packages:

  • chrilves/elm-io (The free monad interface for TEA)
  • brian-watkins/elm-procedure (Continuation-passing channels and subscriptions)

The Flow pattern

In a traditional TEA application you define a central Msg type and an update : Msg -> Model -> (Model, Cmd Msg) function that dispatches on it. With Flow there is no separate Msg type and no central dispatch table.

Your 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 runtime executes each Flow immediately as it arrives, keeping behaviour co-located with the event that triggers it.

type alias Flow s a =
Flow s a

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

Flow is 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.

Running a web application

type alias Program flags s a =
Program flags ( s, Model (Flow s a) ) (RuntimeMsg (Flow 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
        }

Subscriptions and channels

await : Channel s a -> Flow s a

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 }))
subscribe : (a -> Flow s ()) -> Channel s 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
ffi :
( { key : String
, fn : String
, value : Value
}
-> Cmd Never
)
-> ( ( { key : String
, value : Value
}
-> Maybe
(Flow
s
{ key : String
, payload : b
}
)
)
-> Sub
(Maybe
(Flow
s
{ key : String
, payload : b
}
)
)
)
-> 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 });
--     }
-- });

Lifting values and commands into Flow

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
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)

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 })

Monadic operations

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!"
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

Zooming into sub-models via optics

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 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.

Terminating a computation

none : Flow s a

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
        )

Forcing Elm to re-render

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 ]

Batching

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.

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)

Assertions and guards

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)

Task helpers

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)

Optics helpers

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 MyModel.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

Read a value through an optic, terminating with none if the optic does not match.

Flow.forAll MyModel.selectedItem
    (\item -> renderItem item)
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 MyModel.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 MyModel.count (\n -> n + 1)
setAll : An_Optic pr ls s a -> a -> Flow s ()

Set the value(s) targeted by an optic.

Flow.setAll MyModel.status Loading
via : An_Optic pr ls s a -> Flow a x -> Flow s x

Run 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 }))

Control flow

when : Bool -> Flow s () -> Flow s ()

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

Flow.when model.isLoggedIn savePreferences
return : a -> Flow s b -> Flow s a

Run a Flow for its effects, then produce a fixed value a regardless of what the flow returned.

saveRecord
    |> Flow.return "saved"
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
bracket_ : Flow s a -> Flow s b -> Flow s c -> Flow s c

Acquire 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 }))
    fetchData
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 MyModel.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 MyModel.isSaving saveRecord