Skip to content

← Ground Rules: Values, Types, Control Flow step 14 of 24

Easy Primitives

if is an expression

FizzBuzz, as data. For 1..=n, produce "Fizz" for multiples of three, "Buzz" for multiples of five, "FizzBuzz" for multiples of both, and the number written out otherwise.

pub fn fizzbuzz_codes(n: u32) -> Vec<String>

n = 15 gives the familiar fifteen strings. n = 0 gives []. n = 1 gives ["1"].

The constraint: every element must come from one if/else if expression bound to one value. No early returns, no pushing from inside the branches.

The starter does not compile, and its error message is the one that makes “if is an expression” click.

No ternary, because none is needed

Rust has no ?: operator, and it does not need one, because if already produces a value:

let label = if n > 0 { "positive" } else { "non-positive" };

This is strictly better than a ternary. It reads the same in the two-branch case, it scales to five branches without becoming unreadable, and it is the same syntax you already use for control flow — one construct, not two.

Two rules make it work:

  1. Every branch must have the same type. The whole if has one type, so all its arms must agree.
  2. If there is no else, the type must be (). An if without an else can produce nothing when the condition is false, and the only value that can stand in for “nothing” is unit. So let x = if c { 5 }; is an error, and if c { do_thing(); } as a statement is fine.

The error you are here to meet

E0308 has several faces, and this problem shows two of them.

Face one: “expected bool, found integer”. Rust’s if takes a bool and only a bool. There is no truthiness — if 1 and if some_vec are both type errors. That one costs about four seconds and then never again.

Face two: the dedicated “if and else have incompatible types”. This is what the starter produces:

error[E0308]: `if` and `else` have incompatible types
   |
   |             "Fizz"
   |             ------ expected because of this
...
   |             i.to_string()
   |             ^^^^^^^^^^^^^ expected `&str`, found `String`

Three branches produce &str — borrowed text living in the binary — and one produces String — owned text on the heap. They are genuinely different types and the if has to be one of them. The fix is to make every branch produce a String, which is the type the Vec needs anyway.

Note what rustc did for you: it named the branch that set the expectation and the branch that disagreed. Reading both halves of that message is the whole skill from item 0.2.

is_multiple_of instead of % == 0

You would probably write i % 15 == 0, and clippy 1.95 will stop you: manual_is_multiple_of fires on that for unsigned integers and suggests i.is_multiple_of(15).

This is a genuinely new method — stabilised in Rust 1.87 — and the lint has a real argument behind it: is_multiple_of says the intent in words, and it does not silently panic when the divisor turns out to be zero, whereas % 0 does. It only exists for unsigned types, which is why you will still write % 2 == 0 for an i32 and see no lint.

Two more default-on lints in the neighbourhood

  • if_same_then_else — two branches with identical bodies, which is either a copy-paste bug or a chain that wants collapsing.
  • needless_boolif c { true } else { false } is c.

Both are the kind of thing you write at 2am, and both are free.