Documentation
Language guide
This guide is the tour of Walnut’s surface language: values, types, modules, layout, and TEA programs.
Values and arithmetic
42 -- Int
3.14 -- Float
"hello" -- String
'w' -- Char
True -- Bool
() -- Unit
1 + 2 * 3 -- 7
10 / 4 -- 2.5 (/ always gives a Float)
10 // 4 -- 2 (integer division)
2 ^ 10 -- 1024
modBy 3 7 -- 1
"wal" ++ "nut" -- "walnut"
Comments:
-- a line comment
{- a block comment
{- they nest -}
-}
Strings support escapes (\n, \t, \", \\, \u{1F330}) and triple-quoted multiline form:
poem = """
roll a walnut
down the hill
"""
Names and definitions
Definitions are equations. There is no let/var/func keyword at the top level — just the name, parameters, =, and a body:
answer = 42
double n = n * 2
hypotenuse a b = sqrt (a * a + b * b)
Type annotations go on the line above. They are currently documentation checked at runtime, not compile time, but write them — they make code legible and they’re ready for the type checker:
hypotenuse : Float -> Float -> Float
hypotenuse a b = sqrt (a * a + b * b)
Top-level declarations can appear in any order; Walnut resolves them lazily.
Functions, currying, pipelines
Every function is curried. Applying fewer arguments returns a new function:
add : Int -> Int -> Int
add a b = a + b
increment = add 1 -- partial application
increment 41 -- 42
Anonymous functions use a backslash:
\x -> x * 2
\name age -> name ++ " is " ++ String.fromInt age
Pipelines make data flow read top-to-bottom, left-to-right:
List.range 1 100
|> List.filter (\n -> modBy 3 n == 0)
|> List.map String.fromInt
|> String.join ", "
|> pipes a value into a function; <| is the reverse. >> and << compose functions:
sanitize = String.trim >> String.toLower
Operators become functions when parenthesized:
List.foldl (+) 0 [1, 2, 3, 4] -- 10
List.foldl steps may return the accumulator unchanged (if x > acc then x else acc). The runtime keeps a single retain on that shared value — do not assume every step allocates a fresh object. Prefer owned ints when lifting record fields into the acc (p.value + 0), since fields are borrowed.
if and case
if is an expression and always has an else:
label n = if n == 1 then "1 item" else String.fromInt n ++ " items"
case matches patterns, top to bottom:
describe : List Int -> String
describe list =
case list of
[] ->
"empty"
[ only ] ->
"just " ++ String.fromInt only
first :: _ ->
"starts with " ++ String.fromInt first
Patterns can be literals, variables, _ wildcards, tuples, lists, head :: tail, constructors, record fields, and ... as name aliases:
area shape =
case shape of
Circle radius ->
pi * radius * radius
Rectangle w h ->
w * h
positionToString (( x, y ) as point) =
String.fromInt x ++ "," ++ String.fromInt y
let ... in
Local definitions:
viewStats scores =
let
total = List.sum scores
count = List.length scores
mean = toFloat total / toFloat count
in
text ("mean: " ++ String.fromFloat mean)
Like top-level declarations, let bindings can reference each other in any order.
Custom types
Custom types (a.k.a. tagged unions / ADTs) are the backbone of Walnut modeling — especially your Msg type:
type Msg
= Increment
| Decrement
| SetName String
| GotResult (Result String Int)
Constructors are functions: SetName : String -> Msg. Make impossible states impossible:
type Session
= Anonymous
| LoggedIn { name : String, token : String }
Maybe and Result are built in:
String.toInt "42" -- Just 42
String.toInt "walnut" -- Nothing
Maybe.withDefault 0 (String.toInt s) -- Int, no null in sight
Records
Records are labeled data with structural access and non-destructive update:
type alias Model =
{ count : Int
, name : String
}
init : Model
init = { count = 0, name = "walnut" }
init.count -- 0
{ init | count = init.count + 1 } -- a NEW record; init is unchanged
-- .field is also a function:
List.map .name users
The { record | field = value } syntax is how every update function in every Walnut app is written:
update msg model =
case msg of
Increment ->
{ model | count = model.count + 1 }
Tuples
point = ( 3, 4 )
Tuple.first point -- 3
distance ( x1, y1 ) ( x2, y2 ) =
sqrt ((x2 - x1) ^ 2.0 + (y2 - y1) ^ 2.0)
Modules and imports
Every file is a module. The header names it and states what it exposes:
module Inventory exposing (Item, total, view)
Imports:
import Inventory -- qualified: Inventory.total
import Inventory as Inv -- aliased: Inv.total
import Inventory exposing (total) -- bare: total
import Ui exposing (..) -- everything unqualified
Basics (arithmetic, not, modBy, identity, …) plus the Maybe/Result/Bool/Order constructors are always in scope, and the stdlib modules (String, List, Maybe, Result, Tuple, Char, Debug, Ui, Walnut) are always reachable fully qualified — import just controls unqualified access.
dispatch (nested TEA updates)
When Msg wraps subdomain messages (Auth AuthMsg | Nav NavMsg | …), the root update can be a compile-time dispatch instead of a hand-written case:
import Update.Auth as Auth
import Update.Nav as Nav
import Update.Tasks as Tasks
update : Msg -> Model -> ( Model, Cmd Msg )
update =
dispatch
[ Auth.update
, Nav.update
, Tasks.update
]
Each handler must have type subMsg -> model -> result. The compiler matches subMsg to a unary Msg constructor payload, checks that every constructor is covered exactly once, and expands to a normal exhaustive case. Missing or duplicate handlers are type errors.
When several constructors share similar payloads, prefer the record form so each field names the constructor explicitly:
update : Msg -> Model -> ( Model, Cmd Msg )
update =
dispatch
{ Auth = Auth.update
, Nav = Nav.update
, Tasks = Tasks.update
}
List and record forms desugar to the same case.
Layout (indentation) rules
Walnut uses the offside (layout) rule; there are no braces or semicolons:
- Top-level declarations start at column 1.
- An expression continues as long as its lines are indented deeper than the construct that opened it.
casebranches align with each other; each branch body is indented past its pattern.letbindings align with each other;infollows at or left of them.
update msg model = -- column 1
case msg of -- indented: still `update`'s body
Increment -> -- branch column: 9
{ model | count = model.count + 1 }
Decrement -> -- next branch, same column
{ model | count = model.count - 1 }
A complete program
Everything above, in the shape every Walnut app takes:
module Main exposing (main)
import Ui exposing (..)
import Walnut
-- MODEL
type alias Model =
{ items : List String
, draft : String
}
init : Model
init =
{ items = [], draft = "" }
-- UPDATE
type Msg
= DraftChanged String
| Add
| Remove String
update : Msg -> Model -> Model
update msg model =
case msg of
DraftChanged value ->
{ model | draft = value }
Add ->
if String.isEmpty (String.trim model.draft) then
model
else
{ model | items = model.items ++ [ model.draft ], draft = "" }
Remove item ->
{ model | items = List.filter (\i -> i /= item) model.items }
-- VIEW
view : Model -> View Msg
view model =
column [ padding 20, spacing 12 ]
[ el [ fontSize 28, bold ] (text "Groceries")
, row [ spacing 8 ]
[ textField [ placeholder "Add an item…", content model.draft, onInput DraftChanged ]
, button [ onTap Add, glassProminent ] [ text "Add" ]
]
, column [ spacing 8 ]
(List.map viewItem model.items)
]
viewItem : String -> View Msg
viewItem item =
row [ spacing 8, padding 12, glass, rounded 12 ]
[ text item
, spacer
, button [ onTap (Remove item) ] [ icon [] "trash" ]
]
-- PROGRAM
main : Walnut.Program Model Msg
main =
Walnut.sandbox { init = init, update = update, view = view }
Language notes
| Area | Walnut today |
|---|---|
| Type checking | Compile time, inferred |
| Effects | Cmd / Sub via Walnut.element (effects.md): Storage, Keychain, CoreData, Notifications, Http, Time, Port |
| Views | Ui.View msg → UIKit |
| Record constructors from aliases | type alias Point = { x : Int, y : Int } also binds Point : Int -> Int -> Point |
| Custom operators | Not supported |
where / GLSL | Not supported |
| Numbers | Int and Float coexist; / and ^ are Float; + - * stay Int when both sides are Int and promote to Float when either is Float |
Constructor names are matched by their bare name at runtime, so avoid two imported types sharing a constructor name (the type checker will police this later).