Rust resembles a functional language in many ways although it does not claim to be one. In fact, I have been thinking of Rust as a “pragmatic Haskell” or as a “well-balanced mixture between C++ and Haskell”.
One of the ways the functional aspects show up is via expressions and how pretty much any construct in Rust can be treated as an expression. But before we begin, a little warning: the examples below are, by no means, idiomatic Rust—I just hope they are simple enough to illustrate what I want to show.
First of all, functions can have return statements—but the preferred style is to avoid them. Instead, the preferred style is to let the evaluation of the last statement serve as the return value, and doing so is as simple as eliding the semicolon of that last statement. This felt weird and confusing at first, but in the end this syntax quirk is not a big deal and it actually feels quite natural. To show this, consider the following function:
fn get_color_with_default(&self, default_color: Color) -> Color {
if self.color == None {
return default_color;
}
return self.color.unwrap();
}
In a previous post titled “Readability: Conditionals as functions”, I argued that you should structure your function by thinking of it as a set of cases, each with an explicit return value. To apply that idea here, we expand the conditional to make all of its branches explicit:
fn get_color_with_default(&self, default_color: Color) -> Color {
if self.color == None {
return default_color;
} else {
return self.color.unwrap();
}
}
And once we have this, we can transform the code by dropping the return
keywords and the trailing semicolons, thus allowing the conditional statement to yield the function’s return value:
fn get_color_with_default(&self, default_color: Color) -> Color {
if self.color == None {
default_color
} else {
self.color.unwrap()
}
}
When written this way, the function becomes just an expression. You can imagine more elaborate versions of the above with nested conditionals and auxiliary variable definitions, but in the end, most functions can be expressed as just that: complex expressions that transform input values into output values.
But let’s take a closer look to the last example: note that we have two “final” statements, one per conditional branch. This leads me to the second place where we can tell that everything is really an expression, and this includes conditional statements, loops, and even inner blocks. All of these constructions can “return” a value which can be assigned to a variable. For example, we can take the conditional statement from the example above and turn it into the value assigned to a variable:
let color = if self.color == None {
default_color
} else {
self.color.unwrap()
};
This feature helps in writing clean code because it lets you abide by the principle of only assigning variables once even if their value is derived from a complex computation, which I previously presented in “Readability: Don’t modify variables”. And this also helps in narrowing the scope of each variable definition to the smallest piece of code in which it is relevant.
There are many more details of the language that make this a reality and which remind me of Haskell. The final one I’d like to mention is the collection of functional types that plague the standard library, like Option
and Some
, and the many places where high-order functions such as maps and filters show up. These are all designed to ensure that the computation of a value can be expressed as a single expression, without intermediate statements and without having to worry about how exactly the computation is executed.
Oh, and by the way: did you notice I referenced two of my previous Readability posts in the text above? One of the reasons I’m enjoying Rust so far is precisely this: the principles of the language are closely aligned with my views on how readable code looks like, and this language kinda forces you to write code that way.