language tutorial

Hello, Colloquial

Safe, expressive, and easy-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:

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, press try it! above and edit the program in place, or install the compiler and use cqlc --run. 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     = "Ada";          // 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.

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 = "Ada" + " " + "Lovelace";

    println(full)
}

Precedence 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.

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.

OperatorsNotes
f(x) a[i] a.b a?.b e?call, index, member, optional member, propagate
-e !e not eunary
**right-associative
* / %
+ -+ also joins strings
... ..< >.. >..<ranges
< <= > >=needs an Ord instance
== !=needs an Eq instance
&& / andshort-circuits
|| / orshort-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 = "Ada";
    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

In Colloquial almost every construct produces a value. 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.

return does exist, for leaving a function early. Reach for it when an 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. That is the language's way of saying “this ran, and there is nothing to look at”.

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. You have met several already and will meet the rest as you go; they are collected here so you can see how few there are.

function let var if elif else when otherwise then
match case for while until repeat yield return by
struct enum typeclass instance requires deriving computed
import export as pub type
true false and or not of

Two of those are only reserved in one position: export is special solely in export import, and of solely inside [count of value]. Anywhere else they are ordinary identifiers. The primitive type names are not on the list at all — 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("Ada"))
}

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

In an arithmetic or comparison expression, _ stands for “the argument” and turns the expression 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)))
}

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) = ("Ada", 36);

    println(toString(entry));
    println(toString(entry == ("Ada", 36)))
}

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 never have to remember a convention or write 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
}

Add by to take a stride rather than every value.

function main() -> Unit = {
    println(toString(1...10 by 3));
    println(toString(0...100 by 25))
}

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. Underneath, a half-open range is the builtin range(start, end) and a stride is rangeStep(start, end, step), and you can call either directly when the endpoints are computed and the function form reads better.

function main() -> Unit = {
    println(toString(range(1, 5)));            // same as 1..<5
    println(toString(rangeStep(0, 10, 3)))     // same as 0...10 by 3
}

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("Ada", "Grace", "Alan");

    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 repeat literal, a fill, and a tabulation that computes each element from its index.

function main() -> Unit = {
    zeros = [5 of 0];                          // five copies of 0
    sevens = Array.fill(3, 7);                 // three copies of 7
    squares = Array.tabulate(5, i => i * i);   // computed from the index
    nothing = Array.empty();                   // length 0

    println(toString(length(zeros)));
    println(toString(sevens[2]));
    println(toString(squares[4]));
    println(toString(length(nothing)))
}

[5 of 0] reads as “five copies of zero”. The of is what distinguishes it from [5, 0], which is a two-element array.

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("Ada", "Grace", "Alan");

    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("Ada", "Grace", "Alan").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
    age: Int
}

function main() -> Unit = {
    ada = Person { name: "Ada", age: 36 };

    println(ada.name);
    println(toString(ada.age))
}

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
    age: Int
} deriving(Show)

function main() -> Unit = {
    ada = Person { name: "Ada", age: 36 };
    older = ada.copy(age = 37);

    println(show(ada));
    println(show(older))
}

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.

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("Ada");

    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("Ada");
    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: "Ada", address: Some(Address { city: "London" }) };
    unknown = User { name: "Bob", address: None };

    println(known.address?.city ?? "address unknown");
    println(unknown.address?.city ?? "address unknown")
}

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.

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: "Rex" });
    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: "Ada" })
}

Note that welcome is constrained only by Greetable, yet it calls name from Named. That is what requires buys.

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: "Ada",
        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. 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 one 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)))
}

A module can also pass another module's names through with export import, which lets you assemble a single convenient import out of several smaller modules.

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

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 ages = Map.empty();

    ages := Map.insert(ages, "Ada", 36);
    ages := Map.insert(ages, "Grace", 45);

    println(toString(Map.size(ages)));
    println(toString(getOrElse(Map.get(ages, "Ada"), 0)));
    println(toString(getOrElse(Map.get(ages, "Nobody"), 0)))
}

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.

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.

FunctionTypeWhat it does
print(String) -> Unitwrite to stdout
println(String) -> Unitwrite a line to stdout
readLine() -> Stringread one line of stdin
readAll() -> Stringread all of stdin
toString(a) -> Stringrender a value as text
show(a) -> Stringrender via a Show instance
length(a) -> Intlength of an array or string
panic(String) -> astop with a message
charAt(String, Int) -> Charone character, bounds-checked
substring(String, Int, Int) -> Stringa slice; the end is clamped
indexOf(String, String) -> Intposition, or -1
toUpper toLower trim(String) -> Stringcase and whitespace
ord chr(Char) -> Int, (Int) -> Charcharacter 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) -> Boolwrite; 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. length is one function for both arrays and strings. A lowercase type variable such as a means the function works for any type — the same generics you saw earlier.

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 ageOf(name: String) -> Int = {
    when {
        [name == "Ada"]   -> 36;
        [name == "Grace"] -> 45;
        otherwise         -> panic(s"no record for ${name}")
    }
}

function main() -> Unit = {
    println(toString(ageOf("Ada")))
}

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.

CommandWhat it does
cqlc file.cql -o progcompile to a native executable
cqlc --run file.cqlcompile, run, and clean up
cqlc --check file.cqltype-check only, no output
cqlc --fmt file.cqlprint the canonically formatted source
cqlc --replan interactive session, no file needed
cqlc --doc file.cqlMarkdown API docs from /// comments
cqlc --versionthe 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)))
}