06 / 06

Syntax

A tour of the language in twelve programs. Every one of them is compiled and run by the check that publishes this page, so what you read here is what the compiler in the footer accepts today.

Variables and types

let binds once, var can be reassigned, const is a compile-time constant. Types are written after the name; the language is gradually typed, but annotating is what keeps the compiler honest — and, as you will see with arrays, sometimes it is what keeps it correct.

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

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

Functions

Parameters are typed, the return type follows ->, and a function with no return type returns nothing.

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

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

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

Control flow

if/else, while, and for over a range or over anything iterable. No parentheses around the condition; braces are not optional.

fn main() {
    let x: int = 10
    if x > 5 {
        print("greater")
    } else {
        print("less or equal")
    }

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

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

Arrays

Arrays grow with push, iterate with for, and chain through iter(). The lambda passed to filter is an ordinary typed function without a name.

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, and keys() to walk them.

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

    print(capitals.size())            // 2
    print(capitals.get("France"))     // Paris
    print(capitals.has("Brazil"))     // false

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

Notice let k: String = ks[i] rather than using ks[i] directly. An element read out of an Array without a type annotation is treated as an int — which, passed to get, is not a compile error but the wrong lookup. Annotate the element; it costs one line and removes a whole class of confusing bugs.

Strings are bytes

length(), substring(), indexOf() and charAt() all count bytes, not characters. For characters — UTF-8 codepoints — there is char_length(). The rest is what you would expect: trim(), split(), contains(), toUpper(), toLower(), and ${…} interpolation inside a literal.

fn main() {
    let text: String = "  café latte  "
    let clean: String = text.trim()

    print(clean.length())         // 11 — BYTES: é takes two
    print(clean.char_length())    // 10 — UTF-8 codepoints
    print(clean.substring(0, 3))  // "caf" — substring cuts bytes too
    print(clean.contains("latte"))

    let parts: Array = clean.split(" ")
    print(parts.length())         // 2

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

Structs

Fields are typed; methods live in an impl block and take 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 and match

A variant can carry data, and match must cover every one of them. The separator is a dot — Shape.Circle — never a double colon; that mistake is common enough that nyx vet has a warning for it.

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

Absence is a value, not a null. match handles both cases, and if let is the short form when you only care about one.

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) => "found: " + int_to_string(v),
        Option.None    => "nothing"
    }
}

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

Result and the ? operator

Failure is a value too. ? unwraps the success case and returns the error to the caller, so a chain of fallible steps reads like a chain of ordinary ones.

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("not positive: " + s)
    }
    return Result.Ok(n)
}

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

// ? hands the Err back to the caller, no explicit match needed
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

A trait is a set of methods a type promises to have. dyn Trait as a parameter type accepts any type that implements it.

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

struct Cat { name: String }

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

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

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

Closures

A function defined inside another captures its environment, and Fn is the type of a function value — returnable, storable, passable as an argument.

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

Where to go next

This was the shape of the language. What the standard library can do — HTTP servers and clients, JSON, TOML, CSV, SQLite, sockets, TLS, threads and channels, cryptography, dates — is a different question, and it is answered by worked examples rather than by prose.

Nyx by exampleAround a hundred recipes, from hello world to an HTTP server. Read one, copy it, change it.Open →