Viscosity
Review
Part IX · Rust for TypeScript developers · Day 31

123. Functions and expressions

In TypeScript, if is a statement, so you reach for the ternary. In Rust, if, blocks and whole function bodies produce values, and one semicolon decides whether they do.

About 15 minutes, including the exercises and quiz
By the end you will be able to
  • Write Rust functions with typed parameters and a return type.
  • Use if and blocks as expressions that produce values.
  • Explain how a missing or extra semicolon changes what a block returns.
  • Read the compiler errors for mismatched branch types and stray semicolons.

The ternary you never liked

You want a label that depends on a number. In TypeScript:

let label: string;
if (n < 0) {
  label = 'negative';
} else {
  label = 'not negative';
}

Four lines, a variable that has to be let, and a moment where it holds nothing. So you write a ternary instead: const label = n < 0 ? 'negative' : 'not negative';. The ternary exists because if is a statement. It does something but has no value.

Chapter 31 drew that line through Drip’s own grammar: expressions produce values, statements don’t. Rust moves most things to the expression side:

let label = if n < 0 {
    "negative"
} else {
    "not negative"
};

No ternary, no mut, and label is never empty.

Intuition

In TypeScript, a block of code is a room you walk through. In Rust, most blocks are vending machines: you walk through, and something drops out the bottom, namely whatever the last line was. Put a semicolon on that last line and the machine drops nothing.

Blocks have values

Any { ... } block is an expression. Its value is its last expression, written without a semicolon:

let y = {
    let a = 3;
    a + 1
};
// y is 4

let a = 3; is a statement, and it ends with a semicolon. a + 1 has none, so it is the block’s value. Put a semicolon after it and it becomes a statement too. The block then has the value (), the unit value, Rust’s “nothing” (much like void).

let itself is a statement, so let x = let y = 6; is an error. In JavaScript, x = y = 6 works, because assignment is an expression.

Definition

Rust is expression-oriented. A block { ... } evaluates to its final expression, or to () if it ends in a statement. if is an expression: every branch must produce the same type, and an if without else produces (). Adding ; after an expression turns it into a statement and throws its value away.

Both branches of an if must agree:

error[E0308]: `if` and `else` have
  incompatible types
 --> f2.rs:3:31
  |
3 |     let s = if b { 1 } else { "no" };
  |                    -          ^^^^
  |                    |  expected integer,
  |                    |  found `&str`
  |                    expected because
  |                    of this

(rustc 1.97, wrapped to fit. b is a bool.) TypeScript would happily give s the type number | string. Rust wants one type.

Functions

You’ve seen one already. Here is the full shape:

fn add(a: i32, b: i32) -> i32 {
    a + b
}
  • fn, a snake_case name, and parameters, each with its type. A function’s parameter types are never inferred.
  • -> i32 is the return type. Leave it out and the function returns ().
  • The body is a block, so its final expression is the return value. a + b with no semicolon is the return.

return still exists, for leaving early:

fn safe_div(a: i32, b: i32) -> i32 {
    if b == 0 {
        return 0;
    }
    a / b
}

Idiomatic Rust uses return only for early exits, and lets the last expression do the rest[1]. Two more differences from TypeScript: a function can be called from code above its definition, and there are no default or optional parameters.

Definition

A Rust function is declared with fn name(param: Type, ...) -> Ret. Every parameter needs a type. The body’s final expression is returned; return expr; returns early. A function with no -> returns ().

Watching a value flow out

Here is chapter 1’s clamp, the Rust way. It has no return and no variables, just one if expression whose value becomes the function’s:

Step throughValues flow outward
fn clamp(x: i32, lo: i32, hi: i32) -> i32 {
if x < lo {
lo
} else if x > hi {
hi
} else {
x
}
}
fn main() {
let r = clamp(15, 0, 10);
println!("{r}");
}
clamp's body as nested expressions: branch blocks inside an if inside the function bodyclamp’s body blockif … else if … else{ lo }{ hi }{ x }x = 15, lo = 0, hi = 10
main calls clamp(15, 0, 10). Inside clamp, x = 15, lo = 0 and hi = 10. The whole body of clamp is one block, and the block holds one expression: the if.
1/8

The last step is the famous mistake. Here it is for real, in a smaller function:

fn inc(x: i32) -> i32 {
    x + 1;
}
error[E0308]: mismatched types
 --> f1.rs:1:19
  |
1 | fn inc(x: i32) -> i32 {
  |    ---            ^^^ expected `i32`,
  |    |                  found `()`
  |    |
  |    implicitly returns `()` as its body
  |    has no tail or `return` expression
2 |     x + 1;
  |          - help: remove this semicolon
  |            to return this value

The compiler even points at the semicolon. When you see “expected i32, found ()”, look for a semicolon at the end of a block.

Try itBlocks and ifs with values
fn clamp(x: i32, lo: i32, hi: i32)
    -> i32 {
    if x < lo {
        lo
    } else if x > hi {
        hi
    } else {
        x
    }
}

fn main() {
    let y = {
        let a = 3;
        a + 1
    };
    println!("{y}");
    println!("{}", clamp(15, 0, 10));

    let n = -2;
    let label = if n < 0 {
        "negative"
    } else {
        "not negative"
    };
    println!("{label}");
}
Open in Rust Playground ↗

Loops look as you’d expect, minus the parentheses: while n > 0 { ... }. Rust has no C-style for (let i = 0; ...) loop. Chapter 134 covers its for.

Under the hood

Your Drip VM already compiles an expression with branches. Look at or_ in the TypeScript VM’s compiler (chapter 55): a JUMP_IF_FALSE, a JUMP, and two paths that each leave exactly one value on the stack. An if expression would compile the same way as Drip’s if statement, with one change: each branch compiles an expression, not a statement, so the value it leaves stays on the stack instead of being popped. Expression-oriented code costs nothing extra to run.

Your turn

ExerciseA letter grade, as one expression

Write grade(score), which returns a char: 'A' for 90 and up, 'B' for 80 to 89, 'C' for 70 to 79, and 'F' below 70. Use an if / else if / else expression as the body, without return.

fn grade(score: u32) -> char {
    todo!()
}
Open in Rust Playground ↗
Extra practiceCollatz steps

Start with a number n. If it is even, halve it; if it is odd, make it 3 * n + 1. Repeat until it reaches 1. Write steps(n), which returns how many steps that takes (steps(1) is 0, steps(6) is 8). Use a while loop, and an if expression for the next value.

fn steps(n: u64) -> u32 {
    todo!()
}
Open in Rust Playground ↗

What you learned

  • Rust is expression-oriented: blocks and if produce values. A block’s value is its final expression, without a semicolon.
  • A trailing ; turns an expression into a statement, giving (). “expected i32, found ()” usually means a stray semicolon.
  • Both branches of an if must have the same type, and an if used as a value needs an else.
  • Functions: fn name(p: T) -> R { ... }. Parameter types are required. The final expression is the return value; return is for early exits.

Next comes the idea that makes Rust Rust: who owns a value, and when it goes away.

Check your understanding

0/6
  1. 1.

    What is y?

    let y = {
        let a = 2;
        a * 5
    };
  2. 2.

    Why doesn’t this compile?

    fn double(n: i32) -> i32 {
        n * 2;
    }
  3. 3.

    Which line is a correct Rust version of TypeScript’s const m = a > b ? a : b;?

  4. 4.

    What does let s = if ok { 1 }; (no else) do?

  5. 5.

    What does a Rust function declared with no -> return?

  6. 6.

    Your Drip VM compiles a or b with jumps that leave one value on the stack. What would a Drip if expression need beyond the if statement’s jumps?

References

  1. [1] Steve Klabnik, Carol Nichols, Chris Krycho (2026). The Rust Programming Language (3rd edition). No Starch Press; free online at doc.rust-lang.org/book. doc.rust-lang.org/book/ (checked 2026-10-31)