Skip to content

Commit

Permalink
06: closures scenario & code
Browse files Browse the repository at this point in the history
  • Loading branch information
wprzytula committed Oct 30, 2024
1 parent 0ad9acd commit e3d3b3a
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 1 deletion.
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@ path = "content/lessons/05_types_reasoning/generics_fun.rs"
[[bin]]
name = "05_static_dynamic_dispatch"
path = "content/lessons/05_types_reasoning/static_dynamic_dispatch.rs"

[[bin]]
name = "06_closures_syntax"
path = "content/lessons/06_closures_iterators/closures_syntax.rs"
[[bin]]
name = "06_closures_capturing"
path = "content/lessons/06_closures_iterators/closures_capturing.rs"
[[bin]]
name = "06_closures_fun"
path = "content/lessons/06_closures_iterators/closures_fun.rs"

[[bin]]
name = "07_box"
path = "content/lessons/07_smart_pointers/box.rs"
Expand Down
87 changes: 87 additions & 0 deletions content/lessons/06_closures_iterators/closures_capturing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
fn main() {
borrowing_immutably_closure();
borrowing_mutably_closure();
moving_in_nonmutating_closure();
moving_in_mutating_closure();
moving_in_moving_out_closure();
}

fn borrowing_immutably_closure() {
let list = vec![1, 2, 3];
println!("Before defining closure: {:?}", list);

let only_borrows = || println!("From closure: {:?}", list);

// This would not really only borrow... (it needs Vec by value).
// let only_borrows = || std::mem::drop::<Vec<_>>(list);

println!("Before calling closure: {:?}", list);
only_borrows();
println!("After calling closure: {:?}", list);
}

fn borrowing_mutably_closure() {
let mut list = vec![1, 2, 3];
println!("Before defining closure: {:?}", list);

let mut borrows_mutably = || list.push(7);

// println!("Before calling closure: {:?}", list);
borrows_mutably();
println!("After calling closure: {:?}", list);
}

fn moving_in_nonmutating_closure() {
let list = vec![1, 2, 3];
println!("Before defining closure: {:?}", list);

// This closure would just borrow the list, because it only prints it.
// However, as spawning threads require passing `impl FnOnce + 'static`,
// we need to use `move` keyword to force the closure to move `list`
// into its captured environment.
std::thread::spawn(move || println!("From thread: {:?}", list))
.join()
.unwrap();
}

fn moving_in_mutating_closure() {
fn append_42(mut appender: impl FnMut(i32)) {
appender(42);
}

let mut appender = {
let mut list = vec![1, 2, 3];
println!("Before defining closure: {:?}", list);

// The `move` keyword is necessary to prevent dangling reference to `list`.
// Of course, the borrow checker protects us from compiling code without `move`.
move |num| list.push(num)
};

append_42(&mut appender);
append_42(&mut appender);
}

fn moving_in_moving_out_closure() {
fn append_multiple_times(appender: impl FnOnce(&mut Vec<String>) + Clone) {
let mut list = Vec::new();

// We can clone this `FnOnce`, because we additionally require `Clone`.
// If we didn't clone it, we couldn't call it more than *once*.
appender.clone()(&mut list);
appender(&mut list);
}

let appender = {
let string = String::from("Ala");
println!("Before defining closure: {:?}", string);

// The `move` keyword is necessary to prevent dangling reference to `list`.
// Of course, the borrow checker protects us from compiling code without `move`.
move |list: &mut Vec<String>| list.push(string)
};

// As `appender` is only `FnOnce`, we need to clone before we consume it by calling it.
append_multiple_times(appender.clone());
append_multiple_times(appender);
}
63 changes: 63 additions & 0 deletions content/lessons/06_closures_iterators/closures_fun.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
fn main() {
fn some_function() -> String {
String::new()
}

let v1 = String::from("v1");
let mut borrowing_immutably_closure = || v1.clone();

let mut v2 = String::from("v2");
let mut borrowing_mutably_closure = || {
v2.push('.');
v2.clone()
};

let v3 = String::from("v3");
let mut moving_in_nonmutating_closure = move || v3.clone();

let mut v4 = String::from("v4");
let mut moving_in_mutating_closure = move || {
v4.push('.');
v4.clone()
};
let v5 = String::from("v5");
let moving_in_moving_out_closure = || v5;

let fn_once_callables: [&dyn FnOnce() -> String; 5] = [
&some_function,
&borrowing_immutably_closure,
&borrowing_mutably_closure,
&moving_in_nonmutating_closure,
&moving_in_moving_out_closure,
];

#[allow(unused_variables)]
for fn_once_callable in fn_once_callables {
// Cannot move a value of type `dyn FnOnce() -> String`.
// The size of `dyn FnOnce() -> String` cannot be statically determined.
// println!("{}", fn_once_callable());

// So, for FnOnce, we need to be their owners to be able to call them,
// and we can't have a `dyn` object owned on stack.
// We will solve this problem soon with smart pointers (e.g., Box).
}

// Mutable reference to FnMut is required to be able to call it.
let fn_mut_callables: [&mut dyn FnMut() -> String; 4] = [
&mut borrowing_immutably_closure,
&mut borrowing_mutably_closure,
&mut moving_in_nonmutating_closure,
&mut moving_in_mutating_closure,
];

for fn_mut_callable in fn_mut_callables {
println!("{}", fn_mut_callable());
}

let fn_callables: &[&dyn Fn() -> String] =
&[&borrowing_immutably_closure, &moving_in_nonmutating_closure];

for fn_callable in fn_callables {
println!("{}", fn_callable());
}
}
20 changes: 20 additions & 0 deletions content/lessons/06_closures_iterators/closures_syntax.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
fn main() {
#[rustfmt::skip]
{
// This is formatted so that with rust-analyzer it renders as well-aligned.

fn add_one_v1 (x: u32) -> u32 { x + 1 } // This is an ordinary function.
let add_one_v2 = |x: u32| -> u32 { x + 1 }; // Closures use pipes instead of parentheses.
let add_one_v3 = |x| { x + 1 }; // Both parameters and return value can have their types inferred.
let add_one_v4 = |x| x + 1 ; // If the body is a single expression, braces can be omitted.

let _res = add_one_v1(0_u32);
let _res = add_one_v2(0_u32);
let _res = add_one_v3(0_u32);
let _res = add_one_v4(0_u32);

// This does not compile, because closures are not generic.
// Their type is inferred once and stays the same.
// let _res = add_one_v4(0_i32);
};
}
42 changes: 41 additions & 1 deletion content/lessons/06_closures_iterators/index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
+++
title = "Closures and Iterators"
date = 2022-11-14
date = 2024-10-30
weight = 1
[extra]
lesson_date = 2024-10-31
Expand All @@ -10,7 +10,47 @@ lesson_date = 2024-10-31

Closures (Polish: "domknięcia") are anonymous functions that can access variables from the scope in which they were defined.

## Closure syntax

{{ include_code_sample(path="lessons/06_closures_iterators/closures_syntax.rs", language="rust") }}

## Closures' types

Closures are unnameable types. That is, each closure gets its own unique type from the compiler,
but we cannot use it. Therefore, closures' types must be inferred.
We will often use `impl` keyword with closure traits (e.g., `impl Fn`) - those traits are described below.

## Closures capture environment

Closures can capture variables from the environment where they are defined. They can do that in two ways:
- Capturing References (borrowing), or
- Moving Ownership.

**HOW** closures capture variables is one thing.
But even more important is **WHAT** closures do with their captures.

{{ include_code_sample(path="lessons/06_closures_iterators/closures_capturing.rs", language="rust") }}

### Functions & closures hierarchy

Based on **WHAT** a closure does with its captures, it implements closure traits:

- `FnOnce` - closures that may move out of their captures environment (and thus called once).
- `FnMut` - closures that may mutate their captures, but don't move out of their captures environment (so can be called multiple times, but require a mutable reference);
- `Fn` - closures that do not mutate their captures (so can be called multiple times through an immutable reference).

For completeness, there is a (concrete) type of function pointers:
- `fn` - functions, closures with no captures.

Those traits and the `fn` type form a hierarchy: `fn` < `Fn` < `FnMut` < `FnOnce`

$$ fn \subseteq Fn \subseteq FnMut \subseteq FnOnce $$

## Examples

We'll go through the examples from [Rust by Example](https://doc.rust-lang.org/rust-by-example/fn/closures.html).
More examples will be seen when working with iterators.


# Iterators

Expand Down

0 comments on commit e3d3b3a

Please sign in to comment.