Algebraic effects
Effects visible in the type, composable handlers. No async/await infecting the whole call stack.
A functional language with algebraic effects and isolated fibers. No garbage collector, no borrow checker.
Designed to be written with — and by — agents.
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(())
}
}
}Effects visible in the type, composable handlers. No async/await infecting the whole call stack.
Four operators, four intents: |> applies, | maps, || flat-maps, |? filters. Each form tells you what it does before you read the function.
Perceus reference counting + isolated fibers. Memory is per-fiber; no global pauses.
Real<USD>, String<UserId>, Int<Seconds>. Information in the type, zero runtime cost.
requires, ensures, Int where >= 0. What SPARK does, without an SMT solver.
Holes (?), --holes-json, structured diagnostics. Designed for humans and agents to write together.
Three programs that show the shape of the language. All run with kai run.
# Hello, world.
#
# Every kaikai program starts at `fn main()`. `println` is the
# default-handled stdout effect — no `import` needed.
#
# $ kai run examples/quickstart/01_hello.kai
# Hello, kaikai
fn main() {
println("Hello, kaikai")
}# FizzBuzz.
#
# Sum types + pattern match. kaikai is expression-oriented: `if`,
# `match`, and blocks all return a value.
#
# $ kai run examples/quickstart/02_fizzbuzz.kai
# 1
# 2
# Fizz
# 4
# Buzz
# ...
# FizzBuzz
type Tag
= Both
| Fizz
| Buzz
| Other(Int)
fn classify(n: Int) : Tag {
if n % 15 == 0 { Both }
else if n % 3 == 0 { Fizz }
else if n % 5 == 0 { Buzz }
else { Other(n) }
}
fn label(c: Tag) : String {
match c {
Both -> "FizzBuzz"
Fizz -> "Fizz"
Buzz -> "Buzz"
Other(n) -> int_to_string(n)
}
}
fn loop(i: Int, n: Int) : Unit / Console {
# `if` without `else` returns `()` implicitly when the condition
# is false — common shape for "do work or stop the recursion".
if i <= n {
println(label(classify(i)))
loop(i + 1, n)
}
}
fn main() {
loop(1, 15)
}# Custom effect with a handler.
#
# Algebraic effects are kaikai's differentiator: a function declares
# the effects it uses in its type, and any caller has to either also
# declare them, or install a handler that provides them.
#
# Below, `Log` is a custom effect with one op, `log(msg)`. `greet`
# uses it but doesn't say HOW logging happens. `main` decides at
# call time: prefix every log with `[INFO]` and route to stdout.
#
# $ kai run examples/quickstart/04_effect.kai
# [INFO] hello, kaikai
# [INFO] hello, world
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(())
}
}
}# Pipe family: four operators, four intents.
#
# |> apply apply a function to the left-hand value
# | map map over the collection
# || flat-map map and flatten the result
# |? filter keep elements that satisfy the predicate
#
# $ kai run examples/quickstart/06_pipes.kai
# total=20
fn range_loop(i: Int, n: Int, acc: [Int]) : [Int]
= if i > n { list_reverse(acc) } else { range_loop(i + 1, n, [i, ...acc]) }
fn range(n: Int) : [Int] = range_loop(1, n, [])
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() {
let total = range(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}")
}# Units of measure: arithmetic that doesn't mix currencies.
#
# Units live in the type. The compiler rejects adding USD to EUR;
# converting between currencies requires an explicit step.
#
# $ kai run examples/quickstart/07_uom.kai
# balance=845.1 USD
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}")
}# Contracts: preconditions and postconditions in the signature.
#
# `requires` rejects callers passing invalid arguments.
# `ensures` demands the result satisfy a property.
# Inside `ensures`, the name `result` refers to the returned value.
#
# $ kai run examples/quickstart/08_contracts.kai
# 5
# 3
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 == 0 - n
{
if n >= 0 { n } else { 0 - n }
}
fn main() {
println(int_to_string(divide(10, 2))) # 5
println(int_to_string(abs(0 - 3))) # 3
}