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.
Un fichier est un programme : les instructions de haut niveau s'exécutent dans l'ordre, sans cérémonie préalable.
println("hello, world")
val who = "Ada"
println("hello ${who}, ${1 + 2} things")hello, world hello Ada, 3 things
val lie une fois, var peut être réaffecté. Les nombres se copient ; listes et maps se partagent. Aucune conversion numérique implicite.
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]
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.
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!
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).
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
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.
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
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.
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
Listes et maps sont natives, avec les méthodes d'ordre supérieur attendues, typées génériquement.
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
Un record est le cas données : champs immuables, égalité structurelle, déstructuration. Une class est celle qui peut changer.
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
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.
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
É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.
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
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.
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
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.
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
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.
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
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.
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
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.
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
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.
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