At this point, we are already somewhat familiar with functions. Since we need to use the main
function, which is the entry point of a Rust program, and we have seen a couple of examples in previous lessons, we understand how a function in Rust looks. However, in this lesson, we are going to learn the ins and outs of functions.
Functions
fn say_hello() {
println!("Hello, world!");
}
fn main() {
say_hello();
}
// Hello, world!
We define a function in Rust using the keyword fn
, followed by the name of the function. Like variables, the function name follows the same convention: it should be in snake_case, such as say_hello
in the example above.
fn say_hello(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
say_hello("John");
}
// Hello, John!
If we want to pass data to a function, we can do so using function arguments. These need to be specified in parentheses ()
along with their data types. If a function has more than one argument, they must be separated by commas. In the example above, the say_hello
function accepts only one argument of type &str
.
fn say_hello(name: &str) -> String {
return format!("Hello, {}!", name);
}
fn main() {
let message: String = say_hello("John");
println!("{}", message);
}
// Hello, John!
Return values
A function can also return a value. If a function returns a value, it must be specified using the ->
annotation followed by the return type. In the example above, the say_hello
function returns a String
value. To return a value, we use the return
statement followed by the value to be returned. Ideally, this should be the last statement in the function body, but it can appear anywhere in the code.
fn say_hello(name: &str) -> String {
if name == "unknown" {
return format!("Hello, world!");
}
return format!("Hello, {}!", name);
}
fn main() {
println!("[1] {}", say_hello("John"));
println!("[2] {}", say_hello("unknown"));
}
// [1] Hello, John!
// [2] Hello, world!
In the example above, we have an if
statement in the say_hello
function body. If the name
argument value is "unknown"
, the body of the if
block is executed, which contains a return
statement. When Rust encounters a return
statement, it immediately returns the specified value, and any statements or code below it are not executed.
fn say_hello(name: &str) -> String {
println!("Executing say_hello()");
format!("Hello, {}!", name)
}
fn main() {
println!("{}", say_hello("John"));
}
// Executing say_hello()
// Return: Hello, John!
To return a value from a function, we don’t necessarily need to use a return
statement. In the Control Flow lesson, we saw how if/else
can also be an expression and how its value is evaluated. The same concept applies to functions. In Rust, when the last line in a function body is an expression, the function implicitly returns that value. In the example above, since format!("Hello, {}!", name)
is the last line of code in the function body and it is an expression (with no semicolon at the end), it is returned.
fn say_hello(name: &str) -> String {
if name == "unknown" {
format!("Hello, world!")
} else {
format!("Hello, {}!", name)
}
}
fn main() {
println!("[1] {}", say_hello("John"));
println!("[2] {}", say_hello("unknown"));
}
// [1] Hello, John!
// [2] Hello, world!
In the example above, the say_hello
function implicitly returns one of the format!()
values based on the if
condition. But how does this happen? What we understood is that this implicit return only occurs when the last line of code in the function body is an expression, and none of the format!()
expressions are on the last line. To understand this better, let’s look at the code below.
fn say_hello(name: &str) -> String {
return if name == "unknown" {
format!("Hello, world!")
} else {
format!("Hello, {}!", name)
};
}
fn main() {
println!("[1] {}", say_hello("John"));
println!("[2] {}", say_hello("unknown"));
}
// [1] Hello, John!
// [2] Hello, world!
This example is the same as the previous one. However, the only change made was in the say_hello
function. Here, we are explicitly returning the if/else
expression. Therefore, we can see that the last line of code in the previous say_hello
function was indeed an expression; however, it was spread over multiple lines. Apologies for my incorrect use of terminology.
A function that doesn’t have an explicit or implicit return mechanism returns ()
(the unit type) by default.
fn say_hello(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let result = say_hello("John");
println!("result: {:?}", result);
}
// Hello, John!
// result: ()
In the example above, say_hello
doesn’t return anything, but if we still store its return value in a variable and print it, it would show up as ()
. It’s like having the function declaration as fn say_hello(name: &str) -> ()
. The main
function we are familiar with doesn’t take any arguments and returns ()
.
fn count_until(max: u8) -> String {
let mut counter: u8 = 0;
loop {
if counter == max {
return format!("Stopped at {}.", max);
}
println!("Current counter is: {}", counter);
counter += 1;
}
}
fn main() {
let result: String = count_until(3);
println!("result: {}", result);
}
$ cargo run
Current counter is: 0
Current counter is: 1
Current counter is: 2
result: Stopped at 3.
As we learned, a return
statement not only returns a value but can also return a value early, terminating the function execution in the process. If there is a loop inside a function, whether it’s a for
, while
, or loop
, the break
statement will break the loop but won’t terminate the function. However, a return
statement can do both. In the program above, when counter
reaches max
, return ...
is executed, which terminates both the loop and the function, returning a value.
We could also use
return;
without specifying a value to break the loop and terminate the function early without returning a value (actually, it returns()
).
No optional parameters (arguments)
A dynamic programming language like JavaScript or Python allows you to declare a function that takes optional or a variable number of arguments. With this feature, you can provide an argument or not, and the function will still work. For example, the following TypeScript function declares a sayHello
function with an optional argument name
. We can also give the name
argument a default value, such as function sayHello(name: string = "world")
.
function sayHello(name?: string): string {
if (!name) {
return "Hello, world!";
}
return `Hello, ${name}`;
}
console.log(sayHello("John")); // argument passed
console.log(sayHello()); // no argument passed
// "Hello, John"
// "Hello, world!"
However, Rust’s function syntax is quite strict in this case. In Rust, a function must declare all parameters, and all arguments must be explicitly provided when calling the function. The notion of an argument not being provided doesn’t exist in Rust. However, we can use the Option
enum to mimic the behavior of an argument being empty, while still complying with Rust’s rules.
fn say_hello(name: Option<&str>) -> String {
match name {
Some(text) => format!("Hello, {}!", text),
None => format!("Hello, world!"),
}
}
fn main() {
println!("[1] {}", say_hello(Some("John")));
println!("[2] {}", say_hello(None));
}
// [1] Hello, John!
// [2] Hello, world!
In the example above, instead of say_hello
accepting a &str
, it now accepts Option<&str>
. We will talk more about the Option
enum in detail in an upcoming lesson. The Option
enum has two variants: Some
and None
. The Some
variant can store a value. With the Option<&str>
type, the variant gets the type Some(&str)
, which means it stores a &str
value. Using a match
statement, we can determine whether the argument name
is Some
or None
. Through pattern matching inside a match
arm, we can extract the &str
value inside Some(&str)
and take appropriate action.
Here,
say_hello
returns aString
implicitly becausematch
is used as an expression, and since it’s the only expression, it is returned implicitly.
Recursive functions
Function recursion is supported in Rust, as in many other languages, because function calls are stored on the stack. When a function is called, a stack frame is created, which contains the function’s arguments, local variables, temporary data, and a return address (indicating where the program should return when the function exits). A recursive function calls itself repeatedly, creating additional stack frames. However, there must be a case where the function no longer calls itself and instead returns, which is when stack unwinding begins. This means that each function call returns (exists), and its stack frame is popped off the stack.
fn factorial(n: u32) -> u32 {
if n == 0 {
return 1;
}
return n * factorial(n - 1);
}
fn main() {
let result = factorial(4);
println!("4! = {}", result);
}
// 4! = 24
In the example above, we have a typical factorial
function that takes an argument n
of type u32
, whose factorial we need to calculate and return. The factorial of N
, denoted as N!
, is equal to N * (N-1)!
. For 4!
, it’s equal to 4 * 3!
, which is equal to 4 * 3 * 2!
, which is equal to 4 * 3 * 2 * 1!
, and finally 4 * 3 * 2 * 1 * 0!
. Since 0!
is 1
, we no longer need to continue. 0!
marks the base case and the end of our recursion.
stack building --->
let result = factorial(4)
* factorial(3)
* factorial(2)
* factorial(1)
* factorial(0);
<--- stack unwinding
We can visualize this as described above. When we call factorial(4)
, it checks if n == 0
and executes return 4 * factorial(3);
. This won’t return yet because factorial(3)
creates a new stack frame and must execute first. This process continues until factorial(0)
, which is the base case. At this point, it doesn’t create a new stack frame and returns immediately because if n == 0
matches, and return 1
is executed. Then, stack unwinding begins, meaning the previous function calls start returning one by one.
Function pointers
A function’s code is stored in the compiled binary and is referenced by the function’s name. For example, say_hello
is the name of a function that refers to its implementation stored in the binary. A function pointer is a pointer to this code.
fn say_hello(name: &str) -> String {
format!("Hello, {}!", name)
}
fn main() {
let say_hello_ptr: fn(&str) -> String = say_hello;
println!("{}", say_hello_ptr("John"));
}
// Hello, John
In the example above, say_hello_ptr
is a function pointer that points to the code of the say_hello
function. Function pointers are first-class citizens in Rust, meaning they can be stored in a variable, passed to functions as arguments, and returned from functions. Their type is the type of the function, and it is represented essentially as the function signature, but with the function and argument names stripped, such as fn(&str) -> String
in the case above.
Functions are immutable in Rust, meaning they can’t be modified at runtime. Therefore, we can’t mutate them through a function pointer. Unlike some data types, such as
String
orVec
, where data can be cloned using the.clone()
method, this concept does not apply to function pointers.
However, a function pointer is not exactly the same as an anonymous function, like those you might be familiar with in other programming languages. A closely related feature in Rust would be closures.
const
functions
A const
function (also called constant function) is similar to a regular function defined with the fn
keyword, but it uses the const
keyword as a prefix. However, unlike a regular function, a const
function can be evaluated at compile time. When used in a constant context, such as when providing the initial value of a constant or static variable, or determining the length of an array (which all must be known at compile time), the const
function call can be used, and the Rust compiler will evaluate it at compile time and substitute the result.
const fn get_squre(num: usize) -> usize {
num * num
}
const SQUARE_OF_2: usize = get_squre(2);
static SQUARE_OF_3: usize = get_squre(3);
fn main() {
let a: [i32; 4] = [1; get_squre(2)];
println!("a: {:?}", a);
}
// a: [1, 1, 1, 1]
In the above example, the get_squre
function is a const
function. Since value of const
and static
variables should be known at the compile time, we can call the get_squre()
function to provide that value since it’s evaluated at the compile time. Since array length should also be known at compile time, we can also use the return value of a const
function.
Usually, const
functions do not even end up in the compiled binary since they are evaluated at compile time and have no purpose at runtime, unless they are used at runtime. This happens when they are called in a non-constant context, such as when a const
function is used to assign the returned value to a let
variable, like let a = get_square(2)
, where the result is computed at runtime.
There are several limitations on const
functions since they are evaluated at compile time. Some of the common ones are as follows:
- No Heap Allocation: You cannot allocate memory on the heap from within the function, such as for
String
,Vec
, etc. Only stack-based, fixed-size data can be used. - No Mutability:
const
functions must be stateless and cannot create side effects, such as mutating a variable. - No I/O Operations: You cannot perform input/output operations (e.g., no file reading or writing) because I/O cannot be guaranteed to happen at compile time.
- No Non-Constant Function Calls: You can only call other functions within it if they are also marked as
const
.
Function optimization
#[inline(always)]
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
let result = add(1, 2);
println!("{}", result);
}
Sometimes compiler inlines the function code where the function has been called to avoid function call overhead at runtime. In the above example, #[inline(always)]
attribute on function add
will force it to expand where it is called. So while compilation, let result = add(1, 2)
would transform into let result = 1 + 2
. But inlining may cause other downsides like increasing the binary size and reduce debuggability. Therfore, it’s should be kept to Rust’s compiler to take an appropriate action or use different variations of #[inline]
macro as per our need.
- Use
#[inline]
for small, frequently called functions where avoiding a function call overhead would be beneficial. - Use
#[inline(always)]
for critical performance paths where function call overhead is unacceptable. - Use
#[inline(never)]
when inlining would increase binary size unnecessarily or when you want to explicitly avoid inlining for debugging purposes.
#[cold]
fn rare() {
// --snip--
}
#[hot]
fn common() {
// --snip--
}
We can also use the #[cold]
or #[hot]
attribute to instruct the compiler to optimize functions.
#[cold]
tells the compiler that this function is used rarely, so it should keep it out of the way to make frequently used code run faster.#[hot]
tells the compiler that this function is used frequently, so it should keep it handy and optimize it to run as fast as possible.
Closures
A closure in Rust is similar to JavaScript’s arrow function or Python’s lambda function. If you’re unfamiliar with them, let’s break it down. A normal function in Rust, defined with the fn
keyword, has a name and is immutable and stateless. However, a closure is much like a function but can capture data from its environment and is anonymous (meaning it doesn’t have a name). They are declared using the || <expr>
syntax. Since they are anonymous, they must either be assigned to a variable, passed as an argument, or returned as a value. Like function pointers, closures are also first-class citizens.
fn main() {
let say_hello = || println!("Hello, world!");
say_hello();
}
// Hello, world!
In the program above, say_hello
holds a closure. The closure doesn’t accept any arguments and returns nothing. The expression <expr>
in || <expr>
is println!("Hello, world!")
, which returns ()
. We can call say_hello
like a normal function, as closures are essentially functions.
You can also use a block
{}
as an expression if you have more than one line of code in the closure, likelet say_hello = || { println!("Hello, world!") };
.
fn main() {
let say_hello = |name: &str| format!("Hello, {}!", name);
println!("{}", say_hello("John"));
}
// Hello, John!
In the example above, the closure accepts an argument of type &str
and returns a String
value. We changed from println!
to format!
, which returns this String
value. But what is the type of the say_hello
variable? If you look at your IDE, you will see impl Fn(&str) -> String
inferred for it. So, what is impl Fn
? To understand this, we need to look at how closures are different from normal functions.
The most important difference, aside from being anonymous, is that closures can capture variables from their environment. What does that mean? Let’s try to demonstrate this with a normal function.
fn main() {
let num: i32 = 3;
// Error: can't capture dynamic environment in a fn item
fn add(a: i32) -> i32 {
a + num
}
let result = add(2);
println!("result: {}", result);
}
In the example above, we have declared a function add
that is only valid inside the main
function, but it tries to use num
, which is declared outside the add
function body. This is not allowed in Rust because normal functions are stateless and can’t remember or hold onto variables from the surrounding code where they were created. This essentially means that fn
functions can’t capture variables from their environment. However, closures are designed to capture data from their environment.
fn main() {
let num: i32 = 3;
let add = |a: i32| a + num;
let result = add(2);
println!("result: {}", result);
}
// result: 5
Now, add
is a closure rather than a function, and it can capture variables from its environment. The program above works just fine. If we look at the IDE again, you will see the type of the variable add
is impl Fn(i32) -> i32
. Fn
is a trait in Rust, which resides in Rust’s standard library and is part of the prelude. The impl
keyword indicates that this value implements the Fn
trait, which in this case is add
. Following the Fn
trait is the function signature, similar to what we saw with function pointers. But what exactly does Fn
do?
We can’t provide an explicit type of impl Trait
to a variable. If you try, for example, let add: impl Fn(i32) -> i32 = ...
, the compiler will throw an error:
`impl Trait` is not allowed in the type of variable bindings
`impl Trait` is only allowed in arguments and return types of functions and methods
Why does this happen? We will cover this in the Traits lesson. For now, we can rely on Rust’s type inference capabilities, and you should be able to visualize the type of the closure easily in your IDE.
There are three ways closures can capture variables from the environment:
- By reference: A closure can capture variables from its environment by immutable reference, meaning it does not take ownership of those variables. When it references these variables by their name, Rust internally provides a reference to the original variable. In the example above,
num
inside the closure body is just a reference tonum
. A closure that only captures its environment by reference automatically implements theFn
trait, which requires types to implement thecall
method, and Rust automatically implements this for such closures. - By mutable reference: A closure can also capture variables from its environment by mutable reference, allowing the closure to mutate these variables. Such closures automatically implement the
FnMut
trait, which requires types to implement thecall_mut
method. Rust automatically implements this method for closures that modify their environment. - By value: Lastly, a closure can take complete ownership of variables from its environment. Once these variables are moved into the closure, they can’t be reused outside of it. These closures implement the
FnOnce
trait, which requires types to implement thecall_once
method. TheOnce
inFnOnce
indicates that the closure can be called only once if it consumes variables. Rust automatically implementscall_once
method for such closures. This is wheremove
keyword also becomes important with closures.
Fn
trait
fn main() {
let v: Vec<i32> = vec![1, 2, 3];
let process = || {
for i in &v {
println!("i: {}", i);
}
};
process();
println!("v: {:?}", v);
}
$ cargo run
i: 1
i: 2
i: 3
v: [1, 2, 3]
In the example above, we have declared a variable v
that holds a Vector
of type i32
. The closure process
captures it by an immutable reference since it uses &v
in the for
loop. Therefore, process
implements the Fn
trait and gets the type impl Fn()
. Since process
did not take ownership of v
, we can still continue to use v
after the process
closure.
FnMut
trait
fn main() {
let mut v: Vec<i32> = vec![1, 2, 3];
let mut process = || {
v.push(4);
};
process();
println!("v: {:?}", v);
}
$ cargo run
v: [1, 2, 3, 4]
In this example, the process
closure is modifying the Vector v
by pushing a value into it. It uses the .push()
method of Vec
to do this. If you take a look at the definition of the .push()
method, you can see that it accepts a mutable reference &mut self
to the Vector. Because of this, process
captures v
as a mutable reference. In order to mutate v
, we need to mark it as mut
, and the process
closure must also be marked as mut
since modifying v
will change the internal state of process
.
Since the closure process
only captures its environment using a mutable reference, it implements the FnMut
trait and gets the type impl FnMut()
. In this case, v
hasn’t been moved into process
because it doesn’t take ownership of it, allowing us to use v
normally after the closure.
FnOnce
trait
fn main() {
let v: Vec<i32> = vec![1, 2, 3];
let process = || {
for i in v {
println!("i: {}", i);
}
};
process();
// Error: borrow of moved value: `v`
// println!("text: {:?}", v); // <-- uncomment
// Error: use of moved value: `process`
// process(); // <-- uncomment
}
$ cargo run
i: 1
i: 2
i: 3
In the example above, the for
loop takes ownership of v
since we removed &
. Therefore, v
is moved into the process
closure. Now, if we try to use v
anywhere after the process
closure declaration, we get the error as shown above. The borrow of moved value
error message tells us that println!
is trying to borrow a moved value, which is v
in this case. Since process
takes ownership of variables in its environment, it automatically implements the FnOnce
trait and is inferred to have the type impl FnOnce()
.
The
println!
macro uses a reference (borrow) of the value under the hood, instead of taking ownership of that value. Although we providedv
to it, the macro implicitly uses&v
due to its implementation.
We also can’t call the process
closure twice because the Vector v
has already been consumed by the for
loop. On the next process()
call, there is no v
left to pass to the for
loop.
move
keyword
In the example above, the process
closure implicitly takes ownership of v
because there is no other option but to take ownership of v
for the program to work. However, in cases where a closure would implicitly capture a variable by reference, we can instruct the closure to take ownership of it explicitly by using the move
keyword.
fn main() {
let v: Vec<i32> = vec![1, 2, 3];
let process = move || {
for i in &v {
println!("i: {}", i);
}
};
process();
process();
// Error: borrow of moved value: `v`
// println!("text: {:?}", v); // <-- uncomment
}
In this example, the process
closure would not normally take ownership of v
since we used &v
, meaning only a reference to it is captured by the closure. Therefore, we can still continue to use v
after the closure. However, when we use the move
keyword, we force the closure to take ownership of the variable it captures—in this case, v
. As a result, when we try to use v
again after the closure, we get the same borrow of moved value
error.
But how can we still call process()
again? Even though the process
closure is a move closure (due to the move
keyword), it doesn’t consume v
when the closure executes because the for
loop is only using a reference to v
(&v
). For this reason, the process
closure gets the impl Fn()
type, allowing us to call it as many times as we want.
Even after using the
move
keyword, closures do not take ownership of types likei32
,u8
,f32
,&str
, etc., since they implement theCopy
trait. This allows their values to be copied, so they can still be used in the move closure without being fully consumed.
Higher-order functions
Rust supports the functional programming paradigm by providing first-class support for both function pointers and closures. One of the key features of functional programming is the use of higher-order functions (HOFs). A higher-order function is one that either takes one or more functions as arguments, returns a function as a result, or both. By using higher-order functions, we can make our programs more declarative, increase reusability through function composition, and abstract away complex logic.
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn subtract(a: i32, b: i32) -> i32 {
a - b
}
fn operate_with_2(fun: fn(a: i32, b: i32) -> i32, value: i32) -> i32 {
fun(2, value)
}
fn main() {
let add_result = operate_with_2(add, 1);
println!("add: {}", add_result);
let subtract_result = operate_with_2(subtract, 1);
println!("subtract: {}", subtract_result);
}
$ cargo run
add: 3
subtract: 1
In the example above, operate_with_2
takes a function pointer as the first argument with the type fn(a: i32, b: i32) -> i32
and a second argument of type i32
. The idea is to call the incoming function pointer with the first argument as 2
and the second argument as the incoming value, then return the computed result. We have defined the add
and subtract
functions to pass them as function pointer arguments. So when we call operate_with_2(add, 1)
, it calls add(2,1)
internally and returns the result.
In order to achieve higher-order functions with function pointers, we need to define the functions beforehand, even if they are used only once. We can improve this with closures. Since closures are anonymous functions, we don’t have to define them beforehand, and they can be passed as values.
fn operate_with_2(fun: fn(a: i32, b: i32) -> i32, value: i32) -> i32 {
fun(2, value)
}
fn main() {
let add_result = operate_with_2(|a, b| a + b, 1);
println!("add: {}", add_result);
let subtract_result = operate_with_2(|a, b| a - b, 1);
println!("subtract: {}", subtract_result);
}
$ cargo run
add: 3
subtract: 1
In the above example, instead of passing a function pointer to operate_with_2
, we pass a closure. But as we learned, a closure has the type impl Fn
, impl FnMut
, or impl FnOnce
, while operate_with_2
accepts a function pointer of type fn
. So how does it work? It works because the closure we are passing doesn’t capture any variables from its environment, allowing Rust to coerce it into a function pointer. Rust automatically coerces closures into function pointers (and vice versa) if the closure does not capture any variables from its environment.
fn operate_with_2(fun: impl Fn(i32, i32) -> i32, value: i32) -> i32 {
fun(2, value)
}
fn main() {
let add_result = operate_with_2(|a, b| a + b, 1);
println!("add: {}", add_result);
let subtract_result = operate_with_2(|a, b| a - b, 1);
println!("subtract: {}", subtract_result);
}
$ cargo run
add: 3
subtract: 1
But if you would like to accept only a closure that might capture its environment, you can specify its type as impl Fn(i32, i32) -> i32
, like we did above. As we learned, this will only allow closures that capture variables from their environment using an immutable reference.
fn print_vector(v: Vec<i32>) -> impl Fn() {
move || {
for i in &v {
println!("i: {}", i);
}
}
}
fn main() {
let v = vec![1, 2, 3];
let printer = print_vector(v);
printer();
printer();
}
$ cargo run
i: 1
i: 2
i: 3
i: 1
i: 2
i: 3
In the above example, the print_vector
function accepts a Vector and returns a closure of type impl Fn()
. When we pass the Vector v
in the print_vector(v)
function call, it takes ownership of v
, meaning that v
can’t be used anymore after that. Inside the print_vector
function, we are returning a movable closure since we want the closure to take ownership of v
. However, since the for
loop inside the closure only borrows v
, ownership stays with the closure, and we can call this closure as many times as we want. Therefore, when we return it from the print_vector
function, we can give it the impl Fn()
type.
fn print_vector(v: Vec<i32>) -> impl FnOnce() {
move || {
for i in v {
println!("i: {}", i);
}
}
}
fn main() {
let v = vec![1, 2, 3];
let printer = print_vector(v);
printer();
// Error: use of moved value: `printer`
// printer(); // <-- uncomment
}
But if we want the returned closure to not be called more than once, we can mark the return type of print_vector
as impl FnOnce()
. For example, if the for
loop inside the closure takes ownership of v
, once the closure is called, it can’t be called again since v
is consumed in the first call. Therefore, as in the program above, we changed the return type of the closure to impl FnOnce()
, which will prevent printer()
from being called twice.
Higher-order functions are a core part of Rust’s standard library. For example, if you want to transform each element of a collection like a Vector
, you can use the .map()
method on an iterator, which requires a closure to perform the transformation.
fn main() {
let v: Vec<i32> = vec![1, 2, 3];
let tv: Vec<i32> = v.iter().map(|i| i * i).collect();
println!("tv: {tv:?}");
}
// tv: [1, 4, 9]
In the example above, we first created an iterator from v
using the .iter()
method on the vector. Then, we used the .map()
method on it and passed a closure to provide a new element value. The .collect()
method is used to convert the iterator back into a vector.
Currying
Currying is a functional programming technique used to transform a function that takes multiple arguments into multiple functions that each take only one argument. This helps create higher-order functions, promoting code composition and reusability. For a typical function with two arguments, such as f(a, b) -> result
, currying would convert it into f(a) -> f(b) -> result
.
fn add(a: i32) -> impl Fn(i32) -> i32 {
move |b: i32| a + b
}
fn main() {
let add_five = add(5);
let result = add_five(3);
println!("5 + 3: {}", result);
// simplified:
println!("2 + 7: {}", add(5)(3));
}
$ cargo run
5 + 3: 8
2 + 7: 8
In the above program, unlike a typical add
function which has the signature fn add(a: i32, b: i32) -> i32
, this version only accepts one argument of type i32
and returns a closure that can be called with a second i32
to produce an i32
value.