Functions
A function takes zero or more input values and optionally returns an output value. By convention, we call the input values parameters in the callee, and arguments in the caller.
# #![allow(unused_variables)] #fn main() { fn foo() { } fn bar(x: i32, y: &str) { } fn baz(mut x: i32) -> i32 { x += 1; x } foo(); bar(1, "Hello, world"); assert_eq!(baz(1), 2); #}
Type annotation
When calling a function with arguments, Rust binds the function parameters to these arguments. For example,
# #![allow(unused_variables)] #fn main() { fn foo(x: i32, y: &str) { } // The next line implicitly executes: // let x: i32 = 1; // let y: &str = "hello, world"; foo(1, "hello, world"); #}
Previously, we stated that a program may elide type annotations in the let
statement and let the compiler infer the types. By contrast, Rust requires type annotations in function definitions. This is because type annotations allow the compiler to detect type errors. This is especially useful for functions, because often its implementor and user are different. The implementor uses type annotations to establish a contract, and the compiler ensures that the function user must fulfill the contract. By contrast, since the scope of a variable is a block and a block is usually written by the same person, enforcing the contract is less important. Moreover, type inference provides a significant ergonomic benefit without sacrificing safety in a common case where the program both binds and initializes a variable in the let
statement.
# #![allow(unused_variables)] #fn main() { let x = "hello, world"; // The above is equivalent to // `let x: &str = "hello, world";` let y = [1, 2, 3]; // The above is equivalent to // `let y: [i32; 3] = [1, 2, 3];` #}
This avoids a common stutter in Java, where the compiler should be able to infer the type of foo
based on the type of the value Foo()
:
Foo foo = new Foo();
Return values
A function may return a value by either using the return
statement or placing the value on the last line executed in the function.
# #![allow(unused_variables)] #fn main() { fn f() -> i32 { return 1; } fn g(x: bool) -> i32 { if x { // Returns 1 1 } else { // Returns -1 -1 } } #}
Note in g()
, the lines that return the value don't end in a semicolon.
Expression vs. statement
Rust is primarily an expression language, meaning that most code lines that produce values or cause effects (e.g., print) are expressions. An expression returns a value but a statement doesn't.
Rust has only two types of statements.
- Declaration statement
- Item declaration. This statement declares an item, such as a function, enumeration, struct, type, static, trait, implementation, or module (to be discussed in later chapters).
let
statement. This statement binds variables.
- Expression statement. This statement consists of an expression followed by a semicolon. It evaluates the expression and then discards its result, therefore aiming for only its side effect, e.g.,
println!()
. This is useful to contain and explicitly sequence expression evaluation. Therefore, if a function wishes to return the value produced on its last line, the line must not end in a semicolon.1
It is OK to end a return
statement in a semicolon, although unnecessary.
If you came from C/C++, you may be surprised that the value of an assignment expression is the empty tuple ()
, also called the unit type, rather than the value being assigned. For example:
# #![allow(unused_variables)] #fn main() { let x; // The value of `y` is `()` instead of `1`. let y = (x = 1); assert_eq!(y, ()); #}
Function type
The function type, fn()
, is another primitive type. A function type specifies the types of all the parameters and that of the return value, if any. The value of a function type contains the pointer to a function.
# #![allow(unused_variables)] #fn main() { fn square(x: i32) -> i32 { x * x } let f: fn(i32) -> i32; f = square; assert_eq!(f(2), 4); fn print(x: &str) { println!("{}", x); } let g: fn(&str) = print; g("hello"); #}