In functional languages, all terms are expressions. There are no statements in function bodies, only a single expression. All control flow operators are then formulated as expressions with a return value. In Rust, this is almost the case; the only non-expressions are let statements and item declarations.
Both of these statements can be wrapped in blocks to create an expression along with any other term. An example for this is the following, in intro_expressions.rs:
let x = {
fn f(x: u32) -> u32 {
x * x
}
let y = f(5);
y * 3
};
This nested format is uncommon in the wild, but it illustrates the permissive nature of Rust grammar.
Returning to the concept of functional style expressions, the emphasis should always be on writing legible literate code without much hassle or bloat. When someone else, or you at a later time, comes to read your code, it should be immediately understandable. Ideally, the code should document itself. If you find yourself constantly writing code twice, once in code and again as comments, then you should reconsider how effective your programming practices really are.
To start with some examples of functional expressions, let's look at an expression that exists in most languages, the ternary conditional operator. In a normal if statement, the condition must occupy its own line and thus cannot be used as a sub-expression.
The following is a traditional if statement, initializing a variable in intro_expressions.rs:
let x;
if true {
x = 1;
} else {
x = 2;
}
With the ternary operator, this assignment can be moved to a single line, shown as follows in intro_expressions.rs:
let x = if true { 1 } else { 2 };
Almost every statement from OOP in Rust is also an expression—if, for, while, and so on. One of the more unique expressions to see in Rust that is uncommon in OOP languages is direct constructor expressions. All Rust types can be instantiated by single expressions. Constructors are only necessary in specific cases, for example, when an internal field requires complex initialization. The following is a simple struct and an equivalent tuple in intro_expressions.rs:
struct MyStruct
{
a: u32,
b: f32,
c: String
}
fn main()
{
MyStruct {
a: 1,
b: 1.0,
c: "".to_string()
};
(1, 1.0, "".to_string());
}
Another distinctive expression from functional languages is pattern matching. Pattern matching can be thought of as a more powerful version of a switch statement. Any expression can be sent into a pattern expression and de-structured to bind internal information into local variables before executing a branch expression. Pattern expressions are uniquely suited for working with enums. The two make a perfect pair.
The following snippet defines a Term as a tagged union of expression options. In the main function, a Term t is constructed, then matched with a pattern expression. Note the syntax similarity between the definition of a tagged union and the matching inside of a pattern expression in intro_expressions.rs:
enum Term
{
TermVal { value: String },
TermVar { symbol: String },
TermApp { f: Box<Term>, x: Box<Term> },
TermAbs { arg: String, body: Box<Term> }
}
fn main()
{
let mut t = Term::TermVar {
symbol: "".to_string()
};
match t {
Term::TermVal { value: v1 } => v1,
Term::TermVar { symbol: v1 } => v1,
Term::TermApp { f: ref v1, x: ref v2 } =>
"TermApp(?,?)".to_string(),
Term::TermAbs { arg: ref mut v1, body: ref mut v2 } =>
"TermAbs(?,?)".to_string()
};
}