Documentation
Effects
Walnut apps start as a sandbox (pure init / update / view). Real side effects — storage, Keychain, Core Data, notifications, HTTP, time, and host ports — use Walnut.element, which adds Cmd and Sub.
Sandbox vs element
Walnut.sandbox | Walnut.element | |
|---|---|---|
init | Model | ( Model, Cmd Msg ) |
update | Msg -> Model -> Model | Msg -> Model -> ( Model, Cmd Msg ) |
view | Model -> View Msg | same |
subscriptions | — | Model -> Sub Msg |
Sandbox programs still run on iOS with empty effects (Cmd.none / Sub.none). Use Walnut.element when you need cmds or subscriptions.
main =
Walnut.element
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}
The loop
init ──▶ (Model, Cmd) ──▶ run Cmds ──▶ maybe more Msgs
│
▼
view / subscriptions
│
user event / Sub event
│
▼
update ──▶ (Model, Cmd) ──▶ …
update stays pure. The host interprets Cmd trees after each update and manages Sub lifecycles.
Cmd
import Cmd exposing (Cmd)
import Storage
import Keychain
import CoreData
import Notifications
import Http
import Time
import Port
Cmd.none
Cmd.batch [ cmd1, cmd2 ]
Cmd.map Persist persistCmd -- (a -> msg) -> Cmd a -> Cmd msg
Sub.map Persist persistSub
Cmd.map / Sub.map rewrite effect callbacks (tagger << toMsg) and recurse through batch. Extend them in the bundled stdlib whenever you add a new callback-carrying ctor.
Storage.set "todo.nextId" "9"
Storage.get "todo.nextId" GotNextId
Keychain.set "api.token" token
Keychain.get "api.token" GotToken
Keychain.delete "api.token"
CoreData.put "todos" "42" "{\"title\":\"Buy milk\"}"
CoreData.get "todos" "42" GotTodo
CoreData.delete "todos" "42"
CoreData.fetchAll "todos" GotTodos -- List String
Notifications.requestPermission PermissionResult
Http.get "https://httpbin.org/get" GotBody
Http.post url body GotBody
Http.request "PUT" url body GotBody -- Result String String -> msg
Time.now Tick -- Int -> msg (Posix ms)
Port.cmd "echo" "hello" GotEcho -- host WalnutPorts.registerCmd
Prefer the module helpers (Storage, Keychain, CoreData, Notifications, Http, Time, Port) over constructing Cmd* tags by hand.
Sub
import Sub exposing (Sub)
import Notifications
import Time
import Port
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.batch
[ Notifications.onReceive PushReceived
, Time.every 1000 Tick -- Int -> msg (Posix ms)
, Port.sub "inbound" FromHost -- String -> msg
]
SubNone/SubBatchSubNotifyOnReceive (String -> msg)— payload is a string (JSON or plain text)SubEvery Int (Int -> msg)— interval in ms; host diffs timers by intervalSubPort String (String -> msg)— inbound host port
On iOS, deliver a notification payload into active subscriptions with:
WalnutNotifications.deliver(payload: "{\"title\":\"Hi\"}")
Deliver into a Port.sub:
WalnutPorts.registerSub("inbound")
WalnutPorts.send("inbound", payload: "hello")
HTTP
Http.get / Http.post / Http.request use URLSession on iOS. The callback is always Result String String -> msg:
Ok body— UTF-8 response body (2xx)Err message— network error or non-2xx status text
Http.get "https://httpbin.org/get" GotBody
GotBody result ->
case result of
Ok text -> …
Err err -> …
Time
Time.every ms toMsg— subscription; fires with Posix milliseconds since epochTime.now toMsg— one-shotCmdwith the current Posix ms
See the HTTP / timer examples in the effects section above.
Ports (host registry)
App-specific native work (MLX, custom SDKs, glue) should use ports. Register handlers from your app’s native shell:
WalnutPorts.registerCmd("echo") { payload, reply in
reply(payload)
}
WalnutPorts.registerSub("inbound")
// later:
WalnutPorts.send("inbound", payload: "from host")
Walnut side:
Port.cmd "echo" "hello" GotEcho
Port.sub "inbound" FromHost
Register the same name on both sides. Ports are how you bridge custom native SDKs without putting that code in the shared runtime.
Which persistence API?
| Module | Backing store | Use for |
|---|---|---|
Storage | UserDefaults (walnut.storage.*) | Small prefs, flags, counters |
Keychain | iOS Keychain (walnut.keychain) | Secrets, tokens, credentials |
CoreData | SQLite via Core Data | Structured / larger documents |
All three speak strings at the language boundary. Encode JSON (or another format) in Walnut when you need structure.
Storage (iOS)
Storage.set/Storage.getStorage.getalways replies withMaybe String(Nothingif missing)
Keychain (iOS)
Keychain.set/Keychain.get/Keychain.delete- Generic password items, service
walnut.keychain, account = key - Accessible after first unlock, this device only
Core Data (iOS)
Document-style store — no .xcdatamodel in the app project. One built-in entity:
| Field | Meaning |
|---|---|
collection | Logical table / type name (e.g. "todos") |
id | Record id within that collection |
payload | Opaque string (usually JSON) |
CoreData.put "todos" id json
CoreData.get "todos" id GotOne
CoreData.fetchAll "todos" GotAll -- List of payloads, sorted by id
CoreData.delete "todos" id
DB file: Application Support (Walnut Core Data store).
Custom schemas / relationships / predicates beyond this document API stay app-specific via ports.
Notifications (iOS)
Notifications.requestPermission→ system permission dialog →Bool -> msgNotifications.onReceive→ subscription for inbound payloads
Remote APNs still needs Xcode capabilities / certificates; the language surface is the Cmd/Sub API above. Local/simulated delivery uses WalnutNotifications.deliver.
What belongs in core vs ports
| Built-in Cmd/Sub | Escape hatch (ports) |
|---|---|
| Storage, Keychain, Core Data, Notifications, HTTP, Time | MLX, custom Core Data schemas, one-off SDK glue |
Do not put MLX into WalnutRT. Use WalnutPorts.registerCmd / registerSub + Port.cmd / Port.sub.
Todo example
A typical app uses Walnut.element to:
CoreData.getprefs on launch (todo.meta/prefs)- First launch → seed tasks, then
CoreData.puteach item + prefs - Later launches →
CoreData.fetchAll "todo.items"and restore the list - Mutations (add / toggle / delete / folder open) → put/delete via Core Data
Notifications.requestPermissionwhen opening the compose sheetNotifications.onReceive→ fillsdraftwith the payload string
Item payloads are tab-separated strings (id, title, project, area, flags).
Notes
- Prefer
Walnut.elementwhen you need effects; sandbox programs still compile with empty cmds/subs. - App-specific native work belongs in ports, not in the shared runtime.