kaikai
  • Get started
  • Examples
  • Playground
  • The name
  • The book
  • Ecosystem
  • Community
  • Blog
ES

Quickstart

Examples

Nine short programs to get to know the language one step at a time. Each fits on a screen and shows you a single idea.

At the end there are three more, compiled to WebAssembly, that run right here in your browser. Jump to them ↓

01 Hello, world 02 FizzBuzz 03 Calculator 04 Effects 05 Concurrency 06 Pipes 07 Units of measure 08 Kinds 09 Contracts 10 A counter without callbacks 11 Honu and the hare 12 The Game of Life
01

Hello, world

Your first program: a main function and one line that says hello. Nothing to import.

01_hello.kai Try it in the playground →
fn main() {
  println("Hello, kaikai")
}
Output
$ kai run 01_hello.kaiHello, kaikai
02

FizzBuzz

The classic, solved without loops: a sum type classifies each number and a chain of operators does the walking.

02_fizzbuzz.kai Try it in the playground →
type Tag
  = Both
  | Fizz
  | Buzz
  | Other(Int)

# Guards let every arm carry its own condition, so the cascade of
# `if / else if` collapses into the match itself.
fn classify(n: Int) : Tag = match n {
  n if n % 15 == 0 -> Both
  n if n % 3 == 0  -> Fizz
  n if n % 5 == 0  -> Buzz
  n                -> Other(n)
}

fn label(c: Tag) : String = match c {
  Both     -> "FizzBuzz"
  Fizz     -> "Fizz"
  Buzz     -> "Buzz"
  # String interpolation: `#{expr}` renders any displayable value.
  Other(n) -> "#{n}"
}

fn main() {
  [1..15]
    | classify  # map: [Int] -> [Tag]
    | label     # map: [Tag] -> [String]
    |! println  # each: print each line
}
Output
$ kai run 02_fizzbuzz.kai1
2
Fizz
4
Buzz
...
FizzBuzz
03

Calculator

An arithmetic expression is a tree. Here you define it with a sum type and walk it by pattern matching.

03_calculator.kai Try it in the playground →
type Expr
  = Lit(Int)
  | Add(Expr, Expr)
  | Mul(Expr, Expr)
  | Neg(Expr)

# A function whose whole body is one expression uses `=`; no braces,
# no `return`.
fn eval(e: Expr) : Int = match e {
  Lit(n)    -> n
  Add(l, r) -> eval(l) + eval(r)
  Mul(l, r) -> eval(l) * eval(r)
  Neg(x)    -> -eval(x)
}

fn show(e: Expr) : String = match e {
  Lit(n)    -> "#{n}"
  Add(l, r) -> "#{show(l)} + #{show(r)}"
  Mul(l, r) -> "#{show(l)} * #{show(r)}"
  Neg(x)    -> "-#{show(x)}"
}

fn main() {
  let e = Add(Lit(2), Mul(Lit(3), Lit(4)))
  println("#{show(e)} = #{eval(e)}")
}
Output
$ kai run 03_calculator.kai2 + 3 * 4 = 14
04

Effects

You define your own effect. The function says what it needs and the caller decides how it is resolved.

04_effect.kai Try it in the playground →
effect Log {
  log(msg: String) : Unit
}

fn greet(name: String) : Unit / Log {
  # `#{...}` interpolates into the string, with no manual concatenation.
  Log.log("hello, #{name}")
}

fn main() {
  handle {
    greet("kaikai")
    greet("world")
  } with Log {
    log(msg, resume) -> {
      println("[INFO] #{msg}")
      resume(())
    }
  }
}
Output
$ kai run 04_effect.kai[INFO] hello, kaikai
[INFO] hello, world
05

Concurrency

Two fibers taking turns: each one moves a step forward and hands control to the other.

05_concurrent.kai Try it in the playground →
import spawn
import loop

fn worker(tag: String, n: Int) : Unit / Stdout + Spawn {
  loop.repeat(n) {
    println(tag)
    spawn.yield()
  }
}

fn main() {
  # Trailing-lambda form: `spawn.spawn { ... }` instead of
  # `spawn.spawn(() => ...)`.
  let f = spawn.spawn { worker("B", 3) }
  worker("A", 3)
  spawn.await(f)
}
Output
$ KAI_THREADS=1 kai run 05_concurrent.kaiA
B
A
B
A
B
06

Pipes

Four operators for chaining steps: apply, map, flat-map and filter. It reads top to bottom, like a recipe.

06_pipes.kai Try it in the playground →
fn square(n: Int) : Int = n * n

fn divisors(n: Int) : [Int] = [1, n]

fn is_even(n: Int) : Bool = n % 2 == 0

fn main() {
  # `[1..4]` is a range literal: no hand-written accumulator loop.
  # `[1..10..2]` adds a step.
  let total = [1..4]                  # [1, 2, 3, 4]
    | square                          # [1, 4, 9, 16]      (map)
    || divisors                       # [1, 1, 1, 4, 1, 9, 1, 16] (flat-map)
    |? is_even                        # [4, 16]            (filter)
    |> list.sum                       # 20                 (apply)

  println("total=#{total}")
}
Output
$ kai run 06_pipes.kaitotal=20
07

Units of measure

Units live in the type, so the compiler will not let you add dollars to euros by accident.

07_uom.kai Try it in the playground →
unit USD
unit EUR

fn to_usd(amount: Real<EUR>, rate: Real<USD/EUR>) : Real<USD>
  = amount * rate

fn main() {
  let salary    : Real<USD> = 1000.0<USD>
  let groceries : Real<USD> =  250.0<USD>
  let fee       : Real<USD> =    5.0<USD>
  let refund    : Real<EUR> =   91.0<EUR>
  let rate      : Real<USD/EUR> = 1.10<USD/EUR>

  let balance = salary - groceries - fee + to_usd(refund, rate)

  # 1000 - 250 - 5 + 100.1 = 845.1 USD
  println("balance=#{balance}")
}
Output
$ kai run 07_uom.kaibalance=845.1 USD
08

Kinds

Units are not a special case in the language. Here you see why, and how to declare a kind of your own.

09_kinds.kai Try it in the playground →
unit m

# `Measure` is declared over `AbelianGroup`, which has multiplicative
# closure: metres times metres is a real quantity, so `u^2` is a unit
# you can name. `[u: Measure]` is an ordinary type parameter that
# ranges over units instead of over types.
fn area[u: Measure](w: Real<u>, h: Real<u>) : Real<u^2> = w * h

# Because units are only a kind, the machinery is open: declare your
# own. Experience points add up, but `xp^2` is meaningless, so this
# kind is declared over `Module`: an additive group with NO
# multiplicative closure. A quantity is either scalar or carries
# exactly one habitant, and `fn sq(a: Real<xp>) : Real<xp^2>` is
# rejected at the point the unit is written:
#
#   error: unit `xp^2` does not exist: `Points` habitants have no
#          products or powers
#
# Same machinery as `Measure`, different theory, different algebra.
kind Points : Module with points
points xp

fn gain(a: Real<xp>, b: Real<xp>) : Real<xp> = a + b

fn main() {
  let floor = area(3.0<m>, 4.0<m>)
  let score = gain(120.0<xp>, 45.0<xp>)

  # Habitants of different kinds never unify either: `3.0<m> +
  # 4.0<xp>` is a type error. Your kind cannot leak into anyone
  # else's.
  println("floor=#{floor}")
  println("score=#{score}")
}
Output
$ kai run 09_kinds.kaifloor=12 m^2
score=165 xp
09

Contracts

What a function demands and what it promises, written in its own signature.

08_contracts.kai Try it in the playground →
fn divide(a: Int, b: Int) : Int
  requires b != 0
  ensures  result * b == a
{
  a / b
}

# Absolute value: the postcondition guarantees a non-negative result
# of the same magnitude as the argument.
fn abs(n: Int) : Int
  ensures result >= 0
  ensures result == n or result == -n
{
  if n >= 0 { n } else { -n }
}

fn main() {
  println("#{divide(10, 2)}")   # 5
  println("#{abs(-3)}")         # 3
}
Output
$ kai run 08_contracts.kai5
3

WebAssembly

kaikai in the browser

These three programs are written in kaikai and compiled to WebAssembly with kai build --target=wasm32-browser. They don’t run on a server: they run right here, in your browser.

All three wait on events or the clock without blocking the page, which takes JavaScript Promise Integration: Chrome or Edge 137, Firefox 153 or Safari 27, or later.

10

A counter without callbacks

A click doesn’t call any function of yours. Dom.next_event waits for the next event the way you’d read a line of input, and the counter’s state is the argument of a recursive function.

Live Click the button
counter.kai
import web.dom
import web.run

fn count(button: Node, n: Int) : Unit / Dom {
  let e = Dom.next_event()
  if e.kind == "click" {
    Dom.set_text(button, "+1 → #{n + 1}")
    count(button, n + 1)
  } else {
    count(button, n)
  }
}

fn main() : Unit / Js + Ffi + Clock =
  run.run(() => {
    let button = Dom.create("button")
    Dom.set_text(button, "+1 → 0")
    Dom.append(Dom.query("body") ?? button, button)
    Dom.listen(button, "click")
    count(button, 0)
  })
$ kai build --target=wasm32-browser counter.kai -o counter
11

Honu and the hare

Two actors run on separate fibers and report their steps to a judge. The judge is the only one that touches the page, because a fiber doesn’t inherit the handlers of whoever spawned it. The hare is faster, but it falls asleep.

Live Press ▶ to start
race.kai
import web.dom
import web.run
import actor
import time

const FINISH : Int = 24

type Step = Step(String, Int)

fn track(who: String, step: Int) : String =
  "#{string.repeat("·", step)}#{who}#{string.repeat("·", FINISH - step)}🏁"

fn runner(judge: Pid[Step], who: String, pace: (Int) -> Int, step: Int) : Unit / Actor[Unit] + Clock {
  Actor.send(judge, Step(who, step))
  if step < FINISH {
    time.sleep(time.millis(pace(step)))
    runner(judge, who, pace, step + 1)
  }
}

fn judge(hare: Node, honu: Node, podium: Node, arrived: Int) : Unit / Actor[Step] + Dom {
  if arrived < 2 {
    match Actor.receive() {
      Step(who, step) -> {
        Dom.set_text(if who == "🐇" { hare } else { honu }, track(who, step))
        if step == FINISH and arrived == 0 { Dom.set_text(podium, "🏆 #{who}") }
        judge(hare, honu, podium, if step == FINISH { arrived + 1 } else { arrived })
      }
    }
  }
}

# Drops the clicks that came in during the race.
fn drain() : Unit / Dom = match Dom.poll_event() {
  Some(_) -> drain()
  None -> ()
}

fn races(button: Node, hare: Node, honu: Node, podium: Node) : Unit / Actor[Step] + Dom + Spawn {
  let _ = Dom.next_event()
  Dom.set_text(podium, "")
  let me = Actor.self()
  # The hare is fast, but halfway there it takes a nap.
  let _ = spawn_actor(() => runner(me, "🐇", s => if s == 12 { 4500 } else { 70 }, 0))
  let _ = spawn_actor(() => runner(me, "🐢", _s => 230, 0))
  judge(hare, honu, podium, 0)
  drain()
  Dom.set_text(button, "↻")
  races(button, hare, honu, podium)
}

fn main() : Unit / Js + Ffi + Clock + Spawn =
  run.run(() => with_mailbox {
    let body = Dom.query("body") ?? Dom.create("div")
    let button = Dom.create("button")
    let hare = Dom.create("pre")
    let honu = Dom.create("pre")
    let podium = Dom.create("pre")
    [button, hare, honu, podium] |! Dom.append(body, .)
    Dom.set_text(button, "▶")
    Dom.set_text(hare, track("🐇", 0))
    Dom.set_text(honu, track("🐢", 0))
    Dom.listen(button, "click")
    races(button, hare, honu, podium)
  })
$ kai build --target=wasm32-browser race.kai -o race
12

The Game of Life

The board is an immutable Vec[Bool]: each generation is a new vector, computed with a pipe over a range. It’s drawn on a canvas through the Dom effect.

Live Click to seed a new board
life.kai
import web.dom
import web.run
import web.timer

const WIDTH : Int = 64
const HEIGHT : Int = 40
const SIDE : Int = 8

fn alive(g: Vec[Bool], x: Int, y: Int) : Int =
  if vec_get(g, ((y + HEIGHT) % HEIGHT) * WIDTH + (x + WIDTH) % WIDTH) { 1 } else { 0 }

fn neighbors(g: Vec[Bool], x: Int, y: Int) : Int =
  alive(g, x - 1, y - 1) + alive(g, x, y - 1) + alive(g, x + 1, y - 1) +
  alive(g, x - 1, y) + alive(g, x + 1, y) +
  alive(g, x - 1, y + 1) + alive(g, x, y + 1) + alive(g, x + 1, y + 1)

fn next(g: Vec[Bool]) : Vec[Bool] =
  [0..WIDTH * HEIGHT - 1] | (i => {
    let n = neighbors(g, i % WIDTH, i / WIDTH)
    n == 3 or (n == 2 and vec_get(g, i))
  })

fn seed() : Vec[Bool] / Random =
  [0..WIDTH * HEIGHT - 1] | (_i => Random.int_range(0, 99) < 28)

fn draw(c: Ctx2d, g: Vec[Bool]) : Unit / Dom {
  Dom.set_fill_style(c, "#0d1117")
  Dom.fill_rect(c, 0, 0, WIDTH * SIDE, HEIGHT * SIDE)
  Dom.set_fill_style(c, "#ffd166")
  [0..WIDTH * HEIGHT - 1] |? (i => vec_get(g, i))
    |! Dom.fill_rect(c, (. % WIDTH) * SIDE, (. / WIDTH) * SIDE, SIDE - 1, SIDE - 1)
}

fn live(c: Ctx2d, g: Vec[Bool]) : Unit / Dom + Timer + Random {
  draw(c, g)
  Timer.sleep_ms(100)
  match Dom.poll_event() {
    Some(e) if e.kind == "click" -> live(c, seed())
    _ -> live(c, next(g))
  }
}

fn main() : Unit / Js + Ffi + Clock + Random =
  run.run(() => {
    let canvas = Dom.create("canvas")
    Dom.set_attr(canvas, "width", "#{WIDTH * SIDE}")
    Dom.set_attr(canvas, "height", "#{HEIGHT * SIDE}")
    Dom.append(Dom.query("body") ?? canvas, canvas)
    Dom.listen(canvas, "click")
    live(Dom.context_2d(canvas), seed())
  })
$ kai build --target=wasm32-browser life.kai -o life

Appendix G of the book covers both WebAssembly targets, what runs on each, and how the compiler knows before it generates anything: WebAssembly

Honu
kaikai

Programming language.

By Eduardo Díaz.

  • Get started
  • Examples
  • The name
  • The book
  • Ecosystem
  • Community
  • Blog
  • GitHub · kaikai
  • GitHub · kaikai-book
  • lnds.net