Expressions
An expression computes a value. Literals, arithmetic, function calls, blocks, ifif and matchmatch, and even letlet bindings are all expressions in Reussir. This chapter begins with the everyday value-producing forms, then builds up to first-class functions and closures.
The short examples can be entered directly in rreplrrepl. Definitions and longer examples can also be placed in a project’s .rr.rr source file.
Reussir has integer, floating-point, Boolean, character, and string literals:
421_000_0000xff0o7550b10103.51e32.5e-7truefalse'R'"hello"
421_000_0000xff0o7550b10103.51e32.5e-7truefalse'R'"hello"
The radix prefixes apply only to integers. Underscores may separate digits and do not change the value. String and character escapes follow the familiar Rust spellings, including \n\n, \xNN\xNN, and \u{...}\u{...}.
A numeric literal does not choose a machine type by itself. Its surrounding expression supplies the type:
fn retry_limit() -> u32 { 1_000}fn scale(x: f32) -> f32 { x * 0.5}
fn retry_limit() -> u32 { 1_000}fn scale(x: f32) -> f32 { x * 0.5}
This lets the same spelling participate in i8i8 through i64i64, u8u8 through u64u64, and the supported floating-point formats. At the interactive prompt, an otherwise unconstrained integer defaults to i64i64 and an unconstrained floating-point literal defaults to f64f64:
λ> 1 + 23 : i64λ> 1.5 * 2.03.0 : f64
λ> 1 + 23 : i64λ> 1.5 * 2.03.0 : f64
A block is a sequence of expressions inside braces. Expressions are separated by semicolons, and the value of the final expression becomes the value of the block:
fn answer() -> i64 { let base = 40; let extra: i64 = 2; base + extra}
fn answer() -> i64 { let base = 40; let extra: i64 = 2; base + extra}
letlet introduces a name for the rest of its enclosing block. Its type annotation is optional when the value determines the type. The binding itself has type unitunit; it is normally followed by the expressions that use the name.
Unlike Rust, a trailing semicolon does not discard the last value. Both functions below return 4242:
fn with_semicolon() -> i64 { 40 + 2;}fn without_semicolon() -> i64 { 40 + 2}
fn with_semicolon() -> i64 { 40 + 2;}fn without_semicolon() -> i64 { 40 + 2}
An empty block, {}{}, has type unitunit.
The five arithmetic operators are ++, --, **, //, and %%. Their operands have one common numeric type, and the result has that type as well. Division therefore follows the selected type: 17 / 517 / 5 is 33 for integers, while floating-point division retains the fractional part.
Prefix -- negates a numeric expression. Prefix !! negates a Boolean. Comparisons produce boolbool, and &&&& and |||| combine Boolean operands.
Binary operators bind in the following order, from tightest to loosest:
| Order | Operators | Meaning |
|---|---|---|
| 1 | ** // %% |
multiplication, division, remainder |
| 2 | ++ -- |
addition and subtraction |
| 3 | ==== !=!= << >> <=<= >=>= |
equality and ordering |
| 4 | &&&& |
Boolean and |
| 5 | |||| |
Boolean or |
Calls, field access, casts, and prefix operators bind before these binary operators. Arithmetic and Boolean chains associate to the left, so a - b - ca - b - c means (a - b) - c(a - b) - c. Parentheses override the normal grouping:
1 + 2 * 3 // 7(1 + 2) * 3 // 9
1 + 2 * 3 // 7(1 + 2) * 3 // 9
Comparisons deliberately do not chain. Write the two comparisons explicitly:
low <= x && x < high // acceptedlow <= x < high // error: comparisons cannot be chained
low <= x && x < high // acceptedlow <= x < high // error: comparisons cannot be chained
&&&& and |||| currently evaluate both operands as ordinary value expressions; they are not short-circuiting control-flow operators. Use an ifif expression when the right-hand expression must run conditionally.
The asas operator converts between numeric types. For example, integer inputs can be converted before floating-point division:
fn mean(total: i64, count: i64) -> f64 { (total as f64) / (count as f64)}
fn mean(total: i64, count: i64) -> f64 { (total as f64) / (count as f64)}
Both the source and destination of asas must be numeric. Parenthesize nested casts or combinations whose grouping should be unmistakable.
A function declaration gives every parameter a type. Write -> T-> T after the parameter list when it returns a value of type TT; omit it for a function returning unitunit.
fn square(x: i64) -> i64 { x * x}fn sum_of_squares(x: i64, y: i64) -> i64 { square(x) + square(y)}
fn square(x: i64) -> i64 { x * x}fn sum_of_squares(x: i64, y: i64) -> i64 { square(x) + square(y)}
A call supplies comma-separated arguments in parentheses. Arguments are checked against the declared parameter types, and nested calls compose like any other expressions.
Generic parameters follow the function name. Reussir normally infers their arguments from values and the expected result, but they may be written explicitly between << and >>:
fn identity<T>(value: T) -> T { value}fn inferred() -> i32 { identity(42)}fn explicit() -> i32 { identity<i32>(42)}
fn identity<T>(value: T) -> T { value}fn inferred() -> i32 { identity(42)}fn explicit() -> i32 { identity<i32>(42)}
A named function is also usable as a first-class function value. Reussir lifts it to a closure of the same parameter shape when a closure is expected:
fn double(x: i32) -> i32 { x * 2}fn apply_twice(f: i32 -> i32, x: i32) -> i32 { f(f(x))}fn doubled_twice() -> i32 { apply_twice(double, 10) // 40}
fn double(x: i32) -> i32 { x * 2}fn apply_twice(f: i32 -> i32, x: i32) -> i32 { f(f(x))}fn doubled_twice() -> i32 { apply_twice(double, 10) // 40}
A lambda writes its parameters between vertical bars, followed by its body:
|x: i32| x + 1|x: i32, y: i32| x + y|| 42
|x: i32| x + 1|x: i32, y: i32| x + y|| 42
The body is one expression. Use a block when it needs local bindings or several steps:
fn adjusted_square(x: i32) -> i32 { let calculate = |value: i32| -> i32 { let squared = value * value; squared + 1 }; calculate(x)}
fn adjusted_square(x: i32) -> i32 { let calculate = |value: i32| -> i32 { let squared = value * value; squared + 1 }; calculate(x)}
The return annotation after the closing || is optional. Parameter annotations may also be omitted when an expected closure type supplies them. Here the parameter of |x| x + 1|x| x + 1 is known to be i32i32 from applyapply:
fn apply(f: i32 -> i32, value: i32) -> i32 { f(value)}fn increment() -> i32 { apply(|x| x + 1, 41)}
fn apply(f: i32 -> i32, value: i32) -> i32 { f(value)}fn increment() -> i32 { apply(|x| x + 1, 41)}
When no expected closure type exists, annotate lambda parameters so inference has a concrete starting point.
A lambda automatically captures names that it reads from the surrounding scope. No capture list or movemove marker is needed:
fn make_adder(offset: i32) -> i32 -> i32 { |value: i32| value + offset}fn use_capture() -> i32 { let add_five = make_adder(5); add_five(37) // 42}
fn make_adder(offset: i32) -> i32 -> i32 { |value: i32| value + offset}fn use_capture() -> i32 { let add_five = make_adder(5); add_five(37) // 42}
In the first function, offsetoffset becomes part of the returned closure. The type i32 -> i32i32 -> i32 describes a one-parameter closure. Parenthesized parameter types describe an n-ary closure, such as (i32, bool) -> i32(i32, bool) -> i32. Arrow types associate to the right, so i32 -> i32 -> i32i32 -> i32 -> i32 means i32 -> (i32 -> i32)i32 -> (i32 -> i32).
Lambdas can be invoked immediately, and a call can follow any expression that produces a closure:
fn immediate() -> i32 { (|x: i32| x * x)(6)}fn chained() -> i32 { make_adder(20)(22)}
fn immediate() -> i32 { (|x: i32| x * x)(6)}fn chained() -> i32 { make_adder(20)(22)}
Reussir permits a closure or named function to receive fewer arguments than its type declares. The supplied values fill the leading parameters, and the result is a new closure over the parameters that remain.
fn add3(a: i32, b: i32, c: i32) -> i32 { a + b + c}fn staged_application() -> i32 { let after_a: (i32, i32) -> i32 = add3(10); let after_b: i32 -> i32 = after_a(20); after_b(12) // 42}
fn add3(a: i32, b: i32, c: i32) -> i32 { a + b + c}fn staged_application() -> i32 { let after_a: (i32, i32) -> i32 = add3(10); let after_b: i32 -> i32 = after_a(20); after_b(12) // 42}
The same application can be written in several equivalent ways:
add3(10, 20, 12)add3(10, 20)(12)add3(10)(20, 12)add3(10)(20)(12)
add3(10, 20, 12)add3(10, 20)(12)add3(10)(20, 12)add3(10)(20)(12)
This is partial application, not automatic currying. The original function’s type remains the three-parameter type (i32, i32, i32) -> i32(i32, i32, i32) -> i32; Reussir does not silently rewrite it as i32 -> i32 -> i32 -> i32i32 -> i32 -> i32 -> i32.
The distinction matters when functions are passed as values. A two-parameter function does not satisfy a higher-order parameter that requests an explicitly curried function:
fn add(a: i32, b: i32) -> i32 { a + b}fn apply_curried(f: i32 -> i32 -> i32) -> i32 { f(20)(22)}fn wrong_shape() -> i32 { apply_curried(add)}
fn add(a: i32, b: i32) -> i32 { a + b}fn apply_curried(f: i32 -> i32 -> i32) -> i32 { f(20)(22)}fn wrong_shape() -> i32 { apply_curried(add)}
The last call is rejected with the essential type mismatch:
expected `i32 -> i32 -> i32`, found `(i32, i32) -> i32`
expected `i32 -> i32 -> i32`, found `(i32, i32) -> i32`
If a curried type is part of the interface, write that structure explicitly by returning a closure:
fn curried_add(a: i32) -> i32 -> i32 { |b: i32| a + b}fn right_shape() -> i32 { apply_curried(curried_add)}
fn curried_add(a: i32) -> i32 -> i32 { |b: i32| a + b}fn right_shape() -> i32 { apply_curried(curried_add)}
Supplying more arguments than a function or closure declares is always an error. Partial application can leave parameters for a later call; it never pushes excess arguments through a completed call automatically.