language tutorial
Hello, Colloquial
Expressive, robust, and fun-to-learn, Colloquial is a statically-typed, lexically-scoped, feature-rich language implementing a hybrid functional-imperative programming paradigm with type inference that is supported by a powerful Hindley–Milner type-checker. It compiles ahead-of-time to native code via an LLVM backend. The core is functional and everything is an expression. Values are immutable by default. The language features algebraic data types, pattern matching, first-class functions, generics, higher-order functions, typeclasses. Imperative constructs (var, while, for, and mutable arrays) are available when you need them.
function main() -> Unit = {
println("Hello, Colloquial!")
}
At a glance:
- Hindley-Milner type inference with let-polymorphism.
- Algebraic data types (
enum) and pattern matching; no class inheritance. - No
nulland no exceptions —Option,Result, and the?operator. - Typeclasses for ad-hoc polymorphism, with
derivingand generic instances. - Immutable
String/List, fixed-length mutableArray, tuples, structs. - String interpolation, pipes, placeholder lambdas, named/default arguments, for-comprehensions, functional record update.
- A module-per-file system with namespaced types and re-export.
Every program needs a main
A Colloquial program lives in a file ending in .cql, and execution starts at a function called main, which takes no arguments and returns Unit — the type of “no interesting value”. There must be exactly one main, and it must have that exact shape; command-line arguments come from the args() builtin rather than from a parameter.
function main() -> Unit = {
println("this runs first");
println("and this runs second")
}
Statements inside a block are separated by semicolons. The last one does not need a trailing semicolon, though it is harmless to write one.
To run it, install the compiler and use cqlc --run — or, if you are reading this on the website, press try it! to edit and run the program in place. Either way the whole toolchain is covered in Compiling and tooling at the end.
values & expressions
Names and values
You introduce a name by writing it, an =, and a value. No keyword is required. The name is immutable: it will refer to that value for as long as it exists.
function main() -> Unit = {
greeting = "Hello";
answer = 42;
println(greeting);
println(toString(answer))
}
You may write let in front if you prefer the emphasis. It means exactly the same thing, and both forms are used freely in real code.
function main() -> Unit = {
let greeting = "Hello";
let answer = 42;
println(s"${greeting}, ${answer}")
}
When you want to change something
A name you intend to reassign must be declared with var, and reassignment uses a distinct operator, :=. Two different spellings for two different acts: = introduces a name, := updates one.
function main() -> Unit = {
var count = 0;
count := count + 1;
count := count + 1;
println(toString(count))
}
Using := on an immutable binding is a compile error, not a warning. This is the single most common way the compiler catches an accidental change:
function main() -> Unit = {
total = 10;
total := 11;
println(toString(total))
}
Reusing a name
Because bindings are immutable, introducing the same name twice does not modify anything — the second binding shadows the first, and the old value is simply no longer reachable by that name. This is a common functional style for a value that passes through several stages.
function main() -> Unit = {
text = " Hello, World ";
text = trim(text);
text = toUpper(text);
println(text)
}
Primitive types
Colloquial is statically typed, but you rarely have to say so: the compiler infers the type of nearly everything from the way you use it. These are the built-in scalar types and how their literals are written.
function main() -> Unit = {
count = 42; // Int - a signed 64-bit integer
small = 42i32; // Int32 - a signed 32-bit integer
ratio = 3.14; // Float - a 64-bit floating-point number
ready = true; // Bool - true or false
initial = 'A'; // Char - a single character
name = "Mateo"; // String - immutable text
nothing = (); // Unit - the "no interesting value" value
println(toString(count));
println(toString(small));
println(toString(ratio));
println(toString(ready));
println(toString(initial));
println(name)
}
Unit is the odd one out: it has exactly one value, written (), so it carries no information. It is the result type of anything you do for its effect rather than its value — which is why main returns it. Should you ever print one, toString(()) renders it as <Unit>.
One printing quirk is worth knowing this early: toString of a Float that happens to be a whole number drops the point, so toString(3.0) is 3 rather than 3.0. The value is still a Float — only its printed form is indistinguishable from an Int.
None of these seven names is a reserved word. Int, Int32, Float, Bool, Char, String and Unit are ordinary identifiers that the type system happens to know about, which is why they are absent from the reserved-word list.
When you want to be explicit — for documentation, or to pin down a type the compiler could not guess — write the type after a colon.
function main() -> Unit = {
let attempts: Int = 3;
let label: String = "retrying";
println(s"${label}: ${attempts}")
}
How big is an Int?
Int spans -9223372036854775808 to 9223372036854775807, and both endpoints are writable directly. A literal outside that range is a compile error rather than a value that silently wraps. Arithmetic that overflows at run time wraps around in the usual two's-complement way.
function main() -> Unit = {
biggest = 9223372036854775807;
smallest = -9223372036854775808;
println(toString(biggest));
println(toString(smallest))
}
Operators
Arithmetic works as you would expect. Integer division truncates toward zero, % is the remainder, and ** is exponentiation.
function main() -> Unit = {
println(toString(7 + 2)); // 9
println(toString(7 - 2)); // 5
println(toString(7 * 2)); // 14
println(toString(7 / 2)); // 3 - truncates toward zero
println(toString(7 % 2)); // 1 - remainder
println(toString(2 ** 10)) // 1024
}
Comparisons produce a Bool. The logical operators come in two spellings apiece — symbolic and worded — and mean exactly the same thing; pick whichever reads better in context. Both short-circuit, so the right-hand side is skipped when the answer is already known.
function main() -> Unit = {
x = 5;
println(toString(x > 0 && x < 10));
println(toString(x > 0 and x < 10));
println(toString(x < 0 || x > 3));
println(toString(x < 0 or x > 3));
println(toString(!false));
println(toString(not false))
}
+ also joins strings, so the same operator that adds numbers concatenates text.
function main() -> Unit = {
full = "Mateo" + " " + "Dailis";
println(full)
}
Precedence mostly follows the conventions you already know: ** binds tighter than * and /, which bind tighter than + and -, which bind tighter than the comparisons, which bind tighter than the logical operators. Parentheses group when you want to be unambiguous.
There is one place where the convention does not hold, and it is worth learning now rather than discovering later: a unary operator binds tighter than **. So -2 ** 2 is (-2) ** 2, which is 4 — not -(2 ** 2), which is -4. Write the parentheses when you mean the second one.
function main() -> Unit = {
println(toString(-2 ** 2)); // 4 - the minus binds first: (-2) ** 2
println(toString(-(2 ** 2))) // -4 - say so explicitly for this
}
Precedence, highest to lowest
Here is every operator in the language, tightest-binding first. Several of them you have not met yet — the ranges, the pipe, the two question marks — and each gets its own section later; they are listed now so that there is one complete table to come back to.
| Operators | Notes |
|---|---|
f(x) a[i] a.b a?.b e? | call, index, member, optional member, propagate |
-e !e not e | unary — tighter than **, so -2 ** 2 is 4 |
** | right-associative |
* / % | |
+ - | + also joins strings |
... ..< >.. >..< .. | ranges, and by for a stride |
< <= > >= | needs an Ord instance |
== != | needs an Eq instance |
&& / and | short-circuits |
|| / or | short-circuits, and binds looser than && |
|> ?? | pipe, coalesce |
:= | reassignment; only to a var |
Strings and text
A String is immutable: every operation that looks like a change actually produces a new string. Prefix a literal with s to interpolate values into it — $name for a plain name, and ${...} for any expression at all.
function main() -> Unit = {
name = "Mateo";
unread = 3;
println(s"Hello, $name!");
println(s"You have ${unread} messages.");
println(s"Tomorrow you will have ${unread + 1}.")
}
Interpolation renders a value the same way toString does, which means it works for your own types too as soon as they can be shown — see Deriving.
Working with the contents
String operations are byte-oriented and are provided as ordinary functions. Indices count from zero, and reading out of bounds stops the program rather than returning garbage.
function main() -> Unit = {
text = "Hello, World";
println(toString(length(text))); // 12
println(toString(charAt(text, 0))); // H
println(substring(text, 7, 12)); // World
println(toString(indexOf(text, "World"))); // 7
println(toString(indexOf(text, "absent"))); // -1 when not found
println(toUpper(text));
println(toLower(text));
println(trim(" padded "))
}
Characters
A Char is a single character written in single quotes. ord gives its numeric code point and chr goes back the other way.
function main() -> Unit = {
letter = 'A';
code = ord(letter);
println(toString(code)); // 65
next = chr(code + 1);
println(toString(next)) // B
}
Everything is an expression
The title of this section is a slogan; the rule behind it is precise, and short enough to state exactly. Every control form is an expression: a braced block, if/elif/else, the ternary, when, a guard block, match, while, until, repeat, for, return, and even a reassignment with := all produce a value and may be used anywhere a value is expected. Bindings and declarations are not expressions: let, var, a bare name = value, and the declaration forms function, struct, enum, type, typeclass and import introduce a name rather than producing a value. They are steps in a block or items in a file, and nothing else.
The first half of that rule is the half you will use constantly. A block in braces is an expression: it evaluates its statements in order and its value is the last expression inside it. That means you can compute something in several steps and still treat the whole thing as a single value.
function main() -> Unit = {
area = {
width = 3;
height = 4;
width * height
};
println(toString(area))
}
Note the absence of a return in that block. The final expression is the result. This is why function bodies, conditional branches, and loop bodies all look the same — they are the same thing.
A block's value is its last expression, so a block that ends in a binding has no final expression to take the value of. Such a block is legal, and its value is Unit:
function main() -> Unit = {
nothing = {
_side = 4 // ends in a binding, so there is no value to hand back
};
println(toString(nothing)) // <Unit>
}
The second half of the rule is what you will bump into. Because a binding is not an expression, you cannot write one where a value belongs — the compiler stops at the let rather than inventing a meaning for it:
function main() -> Unit = {
x = (let y = 5);
println(toString(x))
}
return does exist, for leaving a function early. Reach for it when a guard at the top of a function saves you from nesting the rest of the body inside an else.
function describe(n: Int) -> String {
if [n < 0] {
return "negative"
};
if [n == 0] {
return "zero"
};
"positive"
}
function main() -> Unit = {
println(describe(-5));
println(describe(0));
println(describe(7))
}
Things that exist only to cause an effect — assignment with :=, a while loop, a call to println — evaluate to Unit, which toString renders as <Unit>. That is the language's way of saying “this ran, and there is nothing to look at”.
One control form has a value that surprises people, and it is worth knowing that it follows from this rule rather than breaking it: a for loop is an expression too, so even without yield it hands back a list of what its body produced. Loops covers what that means in practice.
Comments and reserved words
Two mechanical facts about the source text itself, now that you have read enough of it to have wondered about them.
Comments
Line comments run to the end of the line. Block comments may be nested, so you can comment out a region that already contains a comment.
function main() -> Unit = {
// a line comment
/* a block comment,
/* which may be nested */
and continues here */
println("comments are ignored by the compiler")
}
Words you cannot use as names
Almost any identifier you can think of is available, because the language reserves a short list of words and nothing else. Twenty-six are reserved everywhere: wherever one of these appears, it is that keyword, so none of them can name anything of yours.
function let var if else for while return repeat until
match case yield struct enum typeclass instance deriving
type import as true false and or not
Ten more words look like keywords but are reserved in one position only. Everywhere else they are ordinary identifiers, so a variable called when or a function called then is perfectly legal — the parser recognizes them by where they sit, not by how they are spelled.
| Word | Special only here |
|---|---|
elif | after an if block |
then | in a guard block |
otherwise | in a when chooser |
when | immediately before a { |
by | after a range operator |
of | inside [count of value] |
computed | before a struct member |
requires | in a typeclass header |
pub | before a top-level declaration |
export | before import |
pub is the one with a wrinkle: a top-level pub is read as a visibility prefix before anything else is considered, so a top-level binding cannot be named pub. As a local name or a function name it is fine, like the other nine. The primitive type names are on neither list — Int and its siblings are ordinary identifiers too.
Choosing between branches
Conditions go in square brackets and branches are braced blocks. Since the whole thing is an expression, you can bind its result directly to a name.
function classify(x: Int) -> String = {
if [x > 0] {
"positive"
} elif [x < 0] {
"negative"
} else {
"zero"
}
}
function main() -> Unit = {
println(classify(7));
println(classify(-7));
println(classify(0))
}
You can also use it as a statement and ignore the value, which is the familiar imperative shape:
function main() -> Unit = {
hour = 9;
if [hour < 12] {
println("Good morning")
} else {
println("Good afternoon")
}
}
Three shorter forms
For the very small cases there is a ternary. It reads [condition] ? then : else and chains to the right.
function sign(x: Int) -> Int = [x > 0] ? 1 : [x < 0] ? -1 : 0
function main() -> Unit = {
println(toString(sign(42)));
println(toString(sign(-42)));
println(toString(sign(0)))
}
When you are choosing among several unrelated conditions, when lines them up in a column. otherwise is the catch-all, and the arms are separated by semicolons.
function grade(score: Int) -> String = {
when {
[score >= 90] -> "A";
[score >= 80] -> "B";
[score >= 70] -> "C";
otherwise -> "F"
}
}
function main() -> Unit = {
println(grade(95));
println(grade(83));
println(grade(12))
}
A guard block is the same idea in a more clipped style: a bare braced block whose arms use then, ending in an else.
function size(n: Int) -> String = {
{
[n > 100] then "huge";
[n > 10] then "big";
else "small"
}
}
function main() -> Unit = {
println(size(1000));
println(size(50));
println(size(3))
}
All four forms compile to the same thing. Use whichever makes the particular decision clearest — if for one or two branches, when for a column of conditions, the ternary for something that fits comfortably on one line.
functions
Functions
A function declares its parameters with types, and its result type after an arrow. When the body is a single expression, write = and the expression.
function double(x: Int) -> Int = x * 2
function main() -> Unit = {
println(toString(double(21)))
}
When the body needs several steps, use a braced block instead. Note there is no = in this form, and the last expression is the result.
function greet(name: String) -> String {
prefix = "Hello, ";
suffix = "!";
prefix + name + suffix
}
function main() -> Unit = {
println(greet("Mateo"))
}
The result type may be left out, in which case the compiler works it out from the body. Writing it down is still worthwhile on anything that another part of the program depends on — it documents your intent and turns a mistake in the body into an error at the definition rather than at the call.
function triple(x: Int) = x * 3
function main() -> Unit = {
println(toString(triple(5)))
}
Recursion
A function may call itself, and functions may be defined in any order — a function can refer to one declared further down the file.
function factorial(n: Int) -> Int = {
if [n <= 1] {
1
} else {
n * factorial(n - 1)
}
}
function main() -> Unit = {
println(toString(factorial(10)))
}
Arguments
A parameter may declare a default, which makes it optional at the call site. Callers can also name an argument, which is a good idea when the value alone would not tell a reader what it means.
function connect(host: String, port: Int = 8080, secure: Bool = false) -> String = {
scheme = [secure] ? "https" : "http";
s"${scheme}://${host}:${port}"
}
function main() -> Unit = {
println(connect("example.com"));
println(connect("example.com", 9000));
println(connect("example.com", secure = true));
println(connect("example.com", port = 443, secure = true))
}
Named arguments also let you supply a later default while leaving an earlier one alone, as the third call above does for secure.
The placeholder
Inside a call argument, _ stands for “the argument” and turns the arithmetic or comparison around it into a one-argument function. It is a compact way to write the very small functions you pass to other functions.
import Lists(map, forEach);
function main() -> Unit = {
numbers = List(1, 2, 3);
tenfold = map(numbers, _ * 10);
forEach(tenfold, (n: Int) => println(toString(n)))
}
A call argument is the only place a placeholder may appear. map(numbers, _ * 10) is fine, but trying to build a function with one on its own is a compile error, because there is no call for the new function to be an argument to. Write a lambda when you want to name the function.
function main() -> Unit = {
addOne = _ + 1;
println(toString(addOne(41)))
}
Functions as values
Functions are values. You can bind one to a name, pass it to another function, or return it. A function's type is written with the parameter types in parentheses and the result after an arrow.
function twice(f: (Int) -> Int, x: Int) -> Int = f(f(x))
function increment(n: Int) -> Int = n + 1
function main() -> Unit = {
println(toString(twice(increment, 10)))
}
An unnamed function — a lambda — can be written three ways. They are interchangeable, so use whichever fits.
function twice(f: (Int) -> Int, x: Int) -> Int = f(f(x))
function main() -> Unit = {
println(toString(twice((n) => n + 1, 10))); // parenthesised parameters
println(toString(twice(n => n + 1, 10))); // one parameter, no parens
println(toString(twice(|n| n + 1, 10))) // pipe-style parameters
}
Returning a function
A function that returns a function closes over the values in scope where it was created. Here adder builds a new function that remembers amount.
function adder(amount: Int) -> (Int) -> Int = x => x + amount
function main() -> Unit = {
addFive = adder(5);
addTen = adder(10);
println(toString(addFive(1)));
println(toString(addTen(1)))
}
Trailing lambdas
When the last argument to a function is itself a function, you may write it as a block after the call. This is what makes iteration read like a built-in construct even though it is an ordinary function call.
import Lists(forEach);
function main() -> Unit = {
List("one", "two", "three").forEach { word ->
println(word)
}
}
Chaining calls
Any function whose first parameter is the thing you are working on can be called with dot syntax. x.f(y) and f(x, y) are the same call written two ways — there is no separate notion of a method.
function scaled(x: Int, factor: Int) -> Int = x * factor
function main() -> Unit = {
println(toString(scaled(6, 7)));
println(toString(6.scaled(7)))
}
The value of this is that a sequence of transformations reads in the order it happens, left to right, instead of inside out.
import Lists(filter, map, sum);
function even(n: Int) -> Bool = n % 2 == 0
function main() -> Unit = {
total = List(1, 2, 3, 4, 5, 6).filter(even).map(_ * 10).sum();
println(toString(total))
}
The pipe operator |> does the same job for a single value: it feeds the value on its left into the function on its right.
function double(x: Int) -> Int = x * 2
function increment(x: Int) -> Int = x + 1
function main() -> Unit = {
result = 5 |> double |> increment |> double;
println(toString(result))
}
data
Tuples
A tuple groups a fixed number of values that may have different types. It is the lightest way to return more than one thing from a function without inventing a name for the combination.
function divide(a: Int, b: Int) -> (Int, Int) = (a / b, a % b)
function main() -> Unit = {
result = divide(17, 5);
(quotient, remainder) = result;
println(s"17 / 5 is ${quotient} remainder ${remainder}")
}
The type of a tuple is written the same way its values are: (Int, String) is a pair whose first element is an Int and whose second is a String. Tuples can be printed and compared directly.
function main() -> Unit = {
let entry: (String, Int) = ("Mateo", 29);
println(toString(entry));
println(toString(entry == ("Mateo", 29)))
}
Unpacking as you bind
Rather than keep the tuple and reach into it, you can name its parts in one step by writing the shape you expect on the left of the = — which is what the first example above does with (quotient, remainder).
function main() -> Unit = {
point = (3, 4);
(x, y) = point;
println(s"x is ${x} and y is ${y}")
}
Ranges
A range builds a list of integers. Which endpoints are included is part of the operator, so you read it off the symbol instead of remembering a convention or writing n - 1.
import Lists(forEach);
function display(label: String, xs: List<Int>) -> Unit = {
println(s"${label} -> ${xs}")
}
function main() -> Unit = {
display("1...5 ", 1...5); // both ends included
display("1..<5 ", 1..<5); // left included, right excluded
display("1>..5 ", 1>..5); // left excluded, right included
display("1>..<5 ", 1>..<5) // both ends excluded
}
Those four are the whole set. If you know .. from another language, note that it is not a range operator here — it was retired before 1.0 precisely because it was the one spelling whose endpoints you had to remember rather than read. Writing 1..5 is an error, and the compiler names the replacement: `..` is not a range operator; use `..<` for a half-open range (`1..<5` is 1 2 3 4) or `...` for an inclusive one (`1...5` is 1 2 3 4 5).
Add by to take a stride rather than every value. The endpoints still mean what the operator says they mean, which matters as soon as the stride lands exactly on the far end.
function main() -> Unit = {
println(toString(1...10 by 3)); // [1, 4, 7, 10] - 10 is included
println(toString(1..<10 by 3)); // [1, 4, 7] - 10 is not
println(toString(0...100 by 25)) // [0, 25, 50, 75, 100]
}
A range is an ordinary List<Int>, so anything that works on a list works on a range. Most often you will use one as the source of a loop.
The operators are shorthand for two builtins, which you can call directly when the endpoints are computed and the function form reads better. Both are half-open: range(start, end) is start..<end, and rangeStep(start, end, step) is start..<end by step.
function main() -> Unit = {
println(toString(range(1, 5))); // [1, 2, 3, 4] - same as 1..<5
println(toString(rangeStep(1, 10, 3))); // [1, 4, 7] - same as 1..<10 by 3
println(toString(1...10 by 3)) // [1, 4, 7, 10] - NOT the same
}
That last pair is the trap: rangeStep(a, b, step) is not a ... b by step. An inclusive range is the half-open one with its end moved out by one, so 1...10 by 3 is rangeStep(1, 11, 3) and not rangeStep(1, 10, 3). When in doubt, reach for the operator and let it say which endpoints you meant.
Lists
A List<T> is an immutable sequence: adding to a list produces a new list and leaves the original alone. Build one by calling List with the elements.
function main() -> Unit = {
names = List("Mateo", "Dustin", "Koissi");
println(toString(names))
}
Under the hood a list is built from two constructors — Cons, an element followed by the rest, and Nil, the empty list. You will meet them again in Pattern matching, and they are why a list can be taken apart recursively.
function total(xs: List<Int>) -> Int =
match xs {
case Cons(first, rest) => first + total(rest)
case Nil => 0
}
function main() -> Unit = {
println(toString(total(List(1, 2, 3, 4))));
println(toString(total(Nil)))
}
In practice you rarely write that recursion yourself, because the Lists module in the standard library already has the common operations. Import the ones you want by name and they read as method calls.
import Lists(map, filter, sum, length, reverse, take);
function even(n: Int) -> Bool = n % 2 == 0
function main() -> Unit = {
numbers = 1...10;
println(toString(numbers.filter(even)));
println(toString(numbers.map(n => n * n)));
println(toString(numbers.take(3)));
println(toString(numbers.reverse()));
println(toString(numbers.sum()));
println(toString(numbers.length()))
}
Arrays
An Array<T> has a fixed length, and unlike almost everything else in the language its elements can be changed in place. Reach for one when you need indexed access or in-place updates; reach for a List otherwise.
A bracketed literal builds an array. Note that this is the one place where brackets do not mean “list” — [1, 2, 3] is an Array<Int>, and [T] is another way of writing Array<T>.
function main() -> Unit = {
scores = [10, 20, 30];
println(toString(scores[0]));
println(toString(length(scores)));
scores[1] := 99;
println(toString(scores[1]))
}
Indices start at zero, and an index outside the array stops the program with a message naming the index and the length. It will never read whatever happened to be next in memory.
Building an array
There are three constructors for when you do not want to write the elements out: a fill, a tabulation that computes each element from its index, and an empty array.
function main() -> Unit = {
zeros = Array.fill(5, 0); // five copies of 0
squares = Array.tabulate(5, i => i * i); // computed from the index
nothing = Array.empty(); // length 0
println(toString(zeros));
println(toString(squares[4]));
println(toString(length(nothing)))
}
Array.fill(5, 0) reads as “five copies of zero”, and is distinct from [5, 0], which is a two-element array. There used to be a literal spelling of the same thing, [5 of 0], and an empty one, Array<Int>[]; both were retired before 1.0, because a second way to say something the same length is not worth the extra thing to learn. The compiler names the replacement if you write either: the `[n of v]` array literal was retired; use `Array.fill(n, v)` instead.
A literal may still carry its element type, written in front of the brackets: Array<Int>[1, 2, 3] is [1, 2, 3] with the type spelled out. Reach for it — or for an annotation on the binding — when there is nothing nearby for the compiler to infer the element type from, which is the usual reason an empty array needs help.
function main() -> Unit = {
grid = Array<Int>[7, 8, 9];
names: Array<String> = Array.empty();
grid[0] := 100;
println(toString(grid));
println(toString(length(names)))
}
Loops
while repeats a block for as long as its condition holds. As with conditionals, the condition goes in square brackets.
function main() -> Unit = {
var countdown = 3;
while [countdown > 0] {
println(toString(countdown));
countdown := countdown - 1
};
println("liftoff")
}
Two variations save you from restructuring a loop to get its condition in the right place. repeat … while tests at the bottom, so the body always runs at least once; until tests at the top but loops while the condition is false.
function main() -> Unit = {
var attempts = 0;
repeat {
attempts := attempts + 1;
println(s"attempt ${attempts}")
} while [attempts < 3];
var remaining = 2;
until [remaining == 0] {
println(s"${remaining} to go");
remaining := remaining - 1
}
}
A counting loop has the three-part form you would expect, with := for the step because that is a reassignment.
function main() -> Unit = {
for (var i = 0; i < 3; i := i + 1) {
println(s"i is ${i}")
}
}
Looping over a collection
To visit each element of a list, name the element and the source with <-.
function main() -> Unit = {
names = List("Mateo", "Dustin", "Koissi");
for (name <- names) {
println(s"hello, ${name}")
};
println("that is everyone")
}
One thing to know about this loop: it always collects its body's results into a list, even when you ignore them. That means it cannot be the last expression in a function returning Unit, because the function would then be returning a list. Put a statement after it, as above, or use forEach from the standard library when the loop really is the last thing you do.
import Lists(forEach);
function main() -> Unit = {
List("Mateo", "Dustin", "Koissi").forEach { name ->
println(s"hello, ${name}")
}
}
Loops that produce a value
Add yield and the loop collects its results into a list instead of discarding them. This is a comprehension, and it is usually what you want when the point of the loop is to build something.
function main() -> Unit = {
squares = for (n <- 1...5) yield n * n;
println(toString(squares))
}
A comprehension may filter with if, and may draw from more than one source — later sources restart for each value of the earlier ones, like nested loops.
function main() -> Unit = {
odds = for (n <- 1...10; if n % 2 == 1) yield n;
println(toString(odds));
grid = for (row <- 1...2; col <- 1...3) yield (row, col);
println(toString(grid))
}
your own types
Structs
A struct is a record: a fixed set of named fields, each with a type. Structs are immutable, like everything else by default. Create one by naming the type and giving every field a value.
struct Person {
name: String
id: Int
}
function main() -> Unit = {
mateo = Person { name: "Mateo", id: 101 };
println(mateo.name);
println(toString(mateo.id))
}
One field per line, as above, is the style this tutorial uses, and it is also the canonical one: fields may be divided by a newline, a comma, or a semicolon, and the three mean the same thing, but --fmt rewrites commas and semicolons to newlines, so formatted code settles on one spelling. Note that construction is not so free — a struct's fields are given with :, as in Person { name: "Mateo", id: 101 }.
Since a struct cannot be modified, changing one field means producing a new value. copy does that: it takes the fields you want to change and carries the rest across.
struct Person {
name: String
id: Int
} deriving(Show)
function main() -> Unit = {
mateo = Person { name: "Mateo", id: 101 };
reissued = mateo.copy(id = 102);
println(show(mateo));
println(show(reissued))
}
The deriving(Show) on the end is what lets those values print themselves; Deriving covers it.
Fields that compute themselves
A computed member is derived from the other fields rather than stored. You read it exactly like a field — no parentheses — and it is recalculated on each access.
struct Rectangle {
width: Int
height: Int
computed area: Int = width * height
computed shape: String = if [width == height] {
"square"
} else {
"oblong"
}
}
function main() -> Unit = {
r = Rectangle { width: 3, height: 4 };
println(toString(r.area));
println(r.shape)
}
A computed member can only read the fields — it cannot change anything, and it takes no arguments. If you need either, write an ordinary function.
Enums
An enum describes a value that is exactly one of several alternatives. Each alternative is a variant, and a variant may carry data of its own. This is the language's main tool for modelling a choice, and it replaces both the enumerated constant and the class hierarchy you might reach for elsewhere.
enum Shape {
Circle(Float),
Rectangle(Float, Float),
Empty
}
function main() -> Unit = {
round = Circle(2.0);
boxy = Rectangle(3.0, 4.0);
nothing = Empty;
println(area(round));
println(area(boxy));
println(area(nothing))
}
function area(s: Shape) -> String =
match s {
case Circle(radius) => toString(3.14159 * radius * radius)
case Rectangle(width, height) => toString(width * height)
case Empty => "0"
}
You can name a variant's fields, which documents what they mean and lets callers name them at construction. The names are for readability; the data is still positional when you take it apart. Watch the punctuation: a variant is constructed like the call it is, with = for a named field, where a struct literal uses :.
enum Shape {
Circle(radius: Float),
Rectangle(width: Float, height: Float),
Empty
} deriving(Show, Eq)
function main() -> Unit = {
a = Rectangle(3.0, 4.0);
b = Rectangle(width = 3.0, height = 4.0);
println(show(a));
println(toString(a == b))
}
Enums can refer to themselves
A variant may hold the enum being defined, which is how you describe trees, expressions, and other nested shapes.
enum Tree {
Leaf(Int),
Node(Tree, Tree)
}
function sum(t: Tree) -> Int =
match t {
case Leaf(value) => value
case Node(left, right) => sum(left) + sum(right)
}
function main() -> Unit = {
tree = Node(Leaf(1), Node(Leaf(2), Leaf(3)));
println(toString(sum(tree)))
}
Three enums are built in and you will use them constantly: Option<T>, Result<T, E>, and List<T>. They are ordinary enums with no special privileges — you could have written them yourself.
Pattern matching
match compares a value against a series of shapes and runs the first arm that fits, binding any names in the pattern as it goes. It is an expression, so it has a value.
function describe(n: Int) -> String =
match n {
case 0 => "zero"
case 1 => "one"
case -1 => "minus one"
case _ => "something else"
}
function main() -> Unit = {
println(describe(0));
println(describe(-1));
println(describe(42))
}
_ is the wildcard: it matches anything and binds nothing. A plain name also matches anything, but gives you the value.
The patterns you can write
enum Shape {
Circle(Float),
Rectangle(Float, Float),
Empty
} deriving(Show)
function report(s: Shape) -> String =
match s {
case Circle(radius) => s"circle of radius ${radius}"
case Rectangle(w, h) => s"rectangle ${w} by ${h}"
case Empty => "nothing at all"
}
function classify(n: Int) -> String =
match n {
case 1 | 2 | 3 => "small"
case 10...99 => "two digits"
case big if big > 1000 => "enormous"
case _ => "middling"
}
function pointKind(p: (Int, Int)) -> String =
match p {
case (0, 0) => "origin"
case (0, _) => "on the y axis"
case (_, 0) => "on the x axis"
case _ => "somewhere else"
}
function main() -> Unit = {
println(report(Circle(1.5)));
println(report(Rectangle(2.0, 3.0)));
println(classify(2));
println(classify(42));
println(classify(9999));
println(pointKind((0, 0)));
println(pointKind((3, 0)))
}
So the pieces are: a literal; a constructor with its contents; a tuple shape; | for “either of these”; a range; and if after a pattern to add a condition. One more is occasionally useful — @ binds the whole value while still matching inside it.
enum Shape {
Circle(Float),
Empty
} deriving(Show)
function tag(s: Shape) -> String =
match s {
case whole @ Circle(_) => s"a round one: ${whole}"
case Empty => "nothing"
}
function main() -> Unit = {
println(tag(Circle(2.0)));
println(tag(Empty))
}
Every case must be covered
A match has to account for every value its subject could be. Leave a variant out and the program does not compile — the error names a value you failed to handle. This is checked at compile time, so a match can never fall off the end at run time.
enum Colour {
Red,
Green,
Blue
}
function name(c: Colour) -> String =
match c {
case Red => "red"
case Green => "green"
}
function main() -> Unit = {
println(name(Blue))
}
For types with too many values to list — Int, String, Char, Float — finish with a wildcard or a plain name. An arm that could never be reached because an earlier arm already covers it is also an error, which catches an arm you have accidentally written twice.
absence & failure
When there might be nothing
Colloquial has no null. A value that might be absent has the type Option<T>, which is either Some(value) or None. Because the possibility is in the type, the compiler will not let you forget it — there is no way to accidentally use an absent value as though it were present.
function firstEven(xs: List<Int>) -> Option<Int> =
match xs {
case Cons(head, tail) => if [head % 2 == 0] {
Some(head)
} else {
firstEven(tail)
}
case Nil => None
}
function main() -> Unit = {
match firstEven(List(1, 3, 4, 5)) {
case Some(n) => println(s"found ${n}")
case None => println("nothing even here")
};
match firstEven(List(1, 3, 5)) {
case Some(n) => println(s"found ${n}")
case None => println("nothing even here")
}
}
Getting at the value
Matching is always available, but for the common cases there is less ceremony. if [let x = …] runs its block only when the option holds something, binding the contents to a name.
function main() -> Unit = {
stored: Option<String> = Some("Mateo");
if [let name = stored] {
println(s"hello, ${name}")
} else {
println("nobody here")
}
}
?? supplies a fallback: it evaluates to the contents when there are some, and to the right-hand side when there are not.
function main() -> Unit = {
provided: Option<String> = Some("Mateo");
missing: Option<String> = None;
println(provided ?? "anonymous");
println(missing ?? "anonymous")
}
Reaching through several options
?. follows a field only if there is something to follow. The moment any step is None, the whole chain is None — no nesting and no repeated checks.
struct Address {
city: String
}
struct User {
name: String
address: Option<Address>
}
function main() -> Unit = {
known = User { name: "Mateo", address: Some(Address { city: "Boston" }) };
unknown = User { name: "Dustin", address: None };
println(known.address?.city ?? "address unknown");
println(unknown.address?.city ?? "address unknown")
}
There is one more way to get at the contents, the postfix ?, which hands the absence back to your caller instead of dealing with it here. It works the same way for Option and for Result, so it is covered once in the next section.
When something can fail
There are no exceptions either. A function that can fail returns Result<T, E> — either Ok(value) or Err(problem). The failure is part of the signature, so a caller can see it without reading the body, and the compiler makes sure it is dealt with.
function divide(a: Int, b: Int) -> Result<Int, String> = {
if [b == 0] {
Err("cannot divide by zero")
} else {
Ok(a / b)
}
}
function main() -> Unit = {
match divide(10, 2) {
case Ok(value) => println(s"got ${value}")
case Err(reason) => println(s"failed: ${reason}")
};
match divide(10, 0) {
case Ok(value) => println(s"got ${value}")
case Err(reason) => println(s"failed: ${reason}")
}
}
Passing failure upwards
Matching every intermediate result would bury the interesting code under error handling. The ? operator does it for you: it unwraps an Ok and carries on, or returns the Err from the enclosing function immediately. It is the one piece of syntax that makes failure handling short without making it invisible.
function divide(a: Int, b: Int) -> Result<Int, String> = {
if [b == 0] {
Err("cannot divide by zero")
} else {
Ok(a / b)
}
}
function average(total: Int, count: Int, scale: Int) -> Result<Int, String> = {
mean = divide(total, count)?;
scaled = divide(mean, scale)?;
Ok(scaled)
}
function main() -> Unit = {
match average(100, 5, 2) {
case Ok(value) => println(s"average is ${value}")
case Err(reason) => println(s"failed: ${reason}")
};
match average(100, 0, 2) {
case Ok(value) => println(s"average is ${value}")
case Err(reason) => println(s"failed: ${reason}")
}
}
Read divide(total, count)? as “divide, and if that failed, stop here and hand the problem to my caller”. The three lines of average describe the successful path, and the failing path is still fully handled.
? is not Result-only. It does the same job for an Option: it unwraps a Some and carries on, or returns None from the enclosing function immediately. Either way the rule is the same — ? may only appear in a function whose own result type can carry the failure it is passing on, so an Option's ? needs a function returning an Option.
function firstEven(xs: List<Int>) -> Option<Int> =
match xs {
case Cons(head, tail) => if [head % 2 == 0] {
Some(head)
} else {
firstEven(tail)
}
case Nil => None
}
function doubleFirstEven(xs: List<Int>) -> Option<Int> = {
found = firstEven(xs)?;
Some(found * 2)
}
function main() -> Unit = {
println(toString(doubleFirstEven(List(1, 4, 5))));
println(toString(doubleFirstEven(List(1, 3, 5))))
}
abstraction
Generics
A function can work for any type by naming a type parameter in angle brackets. Inside the function that name stands for whatever type the caller used, and the compiler checks each call separately.
function firstOrElse<T>(xs: List<T>, fallback: T) -> T =
match xs {
case Cons(head, _) => head
case Nil => fallback
}
function main() -> Unit = {
println(toString(firstOrElse(List(1, 2, 3), 0)));
println(firstOrElse(List("a", "b"), "none"));
println(firstOrElse(Nil, "empty"))
}
Types can be generic too, which is how a container works for any element type.
struct Box<T> {
contents: T
}
function unwrap<T>(b: Box<T>) -> T = b.contents
function main() -> Unit = {
number = Box { contents: 42 };
text = Box { contents: "hello" };
println(toString(unwrap(number)));
println(unwrap(text))
}
Usually the compiler infers the type parameter from the arguments. When you want to state it, put it at the call site.
function identity<T>(x: T) -> T = x
function main() -> Unit = {
println(toString(identity<Int>(42)));
println(identity<String>("explicit"))
}
Naming a shape
A type alias gives a long type a short name. It is transparent — the alias and its target are the same type, and you can use either wherever the other is expected.
type Point = (Int, Int)
type Lookup = List<(String, Int)>
type Transform = (Int) -> Int
function shift(p: Point, by: Int) -> Point = {
(x, y) = p;
(x + by, y + by)
}
function main() -> Unit = {
start: Point = (1, 2);
println(toString(shift(start, 10)));
let apply: Transform = n => n * 2;
println(toString(apply(21)))
}
Typeclasses
A generic function accepts any type, which means it cannot do much with the values — it has no way to know what operations they support. A typeclass is how you say “any type that can do this”. You declare the operations, and each type opts in with an instance.
typeclass Describable<T> {
function describe(self: T) -> String;
}
struct Dog {
name: String
}
struct Robot {
serial: Int
}
instance Describable<Dog> {
function describe(self: Dog) -> String = s"a dog called ${self.name}";
}
instance Describable<Robot> {
function describe(self: Robot) -> String = s"robot #${self.serial}";
}
function introduce<T: Describable>(thing: T) -> Unit = {
println(s"This is ${describe(thing)}.")
}
function main() -> Unit = {
introduce(Dog { name: "Polo" });
introduce(Robot { serial: 7 })
}
<T: Describable> is a constraint: it lets introduce accept any type at all, provided that type has an instance. Call it with something that does not and you get a compile error at the call site, naming the missing instance.
One instance for many types
An instance can itself be generic. This one says: any list is describable, as long as its elements are.
typeclass Describable<T> {
function describe(self: T) -> String;
}
instance Describable<Int> {
function describe(self: Int) -> String = s"the number ${self}";
}
instance<T: Describable> Describable<List<T>> {
function describe(self: List<T>) -> String =
match self {
case Cons(head, Nil) => describe(head)
case Cons(head, tail) => describe(head) + ", then " + describe(tail)
case Nil => "nothing"
};
}
function main() -> Unit = {
empty: List<Int> = Nil;
println(describe(42));
println(describe(List(1, 2, 3)));
println(describe(empty))
}
Note the annotation on empty. A bare Nil does not say what it is a list of, so the compiler has no element type to find an instance for; naming the type settles it.
Building on another class
requires says that every type implementing this class must also implement another one. In exchange, a function constrained by the smaller class may use the required class's operations as well.
typeclass Named<T> {
function name(self: T) -> String;
}
typeclass Greetable<T> requires Named<T> {
function greeting(self: T) -> String;
}
struct Person {
given: String
}
instance Named<Person> {
function name(self: Person) -> String = self.given;
}
instance Greetable<Person> {
function greeting(self: Person) -> String = "Good morning";
}
function welcome<T: Greetable>(who: T) -> Unit = {
println(s"${greeting(who)}, ${name(who)}!")
}
function main() -> Unit = {
welcome(Person { given: "Mateo" })
}
Note that welcome is constrained only by Greetable, yet it calls name from Named. That is what requires buys.
The classes the compiler already knows
Four typeclasses are built in, and you constrain by them exactly as you would by your own. Show turns a value into text, Eq is behind == and !=, Ord is behind < <= > >=, and Num is behind the arithmetic operators. The first three the compiler will implement for you — that is what Deriving is about. Num is the one you cannot derive; it is there so that a generic function can say it needs numbers.
function twice<T: Num>(x: T) -> T = x + x
function main() -> Unit = {
println(toString(twice(21))); // 42
println(toString(twice(1.5))) // 3 - toString drops a whole Float's point
}
One asymmetry worth knowing: show is callable as an ordinary function on anything showable, but the other three classes are reached only through their operators. There is no eq, compare or add in scope — write a == b and a + b.
Deriving
Three typeclasses come up so often that the compiler will write the instances for you: Show for turning a value into text, Eq for == and !=, and Ord for <, <=, >, and >=. List them after the type.
struct Version {
major: Int
minor: Int
} deriving(Show, Eq, Ord)
function main() -> Unit = {
old = Version { major: 1, minor: 2 };
new = Version { major: 1, minor: 10 };
println(show(old));
println(toString(old == new));
println(toString(old < new))
}
Derived behaviour is structural and goes all the way down: fields are compared in the order they are declared, and a field that is itself a struct, an enum, a tuple, a list, or an Option is handled by the same rules.
enum Status {
Active,
Suspended(reason: String)
} deriving(Show, Eq)
struct Account {
owner: String
status: Status
tags: List<String>
} deriving(Show, Eq)
function main() -> Unit = {
a = Account {
owner: "Mateo",
status: Suspended("late payment"),
tags: List("premium", "legacy")
};
println(show(a));
println(toString(a == a))
}
Once a type can be shown, string interpolation can print it too — ${value} and toString(value) both go through the same instance. Without one, interpolation falls back to the type's name in angle brackets, which is a useful hint that you meant to add deriving(Show).
Using an operator that a type has not opted into is an error rather than a silent comparison of whatever the value happens to be made of. This program does not compile, because Version derives nothing:
struct Version {
major: Int
minor: Int
}
function main() -> Unit = {
a = Version { major: 1, minor: 0 };
b = Version { major: 2, minor: 0 };
println(toString(a < b))
}
programs in the large
Modules
One file is one module, named after the file. There is nothing to declare at the top — Temperature.cql is the module Temperature. Import it to use what it exports.
pub struct Temperature {
celsius: Float
} deriving(Show)
pub function fromCelsius(c: Float) -> Temperature = Temperature { celsius: c }
pub function fromFahrenheit(f: Float) -> Temperature =
Temperature { celsius: (f - 32.0) / 1.8 }
function round(x: Float) -> Float = x
Then, in another file:
import Temperature;
function main() -> Unit = {
boiling = Temperature.fromCelsius(100.0);
body = Temperature.fromFahrenheit(98.6);
println(show(boiling));
println(toString(body.celsius))
}
Types are namespaced by their module too, so the type above is written Temperature.Temperature when you need to name it. Module names are flat: there is one level of qualification and no nested paths.
Three ways to import
A plain import gives you qualified access, and only qualified access: import Lists; lets you write Lists.map but does not put map itself in scope. An alias shortens the qualifier. A selective import brings particular names in unqualified, which is what you want for functions you use constantly.
import Temperature;
import Temperature as T;
import Temperature(fromCelsius);
function main() -> Unit = {
a = Temperature.fromCelsius(0.0);
b = T.fromCelsius(50.0);
c = fromCelsius(100.0);
println(show(a));
println(show(b));
println(show(c))
}
What a module keeps to itself
Top-level declarations are private by default. Mark the ones that form your module's surface with pub and the rest stay internal — reaching for a private function or global from another module is a compile error, so you can refactor a private helper without wondering who depends on it. In the module above, round is private:
import Temperature;
function main() -> Unit = {
println(toString(Temperature.round(1.5)))
}
The same rule covers types. A struct, enum or type alias without pub cannot be named from another module either — and since building one counts as using it, a private type is neither nameable nor constructible from outside. Inside its own module it stays perfectly ordinary, which is the common case: most of a module's data types have no reason to be part of its surface.
That gives you a genuinely useful shape: a public type whose insides stay private. A pub struct may have a field whose type is private, because a field's type is read in the module that declared the struct, not in the module using it. Callers can hold the value, pass it around and print it — they just cannot name what is inside. It is the plainest way to publish a type while keeping its representation yours, and it is how you would build something like Map or Set yourself.
A module can also re-export another module's names with export import, which lets you assemble a single convenient import out of several smaller modules. The names arrive under the re-exporting module's qualifier: if Kitchen says export import Temperature;, then whoever imports Kitchen writes Kitchen.fromCelsius. They never learn that Temperature exists, and Temperature.fromCelsius is not in scope for them.
export import Temperature;
pub function freezing() -> Temperature.Temperature = Temperature.fromCelsius(0.0)
The standard library
The standard library is written in Colloquial and is available without any setup. Every module is container-first — the thing you are working on is the first parameter — so everything chains with dot syntax and pipes.
import Lists(map, filter, foldLeft, sum, zip, reverse);
import Sort(sort, sortBy);
import Strings(join);
function main() -> Unit = {
numbers = List(5, 3, 8, 1);
println(toString(numbers.sort()));
println(toString(numbers.map(n => n * 2)));
println(toString(numbers.filter(n => n > 3)));
println(toString(numbers.foldLeft(0, (acc: Int, n: Int) => acc + n)));
println(toString(numbers.zip(List("a", "b", "c", "d"))));
println(join(List("x", "y", "z"), ", "))
}
What is in it
- Lists — map, filter, foldLeft, foldRight, length, isEmpty, append, reverse, head, tail, take, drop, forEach, all, any, sum, product, contains, indexOf, maximum, minimum, zip, flatten, flatMap, interleave, chunk.
- Options — map, flatMap, getOrElse, orElse, isSome, isNone, filter, toList.
- Results — map, mapErr, flatMap, getOrElse, isOk, isErr, toOption.
- Strings — join, concatAll, replicate.
- Sort — sort by
Ord, sortBy a key, sortWith a comparison. All stable. - Map — persistent and immutable, keyed by
Eq: empty, insert, get, getOrElse, contains, remove, size, isEmpty, keys, values, toList, fromList. - Set — the same idea for membership: empty, insert, contains, remove, size, isEmpty, toList, fromList, union, intersect.
Two of those names exist as builtins as well. Lists.length does for a list what the builtin length does for an array or a string. Lists.indexOf answers with an Option<Int>, where the builtin indexOf on strings answers with -1 for “not found” — so when you import the list version by name, note which convention you are reading.
Map and Set are immutable: inserting returns a new collection rather than changing the one you had. That is why the example below keeps reassigning a var.
import Map;
import Options(getOrElse);
function main() -> Unit = {
var ids = Map.empty();
ids := Map.insert(ids, "Mateo", 101);
ids := Map.insert(ids, "Dustin", 204);
println(toString(Map.size(ids)));
println(toString(Map.isEmpty(ids)));
println(toString(getOrElse(Map.get(ids, "Mateo"), 0)));
println(toString(getOrElse(Map.get(ids, "Nobody"), 0)));
println(toString(Map.getOrElse(ids, "Dustin", 0))); // the same, in one call
println(toString(Map.keys(ids)));
println(toString(Map.values(ids)))
}
Looking a key up returns an Option, not a value and not an error — a missing key is an ordinary, expected outcome, so it shows up in the type. Map.getOrElse is the shorthand for the common case where you have a fallback in mind anyway.
keys and values come back as lists in matching order, so the first key goes with the first value. The order itself is whatever the map happens to hold internally rather than the order you inserted in, so sort them if you need a stable arrangement.
If you write your own module with the same name as one of these, yours wins. Nothing in the standard library is privileged.
Builtins
A smaller set of functions comes from the compiler itself rather than from a module, so they need no import at all. You have been using println and toString since the first page; this is the complete list, with types.
| Function | Type | What it does |
|---|---|---|
print | (String) -> Unit | write to stdout |
println | (String) -> Unit | write a line to stdout |
readLine | () -> String | read one line of stdin |
readAll | () -> String | read all of stdin |
toString | (a) -> String | render a value as text |
show | (a) -> String | render via a Show instance |
length | ([a]) -> Int or (String) -> Int | elements in an array, bytes in a string |
panic | (String) -> a | stop with a message |
charAt | (String, Int) -> Char | one character, bounds-checked |
substring | (String, Int, Int) -> String | a slice; the end is clamped |
indexOf | (String, String) -> Int | position, or -1 |
toUpper toLower trim | (String) -> String | case and whitespace |
ord chr | (Char) -> Int, (Int) -> Char | character and code point |
range | (Int, Int) -> List<Int> | half-open range |
rangeStep | (Int, Int, Int) -> List<Int> | range with a stride |
List | (a, a, …) -> List<a> | build a list |
Array.fill | (Int, a) -> [a] | array of copies |
Array.tabulate | (Int, (Int) -> a) -> [a] | array from an index function |
Array.empty | () -> [a] | zero-length array |
readFile | (String) -> Option<String> | whole file, if present |
writeFile | (String, String) -> Bool | write; false on failure |
args | () -> [String] | command-line arguments |
getEnv | (String) -> Option<String> | environment variable, if set |
print and println differ only in the trailing newline. A lowercase type variable such as a means the function works for any type — the same generics you saw earlier, so [a] is “an array of anything”, [T] and Array<T> being two spellings of the one type.
length is the one builtin with two types rather than one: it measures an array of anything, or a string, and nothing else — length(5) does not compile. It is also the one builtin you should know the shadowing rule for. If a binding named length is visible where you call it — a local, a parameter, one of your own module's functions, or a selective import Lists(length) — then yours is the one that runs, and the builtin's string case goes with it. A plain import Lists; changes nothing, because it brings no bare names into scope at all.
Talking to the outside world
Input and output are ordinary functions. Note the types: reading a file gives an Option<String> because the file may not be there, and writing returns a Bool saying whether it worked. Neither can fail silently.
function main() -> Unit = {
ok = writeFile("greeting.txt", "Hello from Colloquial\n");
if [not ok] {
println("could not write the file")
} else {
match readFile("greeting.txt") {
case Some(contents) => print(contents)
case None => println("could not read it back")
}
}
}
Standard input and the environment work the same way. readLine takes one line, readAll takes everything, and getEnv returns an Option because the variable may not be set.
function main() -> Unit = {
home = getEnv("HOME") ?? "(not set)";
println(s"HOME is ${home}")
}
Command-line arguments
args() returns the arguments as an Array<String>, without the program's own name. This is why main takes no parameters.
function main() -> Unit = {
given = args();
println(s"got ${length(given)} argument(s)");
for (var i = 0; i < length(given); i := i + 1) {
println(s" ${i}: ${given[i]}")
}
}
Pass arguments through --run with a -- separator, so the compiler knows which flags are yours:
$ cqlc --run echo.cql -- one two three
got 3 argument(s)
0: one
1: two
2: three
When a program gives up
Most failures belong in a Result. But some situations mean the program has already gone wrong — an index past the end of an array, a division by zero — and there is nothing sensible to return. Those stop the program with a message on standard error and a non-zero exit code. They are never a crash, a wrong answer, or silence.
$ cqlc --run oops.cql
cql: panic: array index out of bounds: index 5, length 3
The situations that panic are: an explicit call to panic; integer division or remainder by zero; an array or string index out of range, whether reading or writing; chr of a number that is not a character; and a negative exponent on an integer power.
You can stop the program yourself when a situation is genuinely impossible. panic can be used anywhere a value is expected, because it never returns.
function idOf(name: String) -> Int = {
when {
[name == "Mateo"] -> 101;
[name == "Dustin"] -> 204;
otherwise -> panic(s"no record for ${name}")
}
}
function main() -> Unit = {
println(toString(idOf("Mateo")))
}
Prefer a Result whenever the caller could reasonably do something about the problem. Save panic for the cases where carrying on would be worse than stopping.
Floating-point division by zero is not a panic: it follows the usual IEEE rules and produces an infinity or a NaN.
the toolchain
Compiling and tooling
The compiler is cqlc, and the fastest way to see a program run is --run, which compiles to a temporary executable, runs it, and cleans up after itself.
$ cqlc --run hello.cql
Hello, Colloquial!
When you want a real executable, name it with -o. The result is an ordinary native binary with no runtime to install alongside it and no virtual machine underneath it.
$ cqlc hello.cql -o hello
$ ./hello
Hello, Colloquial!
While you are still writing, --check type-checks without producing any output, which is the quickest way to ask “is this program well-formed?” Errors point at a span of your source and explain what the compiler expected.
$ cqlc --check hello.cql
The compiler carries the rest of the tools with it — there is nothing extra to install.
| Command | What it does |
|---|---|
cqlc file.cql -o prog | compile to a native executable |
cqlc --run file.cql | compile, run, and clean up |
cqlc --check file.cql | type-check only, no output |
cqlc --fmt file.cql | print the canonically formatted source |
cqlc --repl | an interactive session, no file needed |
cqlc --doc file.cql | Markdown API docs from /// comments |
cqlc --version | the compiler version |
--fmt is safe to run on anything. It normalizes indentation and spacing while keeping your line breaks and every comment, and because whitespace never reaches the parser, formatting cannot change what a program means. It works even on a file that does not compile yet.
The compiler also warns about things that are legal but probably not what you meant: a local binding you never read, code after a return that can never run, and a name that shadows one from an enclosing scope. If a binding is deliberately unused, start its name with an underscore to say so. On an unknown name, the compiler suggests the closest one it knows.
Documenting as you go
A comment starting with three slashes is a doc comment, and --doc collects them into Markdown with a signature for each declaration.
/// Convert a temperature in Fahrenheit to Celsius.
///
/// The result is not rounded.
pub function toCelsius(f: Float) -> Float = (f - 32.0) / 1.8
function main() -> Unit = {
println(toString(toCelsius(212.0)))
}