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:

  1. Top-level declarations start at column 1.
  2. An expression continues as long as its lines are indented deeper than the construct that opened it.
  3. case branches align with each other; each branch body is indented past its pattern.
  4. let bindings align with each other; in follows 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

AreaWalnut today
Type checkingCompile time, inferred
EffectsCmd / Sub via Walnut.element (effects.md): Storage, Keychain, CoreData, Notifications, Http, Time, Port
ViewsUi.View msg → UIKit
Record constructors from aliasestype alias Point = { x : Int, y : Int } also binds Point : Int -> Int -> Point
Custom operatorsNot supported
where / GLSLNot supported
NumbersInt 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).