06 / 06

Sintaxis

Un recorrido por el lenguaje en doce programas. Todos se compilan y se ejecutan en la verificación que publica esta página, así que lo que lees acá es lo que el compilador del pie acepta hoy.

Variables y tipos

let liga una sola vez, var se puede reasignar, const es una constante de compilación. Los tipos van después del nombre; el lenguaje es de tipado gradual, pero anotar es lo que mantiene honesto al compilador — y, como verás con los arrays, a veces es lo que lo mantiene correcto.

fn main() {
    let name: String = "Nyx"   // inmutable
    var counter: int = 0       // mutable
    const MAX: int = 100       // constante

    counter = counter + 1
    print(name)
    print(counter)
    print(MAX)
}

Funciones

Los parámetros llevan tipo, el tipo de retorno va después de ->, y una función sin tipo de retorno no devuelve nada.

fn add(a: int, b: int) -> int {
    return a + b
}

fn greet(name: String) -> String {
    return "Hola, " + name + "!"
}

fn main() {
    print(add(3, 4))
    print(greet("mundo"))
}

Control de flujo

if/else, while, y for sobre un rango o sobre cualquier cosa iterable. Sin paréntesis alrededor de la condición; las llaves no son opcionales.

fn main() {
    let x: int = 10
    if x > 5 {
        print("mayor")
    } else {
        print("menor o igual")
    }

    var i: int = 0
    while i < 3 {
        print(i)
        i = i + 1
    }

    for j in 0..3 {
        print(j)
    }
}

Arrays

Los arrays crecen con push, se recorren con for, y se encadenan con iter(). El lambda que recibe filter es una función tipada común y corriente, sin nombre.

fn main() {
    var nums: Array = [1, 2, 3, 4, 5]
    nums.push(6)
    print(nums.length())

    for n in nums {
        print(n)
    }

    let evens: Array = nums.iter()
        .filter(fn(x: int) -> bool { return x % 2 == 0 })
        .collect()
    print(evens.length())
}

Maps

insert, get, has, size, y keys() para recorrerlos.

fn main() {
    var capitales: Map = Map.new()
    capitales.insert("Argentina", "Buenos Aires")
    capitales.insert("Francia", "Paris")

    print(capitales.size())             // 2
    print(capitales.get("Francia"))     // Paris
    print(capitales.has("Brasil"))      // false

    let ks: Array = capitales.keys()
    var i: int = 0
    while i < ks.length() {
        let k: String = ks[i]
        print(k + " -> " + capitales.get(k))
        i = i + 1
    }
}

Fíjate en let k: String = ks[i] en vez de usar ks[i] directo. Un elemento leído de un Array sin anotación de tipo se trata como int — y eso, pasado a get, no es un error de compilación sino la búsqueda equivocada. Anota el elemento; cuesta una línea y elimina toda una familia de bugs confusos.

Los strings son bytes

length(), substring(), indexOf() y charAt() cuentan bytes, no caracteres. Para caracteres — codepoints UTF-8 — está char_length(). El resto es lo que esperarías: trim(), split(), contains(), toUpper(), toLower(), e interpolación ${…} dentro de un literal.

fn main() {
    let texto: String = "  café latte  "
    let limpio: String = texto.trim()

    print(limpio.length())         // 11 — BYTES: la é ocupa dos
    print(limpio.char_length())    // 10 — codepoints UTF-8
    print(limpio.substring(0, 3))  // "caf" — substring también corta bytes
    print(limpio.contains("latte"))

    let partes: Array = limpio.split(" ")
    print(partes.length())         // 2

    let lang: String = "Nyx"
    print("lenguaje: ${lang}")
}

Structs

Los campos llevan tipo; los métodos viven en un bloque impl y reciben self.

struct Point {
    x: int,
    y: int
}

impl Point {
    fn distance_sq(self) -> int {
        return self.x * self.x + self.y * self.y
    }
}

fn main() {
    let p: Point = Point { x: 3, y: 4 }
    print(p.distance_sq())   // 25
}

Enums y match

Una variante puede llevar datos, y match tiene que cubrirlas todas. El separador es un punto — Shape.Circle — nunca dos puntos dobles; ese error es tan común que nyx vet tiene un aviso para él.

enum Shape {
    Circle(int),
    Rect(int, int),
    Empty
}

fn area(s: Shape) -> int {
    return match s {
        Shape.Circle(r)  => 3 * r * r,
        Shape.Rect(w, h) => w * h,
        Shape.Empty      => 0
    }
}

fn main() {
    print(area(Shape.Circle(5)))
    print(area(Shape.Rect(3, 7)))
    print(area(Shape.Empty))
}

Option

La ausencia es un valor, no un nulo. match maneja los dos casos, y if let es la forma corta cuando solo te importa uno.

fn first_positive(nums: Array, index: int) -> Option<int> {
    let val: int = nums[index]
    if val > 0 {
        return Option.Some(val)
    }
    return Option.None
}

fn describe(opt: Option<int>) -> String {
    return match opt {
        Option.Some(v) => "encontrado: " + int_to_string(v),
        Option.None    => "nada"
    }
}

fn main() {
    let nums: Array = [10, -5, 3]
    print(describe(first_positive(nums, 0)))
    print(describe(first_positive(nums, 1)))

    if let Option.Some(v) = first_positive(nums, 2) {
        print("if let: " + int_to_string(v))
    } else {
        print("if let: nada")
    }
}

Result y el operador ?

El fallo también es un valor. ? desenvuelve el caso exitoso y devuelve el error a quien llamó, así una cadena de pasos que pueden fallar se lee como una cadena de pasos comunes.

enum Result {
    Ok(int),
    Err(String)
}

fn parse_positive(s: String) -> Result {
    let n: int = string_to_int(s)
    if n <= 0 {
        return Result.Err("no es positivo: " + s)
    }
    return Result.Ok(n)
}

fn double_it(n: int) -> Result {
    if n > 1000 {
        return Result.Err("demasiado grande")
    }
    return Result.Ok(n * 2)
}

// ? le devuelve el Err a quien llamó, sin match explícito
fn pipeline(s: String) -> Result {
    let n: int = parse_positive(s)?
    let d: int = double_it(n)?
    return Result.Ok(d + 1)
}

fn show(r: Result) {
    match r {
        Result.Ok(v)  => print("ok: " + int_to_string(v)),
        Result.Err(e) => print("err: " + e)
    }
}

fn main() {
    show(pipeline("42"))
    show(pipeline("-5"))
    show(pipeline("2000"))
}

Traits

Un trait es un conjunto de métodos que un tipo promete tener. dyn Trait como tipo de parámetro acepta cualquier tipo que lo implemente.

trait Describable {
    fn describe(self) -> String
}

struct Cat { name: String }

impl Describable for Cat {
    fn describe(self) -> String {
        return self.name + " el gato"
    }
}

fn print_desc(d: dyn Describable) {
    print(d.describe())
}

fn main() {
    let c: Cat = Cat { name: "Bigotes" }
    print_desc(c)
}

Closures

Una función definida dentro de otra captura su entorno, y Fn es el tipo de un valor función — se puede devolver, guardar y pasar como argumento.

fn make_adder(n: int) -> Fn {
    fn add(x: int) -> int {
        return n + x
    }
    return add
}

fn apply(f: Fn, value: int) -> int {
    return f(value)
}

fn main() {
    let add5: Fn = make_adder(5)
    print(add5(3))
    print(apply(add5, 10))

    let nums: Array = [1, 2, 3, 4, 5, 6]
    let evens: Array = nums.iter()
        .filter(fn(x: int) -> bool { return x % 2 == 0 })
        .collect()
    print(evens.length())
}

A dónde ir después

Esta era la forma del lenguaje. Lo que puede hacer la biblioteca estándar — servidores y clientes HTTP, JSON, TOML, CSV, SQLite, sockets, TLS, hilos y canales, criptografía, fechas — es otra pregunta, y se responde con ejemplos resueltos en vez de con prosa.

Nyx por ejemploAlrededor de cien recetas, desde el hola mundo hasta un servidor HTTP. Lee una, cópiala, cámbiala.Abrir →