# elm-flow

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

This library merges concepts from two community packages:

- [`chrilves/elm-io`](https://package.elm-lang.org/packages/chrilves/elm-io/latest/) — the free monad interface for TEA
- [`brian-watkins/elm-procedure`](https://github.com/brian-watkins/elm-procedure) — continuation-passing channels and subscriptions

## Overview

`Flow s a` is a free monad that describes a computation which can:

- Read and write application state `s`
- Fire Elm `Cmd`s and receive their results
- Subscribe to channels (ports, WebSockets, timers, …) and resume when a value arrives
- Run multiple branches concurrently via `Batch`

The runtime interpreter (`Flow.Program`) drives these computations inside a standard TEA `update` loop, so the rest of your Elm app stays unchanged.

## Quick start

With `elm-flow`, you do **not** define a custom `Msg` type or an `update` function.
Instead, your `view` handlers and `subscriptions` emit `Flow` values directly,
and the library runtime interprets them.

```elm
import Flow exposing (Flow)
import Html
import Html.Events

type alias Model =
    { count : Int }

main : Flow.Program () Model Never
main =
    Flow.element
        { init = \_ -> ( { count = 0 }, Flow.none )
        , view = view
        , subscriptions = \_ -> Sub.none
        }

view : Model -> Html.Html (Flow Model ())
view model =
    Html.button
        [ Html.Events.onClick (Flow.modify (\m -> { m | count = m.count + 1 })) ]
        [ Html.text ("Count: " ++ String.fromInt model.count) ]
```

## Channels

Channels connect external event sources (ports, WebSockets, timers) to a `Flow` pipeline. Use `Flow.Channel.connect` to build a channel from an Elm subscription port and an optional command port, then pass it to `Flow.await` (receive one value) or `Flow.subscribe` (handle values indefinitely).

```elm
-- In Ports.elm
port onMessage : (String -> msg) -> Sub msg
port send : String -> Cmd msg

myChannel : Channel Model String
myChannel =
    Flow.Channel.connect
        (\callback -> Ports.onMessage (\s -> callback s))
        (\_ -> Ports.send "subscribe")

listenLoop : Flow Model ()
listenLoop =
    Flow.subscribe
        (\msg -> Flow.modify (\m -> { m | lastMessage = msg }))
        myChannel
```

## Optics helpers

When you have nested state, the optics helpers (`try`, `forAll`, `over`, `setAll`, `via`) work with [`erlandsona/elm-accessors`](https://package.elm-lang.org/packages/erlandsona/elm-accessors/latest/) lenses to target sub-fields without manual getter/setter boilerplate.

## API reference

See the generated documentation for the full API.

### Core

| Function | Description |
|---|---|
| `pure` | Lift a plain value into Flow |
| `lift` | Lift a `Cmd` into Flow |
| `get` / `set` / `modify` | Read and write the model |
| `andThen` / `map` | Sequence and transform computations |
| `batch` / `batchM` | Run multiple branches |
| `none` | Terminate / no-op |

### Async

| Function | Description |
|---|---|
| `await` | Suspend until a Channel delivers one value |
| `subscribe` | Handle every value from a Channel indefinitely |
| `yield` | Force a render cycle before continuing |
| `async` | Fire-and-forget a sub-computation |

### Control flow

| Function | Description |
|---|---|
| `when` | Conditional execution |
| `bracket_` | Acquire/release resources around a computation |
| `setting` | Temporarily set a Bool lens to `True` |
| `locking` | Skip execution if a Bool lens is already `True` |

## License

BSD-3-Clause
