Skip to content

Five-minute tour ​

Each step is one complete idea, and every sample is Core v0, the subset the self-hosted compiler is written in.

1. Names, and what they may do ​

talor
const LIMIT: i64 = 10;          // module level, computed at compile time

fn fibonacci(n: i64): i64 {
    if n < 2 { return n; }
    let prev: i64 = 0;          // a local the body writes is mutable
    let curr: i64 = 1;
    let i: i64 = 2;
    while i <= n {
        let next = prev + curr; // never written, so never mutable
        prev = curr;
        curr = next;
        i += 1;
    }
    curr                        // the tail expression is the value
}

if, while and blocks need no parentheses around their condition, but braces are mandatory on every body. A function's value is its last expression, without ;.

2. Text ​

talor
fn describe(n: i64): string {
    let size = if n < 10 { "small" } else if n < 100 { "medium" } else { "large" };
    let parity = { let r = n % 2; if r == 0 { "even" } else { "odd" } };
    `${n} is ${size} and ${parity}`
}

if and a block are expressions. A template literal is the only formatting mechanism: each ${...} holds an expression, and the pieces are concatenated into a string.

3. One value, one of several shapes ​

talor
use std.math.{PI};

variant Shape {
    Circle(f64),
    Rect { w: f64, h: f64 },
    Dot,
}

fn area(s: Shape): f64 {
    match s {
        Shape.Circle(r) => PI * r.powi(2),
        Shape.Rect { w, h } => w * h,
        Shape.Dot => 0.0,
    }
}

A variant is a tagged union: a value is exactly one case, each case carries its own data, and the only way to read it is a match, which must be exhaustive. Member access is a dot, always: Shape.Circle(1.0). There is no ::.

4. Absence and failure are values ​

talor
fn parse_count(s: string): Result<i64, string> {
    match s.to_i64() {
        Some(n) => Ok(n * 2),
        None => Err(`not a number: ${s}`),
    }
}

fn double_or_fail(s: string): Result<i64, string> {
    let n = parse_count(s)?;    // returns the Err from here on Err
    Ok(n)
}

There is no null and no exception: absence is Option<T> (or T?), failure is Result<T, E>, and ? propagates.

5. What a function does to your values ​

talor
use std.math.{PI};

// Shape and area, as step 3 wrote them.
variant Shape {
    Circle(f64),
    Rect { w: f64, h: f64 },
    Dot,
}

fn area(s: Shape): f64 {
    match s {
        Shape.Circle(r) => PI * r.powi(2),
        Shape.Rect { w, h } => w * h,
        Shape.Dot => 0.0,
    }
}

fn append(dst: Array<i64>, src: Array<i64>) {
    for v in src {
        dst.push(v);
    }
}

fn main(): i32 {
    let shapes: Array<Shape> = [Shape.Circle(2.0), Shape.Rect { w: 3.0, h: 4.0 }];
    let areas: Array<i64> = [];
    for s in shapes {
        areas.push(area(s) as i64);
    }
    append(areas, [100, 200]);
    println(`areas: ${areas.len()}`);
    0
}

Parameters borrow by default, and the compiler reads the body for the rest: append pushes into dst, so dst is edits and the argument has to be a place that may be written; a function that keeps its argument is takes, and using the value afterwards is an error that names the call. Nothing is written at the call site, ever. A clause after the signature (edits dst, takes src) states that effect when the author wants it said out loud; it is optional, and a function that says nothing is checked exactly as strictly.

6. Types of your own ​

talor
use std.list.{filter};

struct Account { owner: string, cents: i64 }

impl Account {
    fn deposit(self, amount: i64) { self.cents += amount; }
    fn label(self): string { `${self.owner}: ${self.cents} cents` }
}

trait Describe { fn describe(self): string; }
impl Describe for Account { fn describe(self): string { self.label() } }

fn largest<T: Describe>(items: Array<T>, size: fn(T): i64): string {
    let best = 0;
    let text = "";
    for it in items {
        if size(it) > best { best = size(it); text = it.describe(); }
    }
    text
}

fn main(): i32 {
    let a = Account { owner: "ana", cents: 500 };
    a.deposit(250);
    let all = [a, Account { owner: "bia", cents: 900 }];
    let rich = filter(all, (x) => x.cents > 600);
    println(largest(rich, (x) => x.cents));
    0
}

Methods live in an impl block, never inside the struct, and deposit writes self, which is all it takes for the call to need a value that may be written. A trait is implemented by name, a generic parameter states the traits it needs (T: Describe), and a closure such as (x) => x.cents is a value whose parameter type the call infers.

Where to go from here ​

  • Learn covers each of these in full.
  • The timeline records what the language has gained.

Talor v0.1.0 - Released under the MIT OR Apache-2.0 license.