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 →