Typed functional language for iOS

Walnut Write native iOS apps in a typed functional language.

Walnut compiles init / update / view programs to native code through LLVM. Static types catch bugs before the app runs. The language is small enough to learn in a day, but UIKit and ports let you reach anything on the platform.

Get started Pricing brew tap franckverrot/walnut && brew install walnut
Main.walnut
type alias Model =
    { count : Int }

type Msg
    = Increment
    | Decrement

update : Msg -> Model -> Model
update msg model =
    case msg of
        Increment ->
            { model | count = model.count + 1 }
        Decrement ->
            { model | count = model.count - 1 }

view : Model -> View Msg
view model =
    column [ centerX, spacing 24 ]
        [ el [ fontSize 64, bold ]
            (text (String.fromInt model.count))
        , button [ onTap Increment, glassProminent ]
            [ text "+" ]
        ]
Lumen Today screen with goals and next action
Todo app with morning and afternoon tasks

Why Walnut

If it typechecks, it runs.
If it compiles, it ships.

01

Static types everywhere

Messages, models, and views are ordinary typed data. No hidden widget trees, no mystery runtime crashes from mistyped updates. If the compiler accepts it, the app works.

02

One loop, explicit effects

Every app is init / update / view. Side effects are typed commands and subscriptions. There is one way to structure an app and it scales from a counter to a multi-tab production app.

03

Full platform access

Need HealthKit, Core ML, or a custom UIView? Ports and native "ClassName" open the entire iOS SDK without stuffing it into the language core.

Tooling

Fast checks. Host tests. Hot reload.

Walnut's toolchain is built for tight iteration loops. Typecheck in milliseconds, run tests on your Mac without a Simulator, and hot-reload UI changes into a running app.

  • Exhaustive case on messages: miss a branch and the compiler rejects it
  • walnut check in milliseconds, no Xcode build needed
  • walnut test runs pure update logic on the host JIT
  • walnut simulator --watch hot-reloads without killing the app
dispatch keeps App thin
-- Root update: compile-time exhaustive
update msg model =
    dispatch
        [ Auth.update
        , Nav.update
        , Tasks.update
        , Persist.update
        ]
        msg
        model
-- Ports: unbounded native, typed at the edge
Port.cmd "mlx.rank" payload GotRanks
Port.sub "appIntent" IntentFired

Language

Small programs. Full apps.

Views are data

column [ spacing 12, padding 20 ]
    [ el [ bold ] (text model.title)
    , button [ onTap Save, glass ]
        [ text "Save" ]
    ]

Effects stay typed

Http.get
    { url = "https://api.example.com/v1"
    , expect = Http.expectString GotBody
    }

UIKit when you need it

uiSwitch
    [ propBool "on" model.lit
    , onNativeBool "valueChanged" SetLit
    ]

Toolchain

From .walnut to the phone.

  1. checkStatic types, exhaustive messages
  2. compileLLVM to native object code
  3. hostThin UIKit shell + TEA loop
  4. shipSimulator, device, TestFlight

Install in one command.

Prebuilt CLI, stdlib, iOS runtime, and docs. Apple Silicon.

brew tap franckverrot/walnut
brew install walnut
walnut version
walnut new HelloApp