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 asif). If an arm needs several statements, use a block that ends inreturn: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 anon-exhaustive-matcherror 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:
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 ani32so we can compare it to ASCII codes.is_vowelreturns abooland is called like any other function.first_vowelreturns?string— sometimes it has a value, sometimes it'snone..hasand.valare 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.