Values and types

This chapter is the ground the rest stands on: numbers, text, arrays, conditions and loops. One program shows all of it; the sections after it walk through the parts.

topo/code/values.nx
fn main() {
    // bindings: `let` is immutable, `var` can be reassigned
    let answer = 42
    var count = 0
    count += 1
    println("{} {}", .{answer, count})

    // integers have a width; the literal takes the type the context asks for
    let small: u8 = 200
    let wide: i64 = 3_000_000_000
    let hex = 0xFF
    let bits = 0b1010
    println("{} {} {} {}", .{small, wide, hex, bits})

    // arithmetic traps on overflow; wrapping and saturating forms never do
    let wrapped = small +% 100
    let clamped = small +| 100
    println("{} {}", .{wrapped, clamped})

    // floats, bools, chars, text
    let ratio = 22.0 / 7.0
    let precise = ratio > 3.14 and ratio < 3.15
    let letter = 'n'
    let text = "Nexium"
    println("{:.3} {} {} {} {}", .{ratio, precise, letter, text, text.len})

    // casts are explicit; narrowing is checked at run time
    let f = 7.9
    let truncated = f as i32
    let widened = small as u32 * 1000
    println("{} {}", .{truncated, widened})

    // arrays have a fixed length, slices view a range of anything
    let primes = [2, 3, 5, 7, 11]
    let middle = primes[1..4]
    println("{} {} {}", .{primes.len, middle.len, middle[0]})

    // tuples group values of different types
    let pair = (3, "three")
    println("{} is {}", .{pair.0, pair.1})

    // if is an expression; blocks are expressions whose value is the last line
    let size = if answer > 40 { "big" } else { "small" }
    let squared = {
        let base = answer
        base * base
    }
    println("{} {}", .{size, squared})

    // loops
    var total = 0
    for p in primes { total += p }
    for p, i in primes { if i == 0 { println("first prime {}", .{p}) } }
    for i in 0..10 step 3 { print("{} ", .{i}) }
    println("", .{})
    while total > 20 { total -= 10 }
    println("{}", .{total})

    // match on numbers, ranges, strings, and anything else with `_`
    for n in [1, 7, 15, 100] {
        let word = match n {
            1 => "one",
            2..=9 => "a few",
            10..=99 => "some",
            _ => "many",
        }
        println("{}: {}", .{n, word})
    }
    match text {
        "Nexium" => println("named", .{}),
        _ => println("unnamed", .{}),
    }

    // a labeled block yields a value with break
    let found = search: {
        for p in primes { if p > 4 { break :search p } }
        0
    }
    println("first prime above 4: {}", .{found})
}
topo/code/values.expected
42 1
200 3000000000 255 10
44 255
3.143 true n Nexium 6
7 200000
5 3 3
3 is three
big 1764
first prime 2
0 3 6 9 
18
1: one
7: a few
15: some
100: many
named
first prime above 4: 5

Bindings#

let binds a name once; var binds a name you can assign to again. Neither needs a type when the initializer gives one away, and both take one when it matters: let small: u8 = 200. Prefer let; a reader learns something from every var that remains.

Integers#

Nexium has the fixed-width integers a systems language needs: i8 to i128, u8 to u128, and isize/usize for sizes and indices. There is no implicit conversion between widths, not even widening: a + b needs a and b to have the same type, and x as u32 says out loud where a width changes. A literal takes the type its context asks for and is i64 when nothing asks.

The arithmetic operators trap on overflow: 200 + 100 in a u8 is a panic with a message and a location, never a silent wrap to 44. When wrapping is what you mean, say so: +%, -%, *% wrap; +|, -|, *| saturate at the type's limits. A hash function wants *%; a volume control wants +|. Chapter 15 shows how the compiler notices when the trapping form cannot actually trap, so that most arithmetic costs nothing.

Literals: 42, 0xFF, 0o755, 0b1010_1100, 3_000_000_000 (the underscores are for you).

Floats, booleans, characters, text#

f64 is the default float, f32 the other one. bool is true or false; and, or and ! combine them and are spelled as words. char is one Unicode scalar value, 'n' or '\n' or 'é', and compares with the integer types that can hold it.

Text is bytes. A string literal "Nexium" has type []u8, a slice of bytes that must be valid UTF-8; text.len is the byte length, 6 here. The owning counterpart, String, comes in chapter 9, and std.text handles the cases where a byte is not a character.

{:.3} in the format string rounds a float to three decimals; the other placeholders are in the reference.

Casts#

x as T converts between number types, between an integer and a char, between a bool and an integer, and between a unit enum and an integer. Narrowing is checked: 300 as u8 panics unless the compiler can prove the value fits, which it often can (a loop index over a slice, a value that was just compared). @truncate(u8, x) is the cast that wraps instead. 7.9 as i32 truncates toward zero.

Arrays, slices, tuples#

[2, 3, 5, 7, 11] is an array: five i64s, a fixed size that is part of its type ([5]i64), a value that is copied when assigned. primes[1..4] is a slice of it: a pointer and a length, a view that owns nothing. Functions almost always take slices, so that arrays, Lists and pieces of either all fit; primes[..] is the slice of the whole array.

A match takes a slice apart by shape: [] => the empty one, [x] => one element, [first, rest..] => the first and a view of the others ([.., last] reads from the end), and the arms are exhaustive once every length has one.

A tuple groups a few values of different types: (3, "three") has type (i64, []u8) and fields .0 and .1. let (n, word) = pair names the parts in one line, and for (n, word) in pairs does it for each element; over a value that already has a home the names are views of its parts, over a fresh one (a call's result, a literal) they own them.

if, blocks, match#

if is an expression when it has an else, so it can sit on the right of a let. Conditions take no parentheses; bodies always take braces, even for one statement, which keeps if c { return v } unambiguous on one line.

A block is an expression whose value is its last line without a return, which is how squared above gets its value. A labeled block, search: { ... }, is left early with break :search value; that is the idiom for "the first thing that matches, or a default".

match compares a value against patterns in order and takes the first arm that fits. Integers match literals and ranges (2..=9 is inclusive); text matches text; the _ arm takes whatever is left. Matches on enums and booleans must be exhaustive, and the compiler says which case is missing. Chapter 7 uses that with enums, chapter 12 with binary data.

Loops#

while cond { } and for in four forms:

for x in items { }               // arrays, slices, lists, strings, map keys
for (k, v) in m { }              // a map's entries
for x in it { }                  // any value with next(self: *mut Self) -> ?T
for x, i in items { }            // with the index
for i in 0..10 { }               // a range: 0 to 9
for i in 0..10 step 3 { }        // 0, 3, 6, 9; `for i in 10..0 step -1` counts down

break and continue do what they do everywhere; a labeled loop (outer: for ...) is left with break :outer. while c { } else { } runs the else when the condition turns false, not after a break, which is the "search finished without finding" case.

The loop variable of a for over a collection is a view of the element, not a copy you own; that matters for owning element types and is one of the rules of chapter 8.

Next: functions and errors.

Exercises#

The compiler grades these: nx topo 5 writes them into a directory, nx topo check runs them. Or do them here: the code is editable, Run runs it with the compiler in the page (it loads on the first run and never sends your code anywhere), Check grades it as nx topo check would, and Reset puts the exercise back. Ctrl+Enter runs.

fill_wrapfill in the blank

Replace ??? with the addition that wraps around instead of trapping.

fill_wrap.nx
fn main() {
    let small: u8 = 250
    println("{}", .{small ??? 10})
}
The recorded output
4
Hints
  • the chapter's arithmetic section shows three forms of +
Solution
fn main() {
    let small: u8 = 250
    println("{}", .{small +% 10})
}

fix_widthsfix the error

The compiler rejects this program; make it print the recorded output.

fix_widths.nx
fn main() {
    let small: u8 = 200
    let big: u32 = 100_000
    println("{}", .{small + big})
}
The recorded output
100200
Hints
  • there is no implicit conversion between integer widths; as converts
Solution
fn main() {
    let small: u8 = 200
    let big: u32 = 100_000
    println("{}", .{small as u32 + big})
}

predict_slicespredict the output

Write what this prints into predict_slices.guess before you run it.

predict_slices.nx
fn main() {
    let primes = [2, 3, 5, 7, 11, 13]
    let mid = primes[2..5]
    println("{} {} {}", .{mid.len, mid[0], mid[mid.len - 1]})
    let text = "base camp"
    println("{} {}", .{text.len, text[5..]})
}
What does it print?
3 5 11
9 camp
Hints
  • a range 2..5 takes the elements at 2, 3 and 4

Quizno peeking

  1. Which addition wraps around on overflow instead of trapping?
    • +
    • +%
    • +|
    Answer

    +%. +% wraps, +| saturates at the type's limits, and plain + traps with a defined panic.

  2. What is the type of x in let x = 7?
    • i32
    • i64
    • usize
    Answer

    i64. An integer literal takes the type the context asks for; with no context it defaults to i64.

  3. What does let s = "camp" hold?
    • an owned String
    • a view of four bytes in the executable, a []u8
    • a char array
    Answer

    a view of four bytes in the executable, a []u8. A string literal is a slice of bytes that live in the program; String.from("camp") would copy them onto the heap.