421anon/elm-flow/1.0.0

NOTE: the latest version is 6.0.2

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

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 first Flow for its effects, discard its result, then run the second and return its result.

saveRecord
    |> Flow.seq (Flow.pure "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

Force a full render cycle before continuing by writing the current model back to itself via yield. Use this when you need the DOM to update between two state changes.

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 concurrently and collect all their results.

Flow.batchM [ fetchUser, fetchSettings, fetchPermissions ]
    |> Flow.andThen (\results -> ...)

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