Tour of Keal

Half an hour, top to bottom. Every snippet below is a real program and every output is what it actually prints — the suite checks them.

1. Hello, world

A file is a program: top-level statements run in order, and there is no ceremony to get through first.

chapter1.keal▶ Run
println("hello, world")
val who = "Ada"
println("hello ${who}, ${1 + 2} things")
hello, world
hello Ada, 3 things

2. Values and bindings

val binds once, var may be reassigned. Numbers copy; lists and maps are shared. There are no implicit numeric conversions.

chapter2.keal▶ Run
val name = "Ada"
var count = 0
count += 1

val n = 3
val good = n.toFloat() / 2.0
val ratio: Float = 1 / 2   // a literal adapts

val xs = [1, 2]
val ys = xs
ys.add(3)
println("${good} ${ratio} ${xs}")
1.5 0.5 [1, 2, 3]

3. func and proc

Which word you use says whether there is a result. A func must declare what it returns; a proc cannot — so Unit is never written by hand.

chapter3.keal▶ Run
func add(a: Int, b: Int): Int { a + b }

proc greet(name: String, greeting: String = "hello") {
    println("${greeting}, ${name}!")
}

println(add(2, 3))
greet("Ada")
greet("Ada", greeting = "hi")
5
hello, Ada!
hi, Ada!

4. Control flow

Braces are mandatory and a block's value is its last expression, which is why if produces one. unless (c) is if (not c).

chapter4.keal▶ Run
val n = -2
val sign = if (n < 0) { "neg" } else { "pos" }

func lengthOf(s: String?): Int {
    unless (s != null) { return 0 }
    return s.length
}

for (i in 0..3) { println(i) }
println("${sign} ${lengthOf(null)} ${lengthOf("abcd")}")
0
1
2
neg 0 4

5. when

One construct covers what other languages split between switch and match: no fall-through, first arm wins, and it is an expression.

chapter5.keal▶ Run
func describe(n: Int): String {
    return when (n) {
        0 -> "zero"
        1, 2, 3 -> "small"
        in 4..10 -> "medium"
        else -> "large"
    }
}
println(describe(2))
println(describe(7))
println(describe(99))
small
medium
large

6. Null safety

A type does not admit null unless you write ?. After a check that proves something about an immutable binding, the fact holds — and Keal carries it further than most.

chapter6.keal▶ Run
var maybe: String? = null
println(maybe?.length)
println(maybe ?: "default")

val s: String? = "abc"
if (s != null) { println(s.length) }
println(s != null and s.length > 0)
println(s != null implies s.length > 0)
null
default
3
true
true

7. Collections and lambdas

Lists and maps are built in, with the higher-order methods you expect, typed generically.

chapter7.keal▶ Run
val xs = [1, 2, 3, 4]
println(xs.map({ it * 2 }))
println(xs.filter({ it % 2 == 0 }))
println(xs.fold(0, { acc, x -> acc + x }))

val ages = {"ada": 36, "alan": 41}
for (name in ages) { println("${name} is ${ages[name]!!}") }
[2, 4, 6, 8]
[2, 4]
10
ada is 36
alan is 41

8. Records and classes

A record is the data case: immutable fields, structural equality, destructuring. A class is the one that can change.

chapter8.keal▶ Run
record Point(val x: Float, val y: Float)
val a = Point(1.0, 2.0)
val b = Point(1.0, 2.0)
println(a == b)
val Point(x, y) = a
println("${x} ${y}")

class Counter(var n: Int) {
    proc bump() { this.n += 1 }
}
val c = Counter(0)
c.bump()
println(c.n)
true
1.0 2.0
1

9. Generics and traits

Generics are monomorphised — no erasure, no boxing. A trait is a capability a type parameter can be required to have, not a type of its own.

chapter9.keal▶ Run
func firstOr<T>(xs: List<T>, fallback: T): T {
    for (x in xs) { return x }
    return fallback
}
println(firstOr([1, 2], 0))
println(firstOr(["a"], "z"))

func total<T: Add>(xs: List<T>, zero: T): T {
    var acc = zero
    for (x in xs) { acc = acc + x }
    return acc
}
println(total([1, 2, 3], 0))
1
a
6

10. The eight connectives

Written as words, at one flat precedence, so a mixed expression must say what it means with parentheses.

chapter10.keal▶ Run
val a = true
val b = false
println(a and b)
println(a or b)
println(a xor b)
println(a nand b)
println(a nor b)
println(a xnor b)
println(a implies b)
println(not a)
false
true
true
true
false
false
false
false

11. Bits, in words

An Int is 64 bits, and seven operators read it as those. Words, because and, or and xor already belong to Bool. They mix with nothing without parentheses — but they bind tighter than comparison, so the test everybody writes needs none.

chapter11.keal▶ Run
val argb = (255 shl 24) bor (16 shl 16) bor (32 shl 8) bor 64
println((argb ushr 16) band 0xFF)
println(argb band 0xFF)
println(0xF0 bxor 0xFF)
println(bnot 0)
val flag = 0x22
println(flag band 2 != 0)
16
64
15
-1
true

12. deinit and weak

deinit runs when the last reference dies, at the next statement boundary. weak writes the back edge of a cycle without holding it alive, so the cycle still dies.

chapter12.keal▶ Run
var freed = 0
class Item(val id: Int) {
    weak var owner: Owner? = null
    proc deinit() { freed += 1 }
}
class Owner(val id: Int) {
    var held: Item? = null
    proc deinit() { freed += 1 }
}
proc pair() {
    val o = Owner(1)
    val it = Item(2)
    o.held = it
    it.owner = o
}
pair()
println("freed ${freed}")
freed 2

13. Native code and C

keal build compiles through C11 to a real executable, and what it cannot compile it refuses by name — it never mis-compiles.

chapter13.keal▶ Run
native """
#include <math.h>
static double keal_hypot(double a, double b) { return hypot(a, b); }
"""

extern func hypot(a: Float, b: Float): Float = "keal_hypot"

println(hypot(3.0, 4.0))
5.0

14. constexpr

A promise about when the work happens: the compiler runs it and writes the answer into the program as a literal. Where it cannot, it refuses by name rather than quietly leaving the work for run time — and it always finishes, because a compiler that never answers is not a tool.

chapter14.keal▶ Run
constexpr func squares(n: Int): List<Int> {
    var out: List<Int> = []
    for (i in 1..n) { out.add(i * i) }
    return out
}

constexpr val KB = 1024
constexpr val TABLE: List<Int> = squares(8)
println("${KB * KB} ${TABLE.size} ${TABLE[6]}")
1048576 7 49

15. enum

A closed set of names. The checker knows every value the type has, so a when over one needs no else — and the day somebody adds a variant, every when that forgot it is an error rather than a surprise at run time.

chapter15.keal▶ Run
enum Suit { Hearts, Diamonds, Clubs, Spades }

func isRed(s: Suit): Bool {
    return when (s) {
        Suit.Hearts, Suit.Diamonds -> true
        Suit.Clubs, Suit.Spades -> false
    }
}
println("${Suit.Hearts} ${isRed(Suit.Hearts)} ${isRed(Suit.Spades)}")
Hearts true false

16. Macros

A named piece of syntax, spliced where it is written. The ! is not decoration: a macro may assign to what it was given, run an argument twice or never, and let a return pass through to the function around it — three things a call cannot do.

chapter16.keal▶ Run
macro swap(a, b) {
    val held = a
    a = b
    b = held
}

macro guard(cond, fallback) {
    unless (cond) { return fallback }
}

func describe(n: Int): String {
    guard!(n > 0, "not positive")
    return "ok"
}

var p = 1
var q = 2
swap!(p, q)
println("${p} ${q} ${describe(-3)} ${describe(7)}")
2 1 not positive ok