Skip to content

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

Easy Primitives

Functions: parameters, return types, early return

Find the index of the first negative value in a slice, or -1 if there is none.

pub fn first_negative_index(values: &[i32]) -> i32

[1, 2, -3] gives 2. [] gives -1. [-1] gives 0.

The starter is correct, and fails the gate on two separate lints.

Signatures are the inference boundary

Rust infers types aggressively inside a function body and not at all at its edges. Every parameter and the return type must be written out. That is a deliberate design decision with three payoffs:

  • Errors stay local. If inference crossed function boundaries, a change in one function could produce a type error three files away. Rust guarantees the error is where the mistake is.
  • Signatures are documentation that the compiler checks. -> Option<u8> tells you something -> u8 cannot.
  • Public API is stable by construction. You cannot accidentally change a function’s type by editing its body.

Three error codes live at this boundary and you should recognise them:

  • E0308 — you returned the wrong type, or passed one.
  • E0061 — wrong number of arguments. Rust has no default parameters, no optional parameters and no overloading; a function takes exactly what it says.
  • E0425 — “cannot find function in this scope”, which usually means a typo, a missing pub, or a call to something you have not defined.

When return is right

Item 1.12 removed the trailing return. It did not remove return from the language. return is for leaving a function early, from the middle, and this problem is the clean example: the moment you find a negative, there is no reason to keep scanning.

for (i, &v) in values.iter().enumerate() {
    if v < 0 {
        return i as i32;   // early: correct, idiomatic, keep it
    }
}
-1                          // tail expression: no `return` here

That contrast — early return inside the loop, bare tail expression at the end — is the whole shape of idiomatic Rust control flow. Clippy’s needless_return only ever complains about the second kind, and the starter has one.

enumerate() turns an iterator of items into an iterator of (index, item) pairs, with the index as a usize starting at zero. The pattern (i, &v) destructures the pair and dereferences the item in one step.

The other lint: dead_code

The starter also defines a helper is_negative and never calls it. That is dead_code, a rustc lint, and under -D warnings it is a hard error.

Be clear about what this means for you: a correct submission with one unused helper fails. That is legitimate friction, not a bug in the grader, and it will bite you when you write a helper, find a better approach, and forget to delete the first attempt. When it happens, the fix is usually to delete the code — and occasionally to actually call it.

For the record, the escape hatches are #[allow(dead_code)] on the item, or better #[expect(dead_code)], which additionally warns you if the item stops being dead. Reach for them rarely and deliberately.

One small conversion

enumerate gives you a usize; the signature promises an i32. as i32 is fine here — the test data is small and the truncation cannot occur — and it is worth noticing that you have now met the situation item 1.11 warned about, and consciously decided that as is acceptable. That decision, made on purpose, is the whole point of “TryFrom by default, as on purpose”.