kaikai
  • Empezar
  • Ejemplos
  • Playground
  • El nombre
  • El libro
  • Ecosistema
  • Comunidad
  • Blog
EN

Inicio rápido

Ejemplos

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 ↓

01 Hola, mundo 02 FizzBuzz 03 Calculadora 04 Efectos 05 Concurrencia 06 Tuberías 07 Unidades de medida 08 Kinds 09 Contratos 10 Un contador sin callbacks 11 Honu contra la liebre 12 El Juego de la Vida
01

Hola, mundo

Tu primer programa: una función main y una línea que saluda. No hay nada que importar.

01_hello.kai Probar en el playground →
fn main() {
  println("Hello, kaikai")
}
Salida
$ kai run 01_hello.kaiHello, kaikai
02

FizzBuzz

El clásico, resuelto sin bucles: un tipo suma clasifica cada número y una cadena de operadores hace el recorrido.

02_fizzbuzz.kai Probar en el playground →
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
}
Salida
$ kai run 02_fizzbuzz.kai1
2
Fizz
4
Buzz
...
FizzBuzz
03

Calculadora

Una expresión aritmética es un árbol. Aquí lo defines con un tipo suma y lo recorres calzando patrones.

03_calculator.kai Probar en el playground →
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)}")
}
Salida
$ kai run 03_calculator.kai2 + 3 * 4 = 14
04

Efectos

Defines tu propio efecto. La función dice qué necesita y quien la llama decide cómo se resuelve.

04_effect.kai Probar en el playground →
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(())
    }
  }
}
Salida
$ kai run 04_effect.kai[INFO] hello, kaikai
[INFO] hello, world
05

Concurrencia

Dos fibras que se turnan: cada una avanza un paso y le cede el control a la otra.

05_concurrent.kai Probar en el playground →
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)
}
Salida
$ KAI_THREADS=1 kai run 05_concurrent.kaiA
B
A
B
A
B
06

Tuberías

Cuatro operadores para encadenar pasos: aplicar, transformar, aplanar y filtrar. Se lee de arriba hacia abajo, como una receta.

06_pipes.kai Probar en el playground →
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}")
}
Salida
$ kai run 06_pipes.kaitotal=20
07

Unidades de medida

Las unidades van en el tipo, así que el compilador no te deja sumar dólares con euros por descuido.

07_uom.kai Probar en el playground →
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}")
}
Salida
$ kai run 07_uom.kaibalance=845.1 USD
08

Kinds

Las unidades no son un caso especial del lenguaje. Aquí ves por qué, y cómo declarar un kind propio.

09_kinds.kai Probar en el playground →
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}")
}
Salida
$ kai run 09_kinds.kaipiso=12 m^2
puntaje=165 xp
09

Contratos

Lo que la función exige y lo que promete, escrito en su propia firma.

08_contracts.kai Probar en el playground →
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
}
Salida
$ kai run 08_contracts.kai5
3

WebAssembly

kaikai en el navegador

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.

10

Un contador sin callbacks

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.

En vivo Haz clic en el botón
counter.kai
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
11

Honu contra la liebre

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.

En vivo Presiona ▶ para largar
race.kai
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
12

El Juego de la Vida

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.

En vivo Clic para sembrar otro tablero
life.kai
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

Honu
kaikai

Lenguaje de programación.

Por Eduardo Díaz.

  • Empezar
  • Ejemplos
  • El nombre
  • El libro
  • Ecosistema
  • Comunidad
  • Blog
  • GitHub · kaikai
  • GitHub · kaikai-book
  • lnds.net