Chapter 03 9 min read

Functions and control flow

A program is functions calling functions. This chapter is the surface area: how to define them, how to branch and loop, and one feature that surprises people coming from Python — most things in Glide are expressions, including blocks.

Functions

The shape:

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

Read it left to right:

  • fn — declares a function.
  • add — its name.
  • (a: i32, b: i32) — its parameters. Every parameter has an explicit type. No *args, no **kwargs — what you declare is what callers must pass.
  • -> i32 — the return type. A function that returns nothing omits the arrow:
fn shout(msg: string) {
    println!(msg.to_upper());
}
  • { ... } — the body, a block. Statements end with ;.
  • return a + b; — explicit return. This is non-negotiable in Glide; we don't infer return values from trailing expressions like Rust does.

Calling a function

Just like you'd expect:

fn main() -> i32 {
    let s: i32 = add(3, 4);
    println!(s);    // 7
    return 0;
}

Passing big things

Scalars (i32, bool, f64...) are passed by value — the callee gets a copy. Heap objects (*Vector<T>, *HashMap<V>, ...) are passed by pointer — both caller and callee see the same value. Passing a pointer is cheap (it's just an address).

If you want the callee to read a heap value without taking ownership of it, you pass a borrow:

fn print_first(v: &*Vector<i32>) {
    println!(v.get(0));
}

fn main() -> i32 {
    let v: *Vector<i32> = Vector::new();
    v.push_all!(1, 2, 3);
    print_first(&v);
    println!(v.len());   // still ours — borrows don't consume
    return 0;
}

The & means "borrow" — like lending a book. The borrow rules get a whole chapter (Memory model); for now just notice & is "give read access" and &mut is "give write access."

if is an expression

The familiar form works:

if x > 0 {
    println!("positive");
} else if x < 0 {
    println!("negative");
} else {
    println!("zero");
}

But — and this is the key trick — every if is also a value. You can put it on the right side of =:

let kind: string = if x > 0 { "positive" }
                   else if x < 0 { "negative" }
                   else { "zero" };

The whole if/else chain evaluates to one of the three strings, which gets bound to kind. In expression position each branch is a single value with no `return` and no trailing `;` — the bare value is the branch's result. (else is mandatory here: an expression must always produce something.)

match

match is a switch over the shape of a value — an enum variant, or the some/none of a ?T. Each arm is pattern => result. match is where enums shine:

enum HttpMethod {
    Get,
    Post,
    Put,
    Delete,
}

fn allow_body(m: HttpMethod) -> bool {
    return match m {
        HttpMethod::Get    => false,
        HttpMethod::Post   => true,
        HttpMethod::Put    => true,
        HttpMethod::Delete => false,
    };
}
  • Each arm is Pattern => value, (comma-separated, bare value — same expression rule as if). If an arm needs several statements, use a block that ends in return: HttpMethod::Get => { do_thing(); return false; }.
  • _ is the wildcard — matches anything the prior arms didn't catch.
  • The compiler checks that you've covered every variant of the enum. If you ever add a HttpMethod::Patch, you get a non-exhaustive-match error until you handle it — which catches a real class of bug.

Loops

Three loop forms.

`while` runs as long as the condition is true:

let mut i: i32 = 0;
while i < 10 {
    println!(i);
    i = i + 1;
}

`for` is the classic C-style three-part loop (init; condition; step):

for let j: i32 = 0; j < 10; j = j + 1 {
    println!(j);
}

`for ... in` walks any iterable (Vector, range, HashMap, anything implementing the Iterator trait):

let v: *Vector<i32> = Vector::new();
v.push_all!(10, 20, 30);
for x in v {
    println!(x);
}

break exits the enclosing loop; continue skips to the next iteration.

Ranges

A range expression is start..end (half-open, end excluded) or start..=end (closed, end included):

for i in 0..5      { println!(i); }   // 0, 1, 2, 3, 4
for i in 0..=5     { println!(i); }   // 0, 1, 2, 3, 4, 5
for i in 1..=10    { println!(i * i); }

Ranges are just values of type Range<i32> (or Range<i64>). You can collect them, filter them, anything in stdlib::iter works on them.

Blocks as values

A bare { ... } block runs its statements and produces a value via return:

let result: i32 = {
    let a: i32 = expensive();
    let b: i32 = also_expensive();
    return a + b;
};

This is the same machinery if and match use. The point of a bare block is to scope temporary bindings: a and b are dropped at the closing brace, even though their sum lives on as result. You'd reach for this when a calculation needs intermediate variables that nothing else in the function should see.

A first look at ? for error propagation

When a function returns !T (recall: "a T or an error"), you usually want to bail out on errors. The ? operator does that in one character:

fn parse_int(s: string) -> !i32 {
    if s.eq("") { return err("empty"); }
    return ok(42);   // pretend we parsed
}

fn double(s: string) -> !i32 {
    let n: i32 = parse_int(s)?;   // if err → return err; if ok → unwrap to n
    return ok(n * 2);
}

? is short for "if this returned an error, return it from this function too; otherwise pull out the value." It only works inside a function that itself returns !T or ?T. We'll spend a whole chapter on this — see Errors as values.

A worked example

Let's put a few of these together. Here's a function that returns the first vowel in a string, or none if there isn't one:

first_vowel.glide
fn is_vowel(c: i32) -> bool {
    return c == 97 || c == 101 || c == 105 || c == 111 || c == 117
        || c == 65 || c == 69  || c == 73  || c == 79  || c == 85;
}

fn first_vowel(s: string) -> ?string {
    for let i: i32 = 0; i < s.len(); i = i + 1 {
        if is_vowel(s.at(i).to_int()) {
            return some(s.substring(i, i + 1));
        }
    }
    return none();
}

fn main() -> i32 {
    let r: ?string = first_vowel("rhythm and blues");
    if r.has {
        println!("got:", r.val);
    } else {
        println!("no vowel");
    }
    return 0;
}

Three things to notice:

  • s.at(i) returns one char; .to_int() widens it to an i32 so we can compare it to ASCII codes.
  • is_vowel returns a bool and is called like any other function.
  • first_vowel returns ?string — sometimes it has a value, sometimes it's none. .has and .val are how you read it out.

Where to next

You've got functions, branches, loops, and a preview of ?. The next chapter — Errors as values — is the big one: how Glide handles failure without exceptions.