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.sandboxWalnut.element
initModel( Model, Cmd Msg )
updateMsg -> Model -> ModelMsg -> Model -> ( Model, Cmd Msg )
viewModel -> View Msgsame
subscriptionsModel -> 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 / SubBatch
  • SubNotifyOnReceive (String -> msg) — payload is a string (JSON or plain text)
  • SubEvery Int (Int -> msg) — interval in ms; host diffs timers by interval
  • SubPort 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 epoch
  • Time.now toMsg — one-shot Cmd with 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?

ModuleBacking storeUse for
StorageUserDefaults (walnut.storage.*)Small prefs, flags, counters
KeychainiOS Keychain (walnut.keychain)Secrets, tokens, credentials
CoreDataSQLite via Core DataStructured / 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.get
  • Storage.get always replies with Maybe String (Nothing if 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:

FieldMeaning
collectionLogical table / type name (e.g. "todos")
idRecord id within that collection
payloadOpaque 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 -> msg
  • Notifications.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/SubEscape hatch (ports)
Storage, Keychain, Core Data, Notifications, HTTP, TimeMLX, 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:

  1. CoreData.get prefs on launch (todo.meta / prefs)
  2. First launch → seed tasks, then CoreData.put each item + prefs
  3. Later launches → CoreData.fetchAll "todo.items" and restore the list
  4. Mutations (add / toggle / delete / folder open) → put/delete via Core Data
  5. Notifications.requestPermission when opening the compose sheet
  6. Notifications.onReceive → fills draft with the payload string

Item payloads are tab-separated strings (id, title, project, area, flags).

Notes

  • Prefer Walnut.element when you need effects; sandbox programs still compile with empty cmds/subs.
  • App-specific native work belongs in ports, not in the shared runtime.