Documentation

The Walnut Architecture

Walnut applications use TEA (model / update / view), unchanged: one model, messages in, new model and view tree out.

The three parts

Every app defines:

init   : Model                   -- or (Model, Cmd Msg) with element
update : Msg -> Model -> Model   -- or … -> (Model, Cmd Msg)
view   : Model -> View Msg
-- element also: subscriptions : Model -> Sub Msg

and wires them into main via Walnut.sandbox or Walnut.element (see effects.md).

The loop

            ┌───────────────┐
   Model ──▶│     view      │──▶ View Msg (pure data)
    ▲       └───────────────┘         │
    │                                 ▼
┌───────────────┐             ┌───────────────┐
│    update     │             │   Renderer    │──▶ UIKit views
└───────────────┘             └───────────────┘
    ▲                                 │
    │            Msg                  │  user taps, types…
    └─────────────────────────────────┘
  1. At launch, the runtime evaluates init and calls view with it.
  2. The resulting View Msg value is decoded and rendered into UIKit views (see views).
  3. Interactive attributes carry Msg values: onTap Increment literally stores the Increment value in the button. onInput DraftChanged stores the DraftChanged constructor — a function String -> Msg — and the runtime applies it to the field’s text when it changes.
  4. When an event fires, the runtime computes update msg model, stores the new model, and re-renders.

There are no other state containers. Deleting a feature means deleting a Msg case and its update branch — the compiler-style error messages tell you what still refers to it.

Modeling state

Use custom types to make impossible states unrepresentable:

type RemoteData
    = Loading
    | Loaded (List Item)
    | Failed String

type alias Model =
    { catalog : RemoteData
    , cart : List Item
    }

A view over this model cannot forget the loading state — case model.catalog of must handle all three.

Growing an app

TEA scales by composition of plain functions, not by framework machinery:

view : Model -> View Msg
view model =
    column [ padding 20, spacing 16 ]
        [ viewHeader model.user
        , viewCart model.cart
        , viewCatalog model.catalog
        ]

viewCart : List Item -> View Msg
viewCart items = ...

Helper views are just functions returning View Msg. Helper updates are just functions ... -> Model -> Model. When a section grows big, move it to a module:

import Cart

update msg model =
    case msg of
        CartMsg cartMsg ->
            { model | cart = Cart.update cartMsg model.cart }

Why sandbox?

Walnut.sandbox is deliberately the smallest possible program type: state and UI, no managed effects.

For storage, notifications, and other side effects, use Walnut.element — see effects.md:

init          : ( Model, Cmd Msg )
update        : Msg -> Model -> ( Model, Cmd Msg )
subscriptions : Model -> Sub Msg
view          : Model -> View Msg

main =
    Walnut.element { init, update, view, subscriptions }

Cmd and Sub are data interpreted by the iOS host (Storage, Keychain, CoreData, Notifications, …). update stays pure.

Under the hood

main is just a value. Walnut.sandbox / Walnut.element construct a Program. The toolchain compiles your main program for the iOS host. Sandbox apps are wrapped so init/update always return (model, cmd) tuples.

WalnutViewController owns the model, runs Cmds after each update, and refreshes subscriptions. Dispatch is main-thread; async Cmd replies (storage get, permission results) re-enter dispatch on the main queue.