Le tour de Keal

Une demi-heure, de haut en bas. Chaque extrait ci-dessous est un vrai programme et chaque sortie est ce qu'il imprime réellement — la suite les vérifie.

1. Bonjour, monde

Un fichier est un programme : les instructions de haut niveau s'exécutent dans l'ordre, sans cérémonie préalable.

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

2. Valeurs et liaisons

val lie une fois, var peut être réaffecté. Les nombres se copient ; listes et maps se partagent. Aucune conversion numérique implicite.

chapter2.keal▶ Exécuter
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 et proc

Le mot employé dit s'il y a un résultat. Un func doit déclarer ce qu'il retourne ; un proc ne le peut pas — Unit ne s'écrit donc jamais à la main.

chapter3.keal▶ Exécuter
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. Flot de contrôle

Les accolades sont obligatoires et la valeur d'un bloc est sa dernière expression — c'est pourquoi if en produit une. unless (c) vaut if (not c).

chapter4.keal▶ Exécuter
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

Une seule construction couvre ce que d'autres langages séparent entre switch et match : pas de chute, le premier bras gagne, et c'est une expression.

chapter5.keal▶ Exécuter
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. Sûreté face à null

Un type n'admet pas null sans ?. Après un test qui prouve quelque chose sur une liaison immuable, le fait tient — et Keal le porte plus loin que la plupart.

chapter6.keal▶ Exécuter
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 et lambdas

Listes et maps sont natives, avec les méthodes d'ordre supérieur attendues, typées génériquement.

chapter7.keal▶ Exécuter
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. Enregistrements et classes

Un record est le cas données : champs immuables, égalité structurelle, déstructuration. Une class est celle qui peut changer.

chapter8.keal▶ Exécuter
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. Génériques et traits

Les génériques sont monomorphisés — pas d'effacement, pas de boxing. Un trait est une capacité qu'on peut exiger d'un paramètre de type, pas un type en soi.

chapter9.keal▶ Exécuter
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. Les huit connecteurs

Écrits en toutes lettres, à une seule précédence, si bien qu'une expression mixte doit dire ce qu'elle veut dire avec des parenthèses.

chapter10.keal▶ Exécuter
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. Les bits, en toutes lettres

Un Int, c'est 64 bits, et sept opérateurs le lisent ainsi. En toutes lettres, parce que and, or et xor appartiennent déjà à Bool. Ils ne se mélangent à rien sans parenthèses — mais ils lient plus fort que la comparaison, si bien que le test que tout le monde écrit n'en demande aucune.

chapter11.keal▶ Exécuter
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 et weak

deinit s'exécute quand la dernière référence meurt, à la frontière d'instruction suivante. weak écrit l'arête arrière d'un cycle sans la maintenir en vie — le cycle meurt quand même.

chapter12.keal▶ Exécuter
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. Code natif et C

keal build compile via C11 vers un vrai exécutable, et ce qu'il ne peut pas compiler, il le refuse en le nommant — il ne compile jamais de travers.

chapter13.keal▶ Exécuter
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

Une promesse sur le moment où le travail a lieu : le compilateur l'exécute et écrit la réponse dans le programme, sous forme de littéral. Là où il ne peut pas, il refuse en le nommant plutôt que de laisser discrètement le travail à l'exécution — et il termine toujours, car un compilateur qui ne répond jamais n'est pas un outil.

chapter14.keal▶ Exécuter
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

Un ensemble fermé de noms. Le vérificateur connaît toutes les valeurs du type, donc un when sur l'un d'eux n'a pas besoin de else — et le jour où quelqu'un ajoute une variante, chaque when qui l'a oubliée est une erreur plutôt qu'une surprise à l'exécution.

chapter15.keal▶ Exécuter
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

Un morceau de syntaxe nommé, inséré là où il est écrit. Le ! n'est pas décoratif : une macro peut affecter ce qu'on lui donne, exécuter un argument deux fois ou jamais, et laisser un return traverser jusqu'à la fonction autour — trois choses qu'un appel ne peut pas faire.

chapter16.keal▶ Exécuter
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