Ir para o conteúdo

Funções e traits ​

A função ​

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; }
  • O tipo de retorno vem depois de :. Omitido, ele é o tipo do corpo: fn total(self) { self.a + self.b } devolve i64, e um corpo que termina em um statement devolve (). Um corpo que o compilador não consegue tipar sozinho, um que usa ? por exemplo, é recusado com uma mensagem que diz para escrever o tipo.
  • return expr; sai mais cedo. A última expressão do corpo, sem ;, é o valor de retorno.
  • Parâmetros passam por borrow por padrão; a clause depois da assinatura diz o resto.
  • Parâmetros generic aparecem depois do nome: <T, U: Bound>. Um corpo generic é checado uma vez, com seus parâmetros de tipo opacos, mesmo quando nada o chama, então só o que os bounds permitem é válido em um T.

Argumentos default ​

Um parâmetro no fim da lista pode ter um valor default, escrito depois do tipo. Uma chamada que omite o argumento recebe o default, preenchido no call site:

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

fn main(): i32 {
    println(greet("ana"));
    println(greet("bia", "hi"));
    0
}
  • Só os parâmetros do fim têm um: depois de um parâmetro com default, todo parâmetro tem um.
  • Um default é um literal ou o nome de uma const, nunca uma chamada ou outra expressão.
  • Não existem argumentos nomeados: um argumento é colocado pela posição, e omitir um omite todos os que vêm depois dele.
  • Uma função usada como valor mantém a assinatura completa, então uma chamada através do valor passa todos os argumentos.

Métodos: impl ​

impl é o único mecanismo para métodos; eles nunca são escritos dentro do corpo da struct.

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 }
}

O receiver é sempre self, e o efeito de um método sobre ele segue a mesma regra de qualquer outro parâmetro: o compilador o infere do corpo, e uma clause (edits self, takes self) o declara quando o autor o quer na assinatura.

new não é keyword: um construtor é um método estático comum, então dê a ele o nome do que ele faz (of, empty, with_capacity). O único new que a linguagem define é Box.new.

Traits ​

Traits são nominais: um tipo implementa uma trait só por um bloco impl Trait for Type. Ter os métodos certos não basta.

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() }

Chamadas através de um bound são monomorfizadas: dispatch estático, uma cópia por tipo usado. Uma trait pode receber argumentos de tipo, e o impl diz quais - é assim que uma trait nomeia um tipo que ela não possui:

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

Uma trait ainda não tem corpos default de método nem tipos associados: toda implementação escreve todos os métodos.

Um método de trait não carrega edits nem takes próprios: o efeito de cada implementação é lido do corpo dela, e uma chamada através de um bound usa a implementação que escolheu. Onde a trait é usada como dyn, as implementações precisam concordar sobre o que fazem com self e com cada parâmetro, porque uma tabela de métodos tem uma calling convention por método; a recusa nomeia as duas implementações que diferem.

dyn Trait: um tipo para toda implementação ​

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
}

Um valor dyn Trait ocupa duas palavras de máquina: o valor no heap e a tabela dos métodos da trait para o tipo dele, escolhida em tempo de execução. A trait é inferida do contexto onde existe um. Uma trait cujo método recebe ou devolve self por valor não tem forma dyn.

Closures ​

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

Uma closure é um valor de função que carrega as variáveis que menciona. O tipo dela é fn(A): R, e ela vai aonde um valor vai: um parâmetro, um valor de retorno, um campo, um elemento de um 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 devolve uma closure que capturou n, e add5 mantém esse n depois que make_adder retornou. Uma chamada a uma função generic infere os tipos dos parâmetros de uma closure a partir dos outros argumentos, então eles só são escritos quando nada mais os diz. Closures tem o resto: a captura por move, a closure que só pode ser chamada uma vez, a captura de uma view e o compartilhamento de uma closure entre tasks.

Um limite: uma função que escreve, fica com ou devolve um dos seus parâmetros, ou uma parte de um, não pode ser usada como valor. O tipo fn(A): R não carrega clause nem vínculo entre o resultado e um argumento, então uma chamada através do valor não teria como expressar nenhum dos dois.

unsafe ​

unsafe marca uma declaração cujo contrato o compilador não consegue checar:

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") }

Todo extern fn é uma delas, então ele é escrito unsafe extern fn. Chamar qualquer uma das duas só é permitido de dentro de outra unsafe fn ou de um bloco unsafe { ... }. A palavra aparece onde o perigo está e onde ele é assumido, e em nenhum outro lugar.

Uma trait pode ser uma unsafe trait: implementá-la é uma promessa que o compilador não consegue checar, então ela só é implementada por um unsafe impl.

Talor v0.1.0 - Publicado sob a licença MIT OR Apache-2.0.