Skip to content

Listing 2.2, run: variables and functions

Rust in Action, run › Chapter 2 › §2.2.1 · the book's file: ch2/ch2-first-steps.rs ↗

Level: 101 · a companion to Rust in Action, chapter 2

One line: Listing 2.2 compiles unchanged on rustc 1.98.0 and prints ( a + b ) + ( c + d ) = 90, not the a + b = 30 promised by the paragraph that introduces it. Its notes hold up, with one exception: a semicolon after i + j does not make add return (). It makes add fail to compile.

What the listing prints

The book's file, compiled as it ships (edition 2024, no warnings):

ch2-first-steps.rs, run on rustc 1.98.0
( a + b ) + ( c + d ) = 90

e is add(add(10, 20), add(30, 30)). The a + b = 30 in the introduction describes a smaller program than the one printed under it.

Eight claims, run

Verified output of first_steps.rs — regenerated by tools/run_examples.py, never hand-typed.

1. the listing's e = 90
2. a is i32, wide is i64, alone is i32
3. b is i32, c is i32, d is i32, c == d: true
4. size was assigned once, after its let, with no mut: big
5. println! is an expression...
   ...whose value is (); format! gives a String: "90"
6. add is below main, double is inside it: 6
7. sign(-5) = negative, sign(5) = not negative
8. 'é' is a char of 4 bytes; "é" is a &str holding 2 bytes
  1. The 90. The first line recomputes the listing's e from its four values.
  2. "Types can be inferred" — from the use, not from the literal. a is i32 because it is passed to add(i: i32, …). The same 10 passed to a function that takes i64 is an i64. Only a literal that nothing constrains falls back to i32. → Type inference, the two fallbacks
  3. let b: i32, 30i32 and 30_i32 write one type three ways. The annotation goes on the name, the suffix on the number, and the _ before a suffix is only there to make it easier to read. The book says that at times the annotation is required. The case everybody meets first is "42".parse() with no target type, which is E0284. → What a type annotation does, the suffix is the type
  4. Immutable means assigned once, not assigned on the let line. let size; followed by one assignment in each branch compiles without mut. A second assignment to the same name is E0384. A second let is a new variable (shadowing) and needs no mut either. → Variables, The shadowing map
  5. A macro call becomes code, and the code has a value. The book says a macro returns code rather than values. The expansion happens at compile time, and what it expands into is an ordinary expression: println!(…) is worth () and format!(…) is worth a String. → Macros
  6. Items can go in any order. add sits below main in the listing, and a fn may even be declared inside another function's body. let bindings work the other way: a name exists only from its let downwards.
  7. The last expression is the return value, and return still exists for leaving early, which is what the first line of sign does. → A block is an expression
  8. Single quotes make a char, a type of its own. A char is one Unicode scalar value and always four bytes. The same é in double quotes is a &str of two UTF-8 bytes. Not everything a reader sees as one character fits in a char; see the refusal below. → Meet the char, Why char is 32 bits

What the compiler refuses

Every transcript here is rustc 1.98.0 compiling a file with the name shown in the title.

A semicolon after i + j is an error, not a unit return

The note under the listing says a semicolon here makes add return () instead of i32. With -> i32 still in the signature, rustc refuses:

rustc 1.98.0 output for semicolon_on_the_tail.rs
error[E0308]: mismatched types
 --> semicolon_on_the_tail.rs:5:27
  |
5 | fn add(i: i32, j: i32) -> i32 {
  |    ---                    ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
6 |     i + j;
  |          - help: remove this semicolon to return this value

The body's last line is now a statement, so the body is worth (), and the signature promised an i32. A function returns () only when its signature has no ->, and that function refuses a last line with a value, the same error the other way round:

rustc 1.98.0 output for no_return_type.rs
error[E0308]: mismatched types
 --> no_return_type.rs:6:5
  |
5 | fn add(i: i32, j: i32) {
  |                       - help: try adding a return type: `-> i32`
6 |     i + j
  |     ^^^^^ expected `()`, found `i32`

→ The semicolon is the switch, The unit type ()

Parameters need types; the return type does not

The book says type declarations are required when defining functions. That is true of every parameter:

Abridged — the first of two errors in rustc 1.98.0 output for untyped_parameters.rs
error: expected one of `:`, `@`, or `|`, found `,`
 --> untyped_parameters.rs:5:9
  |
5 | fn add(i, j) -> i32 {
  |         ^ expected one of `:`, `@`, or `|`
  |
help: if this is a parameter name, give it a type
  |
5 | fn add(i: TypeName, j) -> i32 {
  |         ++++++++++

The return type may be left out, and leaving it out means -> (), which is the second transcript above.

{} is checked against Display at compile time

The book says println! works out which methods to call on its arguments. It does that while compiling: {} requires Display ↗, {:?} requires Debug, and a type without the one you asked for does not compile:

Abridged — rustc 1.98.0 output for display_a_vec.rs
error[E0277]: `Vec<{integer}>` doesn't implement `std::fmt::Display`
 --> display_a_vec.rs:3:20
  |
3 |     println!("{}", v);
  |               --   ^ `Vec<{integer}>` cannot be formatted with the default formatter
  |               |
  |               required by this formatting parameter
  |
  = help: the trait `std::fmt::Display` is not implemented for `Vec<{integer}>`
  = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead

→ Debug and Display, The format mini-language

One char is one code point, not one visible character

Abridged — rustc 1.98.0 output for two_codepoint_char.rs
error: character literal may only contain one codepoint
 --> two_codepoint_char.rs:2:18
  |
2 |     let thumbs = '\u{1F44D}\u{1F3FD}';
  |                  ^^^^^^^^^^^^^^^^^^^^
  |
help: if you meant to write a string literal, use double quotes

A thumbs-up with a skin tone is two code points, so it needs double quotes. → Grapheme clusters, in Meet the char

main starts a binary, not every crate

A binary without main is E0601 ↗, "main function not found in crate". The same file compiles with rustc --crate-type lib, because a library has no entry point.

The book's footnote says main returns () or a Result. The actual rule is any type that implements Termination ↗. That includes ExitCode: fn main() -> ExitCode { ExitCode::from(3) } exits with status 3. A String is refused with E0277, "main can only return types that implement Termination". → Running a scratch program

Line numbers and names in the prose

  • The text on p. 36 says line 5 is let c = 30_i32;. In the listing, line 4 is let c = 30i32; and line 5 is let d = 30_i32;.
  • The same page puts fn add(…) -> i32 { on line 10. It is line 11; line 10 is blank.
  • The book calls -> "the dagger" or "thin arrow". Neither the Book nor the Reference uses either name: the Book's operator appendix ↗ lists it as the function and closure return type, and the Reference's grammar ↗ names -> Type a FunctionReturnType.
  • The book calls code blocks "lexical scopes". A block is a scope and an expression, and the second job is what makes claim 7 work.

Concepts, keywords, topics

In the listing Keyword or concept Lesson here Normative source
fn main() { fn, the entry point Functions · Running a scratch program Main functions ↗
let a = 10; let, binding, type inference Variables · Type inference let statements ↗
let b: i32 = 20; type annotation, i32 What a type annotation does · Values Integer types ↗
30i32, 30_i32 literal suffix, digit separator Writing a number down Integer literals ↗
add(add(a, b), add(c, d)) call expression Functions Call expressions ↗
println!("…{}", e) macro, Display, format string Macros · Debug and Display · The format mini-language Macros ↗ · std::fmt ↗
-> i32 return type The unit type () Functions ↗
i + j tail expression, block expression A block is an expression Block expressions ↗
; statement The semicolon is the switch Expression statements ↗
mut, by its absence immutability, shadowing The shadowing map —
'a' versus "a", on p. 36 char, &str Meet the char · String vs &str Character literals ↗

Short definitions of type annotation, integer fallback, tail expression, unit type, binding mutability, shadowing, char, E0284 and E0308 are in the glossary. Every error code above has its home lesson in the error index.

See also

Po polsku

Listing 2.2 kompiluje się na rustc 1.98.0 bez zmian i bez ostrzeżeń, ale wypisuje ( a + b ) + ( c + d ) = 90, a nie zapowiadane w tekście a + b = 30. Sprawdzone twierdzenia: typ zmiennej bez adnotacji (type inference) wynika z użycia — a jest i32, bo trafia do add(i: i32, …), a ta sama 10 przekazana funkcji z i64 staje się i64. Adnotacja : i32, przyrostek 30i32 i 30_i32 to jeden typ zapisany na trzy sposoby. Niemutowalność znaczy jedno przypisanie: let size; i przypisanie później kompiluje się bez mut.

Najważniejsza poprawka dotyczy notatki o średniku. Średnik po i + j nie sprawia, że add zwraca () — przy -> i32 w sygnaturze funkcja przestaje się kompilować (E0308, mismatched types), a kompilator sam podpowiada usunięcie średnika. Makro (println!) rozwija się w kod w czasie kompilacji, a ten kod ma wartość: println! daje (), format! daje String. Pojedyncze cudzysłowy tworzą char — jeden punkt kodowy Unicode, zawsze cztery bajty — więc kciuk z odcieniem skóry (dwa punkty kodowe) wymaga cudzysłowów podwójnych.

Szukaj po polsku: wnioskowanie typów w Ruscie · adnotacja typu · wyrażenie końcowe bloku · rust E0308 remove this semicolon · rust main ExitCode Termination