Skip to content

Functions and traits ​

The function ​

talor
fn add(a: i64, b: i64): i64 { a + b }             // tail expression is the value
fn greet(name: string) { println(`hi ${name}`); } // no return type: the body's, here ()
fn first<T>(xs: Array<T>): Option<T> { panic("the body") }
fn show<T: Display>(x: T): string { x.fmt() }
pub fn exported(): i64 { return 1; }
  • The return type follows :. Omitted, it is the type of the body: fn total(self) { self.a + self.b } returns i64, and a body that ends in a statement returns (). A body the compiler cannot type on its own, one that uses ? for instance, is refused with a message that says to write the type.
  • return expr; exits early. The last expression of the body, without ;, is the return value.
  • Parameters are borrowed by default; the clause after the signature says the rest.
  • Generic parameters appear after the name: <T, U: Bound>. A generic body is checked once with its type parameters opaque, even when nothing calls it, so only what the bounds allow is legal on a T.

Default arguments ​

A trailing parameter may have a default value, written after its type. A call that leaves the argument out gets the default, filled in at the call site:

talor
fn greet(name: string, greeting: string = "hello"): string { `${greeting}, ${name}` }

fn main(): i32 {
    println(greet("ana"));
    println(greet("bia", "hi"));
    0
}
  • Only trailing parameters have one: after a parameter with a default, every parameter has one.
  • A default is a literal or the name of a const, never a call or another expression.
  • There are no named arguments: an argument is placed by its position, and leaving one out leaves out every one after it.
  • A function used as a value keeps its full signature, so a call through the value passes every argument.

Methods: impl ​

impl is the only mechanism for methods; they are never written inside the struct body.

talor
impl Point {
    fn of(x: f64, y: f64): Point { Point { x, y } }        // no self: static, Point.of(..)
    fn norm(self): f64 { sqrt(self.x * self.x + self.y * self.y) }
    fn scale(self, k: f64) { self.x *= k; self.y *= k; }
    fn into_pair(self): (f64, f64) { (self.x, self.y) }
}

impl<T> Wrapper<T> {
    fn get(self): T { self.value }
}

The receiver is always self, and a method's effect on it follows the same rule as any other parameter: the compiler infers it from the body, and a clause (edits self, takes self) states it when the author wants it at the signature.

new is not a keyword: a constructor is an ordinary static method, so name it for what it makes (of, empty, with_capacity). The only new the language defines is Box.new.

Traits ​

Traits are nominal: a type implements a trait only through an impl Trait for Type block. Having the right methods is not enough.

talor
pub trait Display {
    fn fmt(self): string;
}

impl Display for Point {
    fn fmt(self): string { `(${self.x}, ${self.y})` }
}

fn show<T: Display>(x: T): string { x.fmt() }

Calls through a bound are monomorphized: static dispatch, one copy per type used. A trait may take type arguments, and the impl says which - that is how a trait names a type it does not own:

talor
trait Peek<T> { fn peek(self): T; }
impl Peek<i64> for Cell { fn peek(self): i64 { self.n } }

A trait has no default method bodies and no associated types yet: every implementation writes every method.

A trait method carries no edits or takes of its own: each implementation's effect is read from its body, and a call through a bound uses the implementation it picked. Where the trait is used as dyn, the implementations must agree on what they do with self and each parameter, because one method table has one calling convention per method; the refusal names the two implementations that differ.

dyn Trait: one type for every implementation ​

talor
trait Speak { fn say(self): string; }

struct Dog { name: string }
struct Cat { lives: i64 }
impl Speak for Dog { fn say(self): string { `${self.name} barks` } }
impl Speak for Cat { fn say(self): string { `a cat with ${self.lives} lives` } }

fn hear(s: dyn Speak): string { s.say() }

fn main(): i32 {
    let d: dyn Speak = dyn Dog { name: "Rex" };   // `dyn` makes one, like `shared` does
    let c: dyn Speak = dyn Cat { lives: 9 };
    println(hear(d));
    println(hear(c));
    0
}

A dyn Trait value is two machine words: the value on the heap and the table of the trait's methods for its type, chosen at run time. The trait is inferred from the context where there is one. A trait whose method takes or returns self by value has no dyn form.

Closures ​

talor
let twice = (a: i64) => a * 2;
let sum = (a: i64, b: i64) => { let s = a + b; s };

A closure is a function value that carries the variables it mentions. Its type is fn(A): R, and it goes wherever a value goes: a parameter, a return value, a field, an element of an array.

talor
use std.list.{map, filter, fold};

fn make_adder(n: i64): fn(i64): i64 {
    (x: i64) => x + n
}

fn apply_twice(f: fn(i64): i64, x: i64): i64 { f(f(x)) }

fn main(): i32 {
    let add5 = make_adder(5);
    println(`add5(10) is ${add5(10)}`);
    println(`applied twice, ${apply_twice(add5, 0)}`);

    let xs: Array<i64> = [5, 3, 8, 1, 9, 2];
    let doubled = map(xs, (x) => x * 2);
    let big = filter(doubled, (x) => x > 6);
    let total = fold(big, 0, (a, b) => a + b);
    println(`doubled, over six, summed: ${total}`);
    0
}

make_adder returns a closure that captured n, and add5 keeps that n after make_adder has returned. A call to a generic function infers a closure's parameter types from the other arguments, so they are written only when nothing else says them. Closures has the rest: capturing by move, the closure that can be called once, capturing a view, and sharing one between tasks.

One limit: a function that writes, keeps or hands back one of its parameters, or a part of one, cannot be used as a value. The type fn(A): R carries no clause and no tie between the result and an argument, so a call through the value would have no way to express either.

unsafe ​

unsafe marks a declaration whose contract the compiler cannot check:

talor
unsafe extern fn talor_env_get(name: string): string;   // C ABI, no body
unsafe fn write_raw(fd: i64, p: i64, n: i64): i64 { panic("the body") }

Every extern fn is one, so it is written unsafe extern fn. Calling either is allowed only from inside another unsafe fn or an unsafe { ... } block. The word appears where the danger is and where it is taken on, and nowhere else.

A trait may be an unsafe trait: implementing it is a promise the compiler cannot check, so it is implemented only by an unsafe impl.

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