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…
└─────────────────────────────────┘
- At launch, the runtime evaluates
initand callsviewwith it. - The resulting
View Msgvalue is decoded and rendered into UIKit views (see views). - Interactive attributes carry
Msgvalues:onTap Incrementliterally stores theIncrementvalue in the button.onInput DraftChangedstores theDraftChangedconstructor — a functionString -> Msg— and the runtime applies it to the field’s text when it changes. - 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.