Hola, mundo
Tu primer programa: una función main y una línea que saluda. No hay nada que importar.
fn main() {
println("Hello, kaikai")
}$ kai run 01_hello.kaiHello, kaikai Inicio rápido
Nueve programas cortos para conocer el lenguaje paso a paso. Cada uno cabe en una pantalla y te muestra una sola idea.
Al final hay tres más, compilados a WebAssembly, que corren aquí mismo, en tu navegador. Ir a ellos
Tu primer programa: una función main y una línea que saluda. No hay nada que importar.
fn main() {
println("Hello, kaikai")
}$ kai run 01_hello.kaiHello, kaikai El clásico, resuelto sin bucles: un tipo suma clasifica cada número y una cadena de operadores hace el recorrido.
type Tag
= Ambos
| Fizz
| Buzz
| Otro(Int)
# Cada rama lleva su propia condición con una guarda, así que no
# necesitas una cascada de `if / else if`.
fn clasificar(n: Int) : Tag = match n {
n if n % 15 == 0 -> Ambos
n if n % 3 == 0 -> Fizz
n if n % 5 == 0 -> Buzz
n -> Otro(n)
}
fn etiqueta(c: Tag) : String = match c {
Ambos -> "FizzBuzz"
Fizz -> "Fizz"
Buzz -> "Buzz"
# `#{expr}` inserta cualquier valor mostrable dentro del texto.
Otro(n) -> "#{n}"
}
fn main() {
[1..15]
| clasificar # transforma: [Int] -> [Tag]
| etiqueta # transforma: [Tag] -> [String]
|! println # recorre: imprime cada línea
}$ kai run 02_fizzbuzz.kai1
2
Fizz
4
Buzz
...
FizzBuzz Una expresión aritmética es un árbol. Aquí lo defines con un tipo suma y lo recorres calzando patrones.
type Expr
= Lit(Int)
| Suma(Expr, Expr)
| Mult(Expr, Expr)
| Neg(Expr)
# Cuando el cuerpo de una función es una sola expresión basta con
# `=`: sin llaves y sin `return`.
fn eval(e: Expr) : Int = match e {
Lit(n) -> n
Suma(l, r) -> eval(l) + eval(r)
Mult(l, r) -> eval(l) * eval(r)
Neg(x) -> -eval(x)
}
fn mostrar(e: Expr) : String = match e {
Lit(n) -> "#{n}"
Suma(l, r) -> "#{mostrar(l)} + #{mostrar(r)}"
Mult(l, r) -> "#{mostrar(l)} * #{mostrar(r)}"
Neg(x) -> "-#{mostrar(x)}"
}
fn main() {
let e = Suma(Lit(2), Mult(Lit(3), Lit(4)))
println("#{mostrar(e)} = #{eval(e)}")
}$ kai run 03_calculator.kai2 + 3 * 4 = 14 Defines tu propio efecto. La función dice qué necesita y quien la llama decide cómo se resuelve.
effect Log {
log(msg: String) : Unit
}
fn saludar(nombre: String) : Unit / Log {
# `#{...}` inserta el valor en el texto, sin concatenar a mano.
Log.log("hello, #{nombre}")
}
fn main() {
handle {
saludar("kaikai")
saludar("world")
} with Log {
log(msg, resume) -> {
println("[INFO] #{msg}")
resume(())
}
}
}$ kai run 04_effect.kai[INFO] hello, kaikai
[INFO] hello, world Dos fibras que se turnan: cada una avanza un paso y le cede el control a la otra.
import spawn
import loop
fn trabajador(tag: String, n: Int) : Unit / Stdout + Spawn {
loop.repeat(n) {
println(tag)
spawn.yield()
}
}
fn main() {
# Con lambda final escribes `spawn.spawn { ... }` en vez de
# `spawn.spawn(() => ...)`.
let f = spawn.spawn { trabajador("B", 3) }
trabajador("A", 3)
spawn.await(f)
}$ KAI_THREADS=1 kai run 05_concurrent.kaiA
B
A
B
A
B Cuatro operadores para encadenar pasos: aplicar, transformar, aplanar y filtrar. Se lee de arriba hacia abajo, como una receta.
fn cuadrado(n: Int) : Int = n * n
fn divisores(n: Int) : [Int] = [1, n]
fn es_par(n: Int) : Bool = n % 2 == 0
fn main() {
# `[1..4]` es un literal de rango: no hace falta un acumulador a mano.
# `[1..10..2]` agrega un paso.
let total = [1..4] # [1, 2, 3, 4]
| cuadrado # [1, 4, 9, 16] (transforma)
|| divisores # [1, 1, 1, 4, 1, 9, 1, 16] (aplana)
|? es_par # [4, 16] (filtra)
|> list.sum # 20 (aplica)
println("total=#{total}")
}$ kai run 06_pipes.kaitotal=20 Las unidades van en el tipo, así que el compilador no te deja sumar dólares con euros por descuido.
unit USD
unit EUR
fn convertir_a_usd(monto: Real<EUR>, tasa: Real<USD/EUR>) : Real<USD>
= monto * tasa
fn main() {
let salario : Real<USD> = 1000.0<USD>
let mercado : Real<USD> = 250.0<USD>
let comision : Real<USD> = 5.0<USD>
let reembolso : Real<EUR> = 91.0<EUR>
let tasa : Real<USD/EUR> = 1.10<USD/EUR>
let balance = salario - mercado - comision + convertir_a_usd(reembolso, tasa)
# 1000 - 250 - 5 + 100.1 = 845.1 USD
println("balance=#{balance}")
}$ kai run 07_uom.kaibalance=845.1 USD Las unidades no son un caso especial del lenguaje. Aquí ves por qué, y cómo declarar un kind propio.
unit m
# `Measure` está declarado sobre `AbelianGroup`, que tiene clausura
# multiplicativa: metros por metros es una cantidad real, así que
# `u^2` es una unidad que puedes nombrar. `[u: Measure]` es un
# parámetro de tipo común que recorre unidades en vez de tipos.
fn area[u: Measure](w: Real<u>, h: Real<u>) : Real<u^2> = w * h
# Como las unidades son solo un kind, la maquinaria queda abierta:
# declara el tuyo. Los puntos de experiencia se suman, pero `xp^2` no
# significa nada, así que este kind se declara sobre `Module`: un
# grupo aditivo SIN clausura multiplicativa. Una cantidad es escalar o
# lleva exactamente un habitante, y `fn sq(a: Real<xp>) : Real<xp^2>`
# se rechaza justo donde se escribe la unidad:
#
# error: unit `xp^2` does not exist: `Points` habitants have no
# products or powers
#
# Misma maquinaria que `Measure`, otra teoría, otra álgebra.
kind Points : Module with points
points xp
fn ganar(a: Real<xp>, b: Real<xp>) : Real<xp> = a + b
fn main() {
let piso = area(3.0<m>, 4.0<m>)
let puntaje = ganar(120.0<xp>, 45.0<xp>)
# Habitantes de kinds distintos tampoco unifican nunca: `3.0<m> +
# 4.0<xp>` es un error de tipo. Tu kind no puede filtrarse en el de
# nadie más.
println("piso=#{piso}")
println("puntaje=#{puntaje}")
}$ kai run 09_kinds.kaipiso=12 m^2
puntaje=165 xp Lo que la función exige y lo que promete, escrito en su propia firma.
fn dividir(a: Int, b: Int) : Int
requires b != 0
ensures result * b == a
{
a / b
}
# Valor absoluto: la postcondición garantiza un resultado no negativo
# y de la misma magnitud que el argumento.
fn abs(n: Int) : Int
ensures result >= 0
ensures result == n or result == -n
{
if n >= 0 { n } else { -n }
}
fn main() {
println("#{dividir(10, 2)}") # 5
println("#{abs(-3)}") # 3
}$ kai run 08_contracts.kai5
3 WebAssembly
Estos tres programas están escritos en kaikai y compilados a WebAssembly con kai build --target=wasm32-browser. No corren en un servidor: corren aquí, en tu navegador.
Los tres esperan eventos o el reloj sin bloquear la página, y para eso necesitan JavaScript Promise Integration: Chrome o Edge 137, Firefox 153 o Safari 27, o una versión posterior.
Un clic no llama a ninguna función tuya. Dom.next_event espera el siguiente evento como si leyeras una línea de la entrada, y el estado del contador es el argumento de una función recursiva.
import web.dom
import web.run
fn contar(boton: Node, n: Int) : Unit / Dom {
let e = Dom.next_event()
if e.kind == "click" {
Dom.set_text(boton, "+1 → #{n + 1}")
contar(boton, n + 1)
} else {
contar(boton, n)
}
}
fn main() : Unit / Js + Ffi + Clock =
run.run(() => {
let boton = Dom.create("button")
Dom.set_text(boton, "+1 → 0")
Dom.append(Dom.query("body") ?? boton, boton)
Dom.listen(boton, "click")
contar(boton, 0)
})$ kai build --target=wasm32-browser counter.kai -o counter Dos actores corren en fibras separadas y le mandan sus pasos a un juez. El juez es el único que toca la página, porque una fibra no hereda los handlers de quien la lanza. La liebre es más rápida, pero se duerme.
import web.dom
import web.run
import actor
import time
const META : Int = 24
type Paso = Paso(String, Int)
fn pista(quien: String, paso: Int) : String =
"#{string.repeat("·", paso)}#{quien}#{string.repeat("·", META - paso)}🏁"
fn corredor(juez: Pid[Paso], quien: String, ritmo: (Int) -> Int, paso: Int) : Unit / Actor[Unit] + Clock {
Actor.send(juez, Paso(quien, paso))
if paso < META {
time.sleep(time.millis(ritmo(paso)))
corredor(juez, quien, ritmo, paso + 1)
}
}
fn juez(liebre: Node, honu: Node, podio: Node, llegaron: Int) : Unit / Actor[Paso] + Dom {
if llegaron < 2 {
match Actor.receive() {
Paso(quien, paso) -> {
Dom.set_text(if quien == "🐇" { liebre } else { honu }, pista(quien, paso))
if paso == META and llegaron == 0 { Dom.set_text(podio, "🏆 #{quien}") }
juez(liebre, honu, podio, if paso == META { llegaron + 1 } else { llegaron })
}
}
}
}
# Descarta los clics que llegaron durante la carrera.
fn vaciar() : Unit / Dom = match Dom.poll_event() {
Some(_) -> vaciar()
None -> ()
}
fn carreras(boton: Node, liebre: Node, honu: Node, podio: Node) : Unit / Actor[Paso] + Dom + Spawn {
let _ = Dom.next_event()
Dom.set_text(podio, "")
let yo = Actor.self()
# La liebre es rápida, pero a mitad de camino se echa una siesta.
let _ = spawn_actor(() => corredor(yo, "🐇", p => if p == 12 { 4500 } else { 70 }, 0))
let _ = spawn_actor(() => corredor(yo, "🐢", _p => 230, 0))
juez(liebre, honu, podio, 0)
vaciar()
Dom.set_text(boton, "↻")
carreras(boton, liebre, honu, podio)
}
fn main() : Unit / Js + Ffi + Clock + Spawn =
run.run(() => with_mailbox {
let cuerpo = Dom.query("body") ?? Dom.create("div")
let boton = Dom.create("button")
let liebre = Dom.create("pre")
let honu = Dom.create("pre")
let podio = Dom.create("pre")
[boton, liebre, honu, podio] |! Dom.append(cuerpo, .)
Dom.set_text(boton, "▶")
Dom.set_text(liebre, pista("🐇", 0))
Dom.set_text(honu, pista("🐢", 0))
Dom.listen(boton, "click")
carreras(boton, liebre, honu, podio)
})$ kai build --target=wasm32-browser race.kai -o race El tablero es un Vec[Bool] inmutable: cada generación es un vector nuevo, calculado con una tubería sobre un rango. Se dibuja en un canvas con el efecto Dom.
import web.dom
import web.run
import web.timer
const ANCHO : Int = 64
const ALTO : Int = 40
const LADO : Int = 8
fn viva(g: Vec[Bool], x: Int, y: Int) : Int =
if vec_get(g, ((y + ALTO) % ALTO) * ANCHO + (x + ANCHO) % ANCHO) { 1 } else { 0 }
fn vecinas(g: Vec[Bool], x: Int, y: Int) : Int =
viva(g, x - 1, y - 1) + viva(g, x, y - 1) + viva(g, x + 1, y - 1) +
viva(g, x - 1, y) + viva(g, x + 1, y) +
viva(g, x - 1, y + 1) + viva(g, x, y + 1) + viva(g, x + 1, y + 1)
fn siguiente(g: Vec[Bool]) : Vec[Bool] =
[0..ANCHO * ALTO - 1] | (i => {
let n = vecinas(g, i % ANCHO, i / ANCHO)
n == 3 or (n == 2 and vec_get(g, i))
})
fn sembrar() : Vec[Bool] / Random =
[0..ANCHO * ALTO - 1] | (_i => Random.int_range(0, 99) < 28)
fn dibujar(c: Ctx2d, g: Vec[Bool]) : Unit / Dom {
Dom.set_fill_style(c, "#0d1117")
Dom.fill_rect(c, 0, 0, ANCHO * LADO, ALTO * LADO)
Dom.set_fill_style(c, "#ffd166")
[0..ANCHO * ALTO - 1] |? (i => vec_get(g, i))
|! Dom.fill_rect(c, (. % ANCHO) * LADO, (. / ANCHO) * LADO, LADO - 1, LADO - 1)
}
fn vivir(c: Ctx2d, g: Vec[Bool]) : Unit / Dom + Timer + Random {
dibujar(c, g)
Timer.sleep_ms(100)
match Dom.poll_event() {
Some(e) if e.kind == "click" -> vivir(c, sembrar())
_ -> vivir(c, siguiente(g))
}
}
fn main() : Unit / Js + Ffi + Clock + Random =
run.run(() => {
let lienzo = Dom.create("canvas")
Dom.set_attr(lienzo, "width", "#{ANCHO * LADO}")
Dom.set_attr(lienzo, "height", "#{ALTO * LADO}")
Dom.append(Dom.query("body") ?? lienzo, lienzo)
Dom.listen(lienzo, "click")
vivir(Dom.context_2d(lienzo), sembrar())
})$ kai build --target=wasm32-browser life.kai -o life El apéndice G del libro cuenta los dos destinos de WebAssembly, qué corre en cada uno y por qué el compilador lo sabe antes de generar nada: WebAssembly