Lenguaje funcional con efectos algebraicos y fibras aisladas. Sin recolector de basura, sin borrow checker.

Diseñado para que humanos y agentes lo escriban juntos.

¿por qué este nombre?

effect.kai
effect Log {
  log(msg: String) : Unit
}

fn greet(name: String) : Unit / Log {
  Log.log("hello, #{name}")
}

fn main() {
  handle {
    greet("kaikai")
    greet("world")
  } with Log {
    log(msg, resume) -> {
      println("[INFO] #{msg}")
      resume(())
    }
  }
}

Por qué kaikai

Efectos algebraicos

Efectos visibles en el tipo, handlers compositivos. Sin async/await que se propaga por toda la pila de llamadas.

Familia de pipes

Cuatro operadores, cuatro intenciones: |> aplica, | mapea, || aplana, |? filtra. Cada forma dice qué hace antes de leer la función.

Sin GC, sin borrow checker

Perceus reference counting + fibras aisladas. La memoria es por-fibra; no hay pausas globales.

Kinds: unidades, monedas, regiones

Real<m/s> para medidas, monedas que no se mezclan, arenas con region { }. Información en el tipo, costo cero en runtime.

Contratos y refinements

requires, ensures, Int where >= 0. Lo que hace SPARK, sin SMT solver.

Diálogo con el compilador

Holes (?), --holes-json, diagnósticos JSON. Diseñado para que humanos y agentes escriban juntos.

Ejemplos

Siete programas que muestran la forma del lenguaje. Todos se ejecutan con kai run.

01_hello.kai
# Hola, mundo.
#
# Todo programa kaikai empieza en `fn main()`. `println` es el efecto
# de salida estándar, con handler por defecto — no requiere `import`.
#
#   $ kai run examples/quickstart/01_hello.kai
#   Hello, kaikai

fn main() {
  println("Hello, kaikai")
}
02_fizzbuzz.kai
# FizzBuzz.
#
# Tipos suma + calce de patrones, expresados como pipeline en vez de
# una recursión escrita a mano: `[1..15]` es un literal de rango, `|`
# mapea sobre la lista y `|>` aplica una función a la lista completa.
#
#   $ kai run examples/quickstart/02_fizzbuzz.kai
#   1
#   2
#   Fizz
#   4
#   Buzz
#   ...
#   FizzBuzz

type Tag
  = Ambos
  | Fizz
  | Buzz
  | Otro(Int)

# Las guardas permiten que cada rama lleve su propia condición, así la
# cascada de `if / else if` se colapsa dentro del match.
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"
  # Interpolación de strings: `#{expr}` renderiza cualquier valor
  # mostrable.
  Otro(n)  -> "#{n}"
}

fn main() {
  [1..15]
    | clasificar              # map: [Int] -> [Tag]
    | etiqueta                # map: [Tag] -> [String]
    |> list.foreach(println)  # apply: imprime cada línea
}
04_effect.kai
# Efecto propio con su handler.
#
# Los efectos algebraicos son el diferenciador de kaikai: una función
# declara en su tipo los efectos que usa, y quien la llame debe o bien
# declararlos también, o instalar un handler que los provea.
#
# Abajo, `Log` es un efecto propio con una sola operación, `log(msg)`.
# `saludar` la usa pero no dice CÓMO se registra. `main` lo decide al
# momento de llamar: prefija cada mensaje con `[INFO]` y lo manda a la
# salida estándar.
#
#   $ kai run examples/quickstart/04_effect.kai
#   [INFO] hello, kaikai
#   [INFO] hello, world

effect Log {
  log(msg: String) : Unit
}

fn saludar(nombre: String) : Unit / Log {
  # `#{...}` interpola dentro del string — sin concatenación manual.
  Log.log("hello, #{nombre}")
}

fn main() {
  handle {
    saludar("kaikai")
    saludar("world")
  } with Log {
    log(msg, resume) -> {
      println("[INFO] #{msg}")
      resume(())
    }
  }
}
06_pipes.kai
# Familia de pipes: cuatro operadores, cuatro intenciones.
#
#   |>   apply       aplica una función al valor de la izquierda
#   |    map         mapea sobre la colección
#   ||   flat-map    mapea y aplana el resultado
#   |?   filter      conserva los elementos que cumplen el predicado
#
#   $ kai run examples/quickstart/06_pipes.kai
#   total=20

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 — sin acumulador escrito a mano.
  # `[1..10..2]` agrega un paso.
  let total = [1..4]                  # [1, 2, 3, 4]
    | cuadrado                        # [1, 4, 9, 16]      (map)
    || divisores                      # [1, 1, 1, 4, 1, 9, 1, 16] (flat-map)
    |? es_par                         # [4, 16]            (filter)
    |> list.sum                       # 20                 (apply)

  println("total=#{total}")
}
07_uom.kai
# Unidades de medida: aritmética que no confunde monedas.
#
# Las unidades viven en el tipo. El compilador rechaza sumar USD
# con EUR; convertir entre monedas requiere un paso explícito.
#
# `unit` no es una función atornillada al lenguaje: mints un habitante
# del kind `Measure`. El ejemplo de kinds muestra qué gana con eso.
#
#   $ kai run examples/quickstart/07_uom.kai
#   balance=845.1 USD

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}")
}
09_kinds.kai
# Kinds: las unidades de medida no son un caso especial.
#
# Los tipos clasifican valores; los kinds clasifican tipos.
#
#   42     : Int       un valor,   y su tipo
#   Int    : Type      un tipo,    y su kind
#   m      : Measure   una unidad, y su kind
#
# Entonces `unit m` no es sintaxis especial atornillada al compilador.
# Mints un habitante del kind `Measure`, exactamente como `type` mints
# uno de `Type` y `effect` uno de `Effect`. Las unidades de medida son
# solo el kind `Measure` — nada más.
#
# Todo kind se declara sobre una *teoría*, y la teoría es la que
# decide el álgebra que obedecen sus habitantes. Esa elección es toda
# la superficie de diseño: se escoge para calzar con el dominio.
#
#   $ kai run examples/quickstart/09_kinds.kai
#   piso=12 m^2
#   puntaje=165 xp

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}")
}
08_contracts.kai
# Contratos: precondiciones y postcondiciones en la firma.
#
# `requires` impide llamar a la función con argumentos inválidos.
# `ensures` exige que el resultado cumpla con una propiedad.
# Dentro de `ensures`, el nombre `result` es el valor devuelto.
#
#   $ kai run examples/quickstart/08_contracts.kai
#   5
#   3

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
}