100 exercises to learn Rust

This commit is contained in:
LukeMathWalker
2024-05-12 22:21:03 +02:00
commit 5edebf6cf2
309 changed files with 13173 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
# A Basic Calculator
In this chapter we'll learn how to use Rust as a **calculator**.
It might not sound like much, but it'll give us a chance to cover a lot of Rust's basics, such as:
- How to define and call functions
- How to declare and use variables
- Primitive types (integers and booleans)
- Arithmetic operators (including overflow and underflow behavior)
- Comparison operators
- Control flow
- Panics
Nailing the basics with a few exercises will get the language flowing under your fingers.
When we move on to more complex topics, such as traits and ownership, you'll be able to focus on the new concepts
without getting bogged down by the syntax or other trivial details.
## References
- The exercise for this section is located in `exercises/02_basic_calculator/00_intro`

View File

@@ -0,0 +1,138 @@
# Types, part 1
In the ["Syntax" section](../01_intro/01_syntax.md) `compute`'s input parameters were of type `u32`.
Let's unpack what that _means_.
## Primitive types
`u32` is one of Rust's **primitive types**. Primitive types are the most basic building blocks of a language.
They're built into the language itself—i.e. they are not defined in terms of other types.
You can combine these primitive types to create more complex types. We'll see how soon enough.
## Integers
`u32`, in particular, is an **unsigned 32-bit integer**.
An integer is a number that can be written without a fractional component. E.g. `1` is an integer, while `1.2` is not.
### Signed vs. unsigned
An integer can be **signed** or **unsigned**.
An unsigned integer can only represent non-negative numbers (i.e. `0` or greater).
A signed integer can represent both positive and negative numbers (e.g. `-1`, `12`, etc.).
The `u` in `u32` stands for **unsigned**.
The equivalent type for signed integer is `i32`, where the `i` stands for integer (i.e. any integer, positive or
negative).
### Bit width
The `32` in `u32` refers to the **number of bits[^bit]** used to represent the number in memory.
The more bits, the larger the range of numbers that can be represented.
Rust supports multiple bit widths for integers: `8`, `16`, `32`, `64`, `128`.
With 32 bits, `u32` can represent numbers from `0` to `2^32 - 1` (a.k.a. [`u32::MAX`](https://doc.rust-lang.org/std/primitive.u32.html#associatedconstant.MAX)).
With the same number of bits, a signed integer (`i32`) can represent numbers from `-2^31` to `2^31 - 1`
(i.e. from [`i32::MIN`](https://doc.rust-lang.org/std/primitive.i32.html#associatedconstant.MIN)
to [`i32::MAX`](https://doc.rust-lang.org/std/primitive.i32.html#associatedconstant.MAX)).
The maximum value for `i32` is smaller than the maximum value for `u32` because one bit is used to represent
the sign of the number. Check out the [two's complement](https://en.wikipedia.org/wiki/Two%27s_complement)
representation for more details on how signed integers are represented in memory.
### Summary
Combining the two variables (signed/unsigned and bit width), we get the following integer types:
| Bit width | Signed | Unsigned |
|-----------|--------|----------|
| 8-bit | `i8` | `u8` |
| 16-bit | `i16` | `u16` |
| 32-bit | `i32` | `u32` |
| 64-bit | `i64` | `u64` |
| 128-bit | `i128` | `u128` |
## Literals
A **literal** is a notation for representing a fixed value in source code.
For example, `42` is a Rust literal for the number forty-two.
### Type annotations for literals
But all values in Rust have a type, so... what's the type of `42`?
The Rust compiler will try to infer the type of a literal based on how it's used.
If you don't provide any context, the compiler will default to `i32` for integer literals.
If you want to use a different type, you can add the desired integer type as a suffix—e.g. `2u64` is a 2 that's
explicitly typed as a `u64`.
### Underscores in literals
You can use underscores `_` to improve the readability of large numbers.
For example, `1_000_000` is the same as `1000000`.
## Arithmetic operators
Rust supports the following arithmetic operators[^traits] for integers:
- `+` for addition
- `-` for subtraction
- `*` for multiplication
- `/` for division
- `%` for remainder
Precedence and associativity rules for these operators are the same as in mathematics.
You can use parentheses to override the default precedence. E.g. `2 * (3 + 4)`.
> ⚠️ **Warning**
>
> The division operator `/` performs integer division when used with integer types.
> I.e. the result is truncated towards zero. For example, `5 / 2` is `2`, not `2.5`.
## No automatic type coercion
As we discussed in the previous exercise, Rust is a statically typed language.
In particular, Rust is quite strict about type coercion. It won't automatically convert a value from one type to
another[^coercion],
even if the conversion is lossless. You have to do it explicitly.
For example, you can't assign a `u8` value to a variable with type `u32`, even though all `u8` values are valid `u32`
values:
```rust
let b: u8 = 100;
let a: u32 = b;
```
It'll throw a compilation error:
```text
error[E0308]: mismatched types
|
3 | let a: u32 = b;
| --- ^ expected `u32`, found `u8`
| |
| expected due to this
|
```
We'll see how to convert between types [later in this course](../04_traits/08_from).
## References
- The exercise for this section is located in `exercises/02_basic_calculator/01_integers`
## Further reading
- [The integer types section](https://doc.rust-lang.org/book/ch03-02-data-types.html#integer-types) in the official Rust book
[^bit]: A bit is the smallest unit of data in a computer. It can only have two values: `0` or `1`.
[^traits]: Rust doesn't let you define custom operators, but it puts you in control of how the built-in operators
behave.
We'll talk about operator overloading [later in the course](../04_traits/03_operator_overloading), after we've covered traits.
[^coercion]: There are some exceptions to this rule, mostly related to references, smart pointers and ergonomics. We'll
cover those [later on](../04_traits/06_deref).
A mental model of "all conversions are explicit" will serve you well in the meantime.

View File

@@ -0,0 +1,104 @@
# Variables
In Rust, you can use the `let` keyword to declare **variables**.
For example:
```rust
let x = 42;
```
Above we defined a variable `x` and assigned it the value `42`.
## Type
Every variable in Rust must have a type. It can either be inferred by the compiler or explicitly specified by the
developer.
### Explicit type annotation
You can specify the variable type by adding a colon `:` followed by the type after the variable name. For example:
```rust
// let <variable_name>: <type> = <expression>;
let x: u32 = 42;
```
In the example above, we explicitly constrained the type of `x` to be `u32`.
### Type inference
If we don't specify the type of a variable, the compiler will try to infer it based on the context in which the variable
is used.
```rust
let x = 42;
let y: u32 = x;
```
In the example above, we didn't specify the type of `x`.
`x` is later assigned to `y`, which is explicitly typed as `u32`. Since Rust doesn't perform automatic type coercion,
the compiler infers the type of `x` to be `u32`—the same as `y` and the only type that will allow the program to compile
without errors.
### Inference limitations
The compiler sometimes needs a little help to infer the correct variable type based on its usage.
In those cases you'll get a compilation error and the compiler will ask you to provide an explicit type hint to
disambiguate the situation.
## Function arguments are variables
Not all heroes wear capes, not all variables are declared with `let`.
Function arguments are variables too!
```rust
fn add_one(x: u32) -> u32 {
x + 1
}
```
In the example above, `x` is a variable of type `u32`.
The only difference between `x` and a variable declared with `let` is that functions arguments **must** have their type
explicitly declared. The compiler won't infer it for you.
This constraint allows the Rust compiler (and us humans!) to understand the function's signature without having to look
at its implementation. That's a big boost for compilation speed[^speed]!
## Initialization
You don't have to initialize a variable when you declare it.
For example
```rust
let x: u32;
```
is a valid variable declaration.
However, you must initialize the variable before using it. The compiler will throw an error if you don't:
```rust
let x: u32;
let y = x + 1;
```
will throw a compilation error:
```text
error[E0381]: used binding `x` isn't initialized
--> src/main.rs:3:9
|
2 | let x: u32;
| - binding declared here but left uninitialized
3 | let y = x + 1;
| ^ `x` used here but it isn't initialized
|
help: consider assigning a value
|
2 | let x: u32 = 0;
| +++
```
## References
- The exercise for this section is located in `exercises/02_basic_calculator/02_variables`
[^speed]: The Rust compiler needs all the help it can get when it comes to compilation speed.

View File

@@ -0,0 +1,85 @@
# Control flow, part 1
All our programs so far have been pretty straightforward.
A sequence of instructions is executed from top to bottom, and that's it.
It's time to introduce some **branching**.
## `if` expressions
The `if` keyword is used to execute a block of code only if a condition is true.
Here's a simple example:
```rust
let number = 3;
if number < 5 {
println!("`number` is smaller than 5");
}
```
This program will print `number is smaller than 5` because the condition `number < 5` is true.
Like most programming languages, Rust supports an optional `else` branch to execute a block of code when the condition in an
`if` expression is false.
For example:
```rust
let number = 3;
if number < 5 {
println!("`number` is smaller than 5");
} else {
println!("`number` is greater than or equal to 5");
}
```
## Booleans
The condition in an `if` expression must be of type `bool`, a **boolean**.
Booleans, just like integers, are a primitive type in Rust.
A boolean can have one of two values: `true` or `false`.
### No truthy or falsy values
If the condition in an `if` expression is not a boolean, you'll get a compilation error.
For example, the following code will not compile:
```rust
let number = 3;
if number {
println!("`number` is not zero");
}
```
You'll get the following compilation error:
```text
error[E0308]: mismatched types
--> src/main.rs:3:8
|
3 | if number {
| ^^^^^^ expected `bool`, found integer
```
This follows from Rust's philosophy around type coercion: there's no automatic conversion from non-boolean types to booleans.
Rust doesn't have the concept of **truthy** or **falsy** values, like JavaScript or Python.
You have to be explicit about the condition you want to check.
### Comparison operators
It's quite common to use comparison operators to build conditions for `if` expressions.
Here are the comparison operators available in Rust when working with integers:
- `==`: equal to
- `!=`: not equal to
- `<`: less than
- `>`: greater than
- `<=`: less than or equal to
- `>=`: greater than or equal to
## References
- The exercise for this section is located in `exercises/02_basic_calculator/03_if_else`

View File

@@ -0,0 +1,58 @@
# Panics
Let's go back to the `speed` function you wrote for the ["Variables" section](../02_variables/README.md).
It probably looked something like this:
```rust
fn speed(start: u32, end: u32, time_elapsed: u32) -> u32 {
let distance = end - start;
distance / time_elapsed
}
```
If you have a keen eye, you might have spotted one issue[^one]: what happens if `time_elapsed` is zero?
You can try it
out [on the Rust playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=36e5ddbe3b3f741dfa9f74c956622bac)!
The program will exit with the following error message:
```text
thread 'main' panicked at src/main.rs:3:5:
attempt to divide by zero
```
This is known as a **panic**.
A panic is Rust's way to signal that something went so wrong that
the program can't continue executing, it's an **unrecoverable error**[^catching]. Division by zero classifies as such an
error.
## The panic! macro
You can intentionally trigger a panic by calling the `panic!` macro[^macro]:
```rust
fn main() {
panic!("This is a panic!");
// The line below will never be executed
let x = 1 + 2;
}
```
There are other mechanism to work with recoverable errors in Rust, which [we'll cover later](../05_ticket_v2/06_fallibility).
For the time being we'll stick with panics as a brutal but simple stopgap solution.
## References
- The exercise for this section is located in `exercises/02_basic_calculator/04_panics`
## Further reading
- [The panic! macro documentation](https://doc.rust-lang.org/std/macro.panic.html)
[^one]: There's another issue with `speed` that we'll address soon enough. Can you spot it?
[^catching]: You can try to catch a panic, but it should be a last resort attempt reserved for very specific
circumstances.
[^macro]: If it's followed by a `!`, it's a macro invocation. Think of macros as spicy functions for now. We'll
cover them in more detail later in the course.

View File

@@ -0,0 +1,15 @@
# Factorial
So far you've learned:
- How to define a function
- How to call a function
- Which integer types are available in Rust
- Which arithmetic operators are available for integers
- How to execute conditional logic via comparisons and `if`/`else` expressions
It looks like you're ready to tackle factorials!
## References
- The exercise for this section is located in `exercises/02_basic_calculator/05_factorial`

View File

@@ -0,0 +1,89 @@
# Loops, part 1: `while`
Your implementation of `factorial` has been forced to use recursion.
This may feel natural to you, especially if you're coming from a functional programming background.
Or it may feel strange, if you're used to more imperative languages like C or Python.
Let's see how you can implement the same functionality using a **loop** instead.
## The `while` loop
A `while` loop is a way to execute a block of code as long as a **condition** is true.
Here's the general syntax:
```rust
while <condition> {
// code to execute
}
```
For example, we might want to sum the numbers from 1 to 5:
```rust
let sum = 0;
let i = 1;
// "while i is less than or equal to 5"
while i <= 5 {
// `+=` is a shorthand for `sum = sum + i`
sum += i;
i += 1;
}
```
This will keep adding 1 to `sum` until `i` is no longer less than or equal to 5.
## The `mut` keyword
The example above won't compile as is. You'll get an error like:
```text
error[E0384]: cannot assign twice to immutable variable `sum`
--> src/main.rs:7:9
|
2 | let sum = 0;
| ---
| |
| first assignment to `sum`
| help: consider making this binding mutable: `mut sum`
...
7 | sum += i;
| ^^^^^^^^ cannot assign twice to immutable variable
error[E0384]: cannot assign twice to immutable variable `i`
--> src/main.rs:8:9
|
3 | let i = 1;
| -
| |
| first assignment to `i`
| help: consider making this binding mutable: `mut i`
...
8 | i += 1;
| ^^^^^^ cannot assign twice to immutable variable
```
This is because variables in Rust are **immutable** by default.
You can't change their value once it has been assigned.
If you want to allow modifications, you have to declare the variable as **mutable** using the `mut` keyword:
```rust
// `sum` and `i` are mutable now!
let mut sum = 0;
let mut i = 1;
while i <= 5 {
sum += i;
i += 1;
}
```
This will compile and run without errors.
## References
- The exercise for this section is located in `exercises/02_basic_calculator/06_while`
## Further reading
- [`while` loop documentation](https://doc.rust-lang.org/std/keyword.while.html)

View File

@@ -0,0 +1,68 @@
# Loops, part 2: `for`
Having to manually increment a counter variable is somewhat tedious. The pattern is also extremely common!
To make this easier, Rust provides a more concise way to iterate over a range of values: the `for` loop.
## The `for` loop
A `for` loop is a way to execute a block of code for each element in an iterator[^iterator].
Here's the general syntax:
```rust
for <element> in <iterator> {
// code to execute
}
```
## Ranges
Rust's standard library provides **range** type that can be used to iterate over a sequence of numbers[^weird-ranges].
For example, if we want to sum the numbers from 1 to 5:
```rust
let mut sum = 0;
for i in 1..=5 {
sum += i;
}
```
Every time the loop runs, `i` will be assigned the next value in the range before executing the block of code.
There are five kinds of ranges in Rust:
- `1..5`: A (half-open) range. It includes all numbers from 1 to 4. It doesn't include the last value, 5.
- `1..=5`: An inclusive range. It includes all numbers from 1 to 5. It includes the last value, 5.
- `1..`: An open-ended range. It includes all numbers from 1 to infinity (well, until the maximum value of the integer type).
- `..5`: A range that starts at the minimum value for the integer type and ends at 4. It doesn't include the last value, 5.
- `..=5`: A range that starts at the minimum value for the integer type and ends at 5. It includes the last value, 5.
You can use a `for` loop with the first three kinds of ranges, where the starting point
is explicitly specified. The last two range types are used in other contexts, that we'll cover later.
The extreme values of a range don't have to be integer literals—they can be variables or expressions too!
For example:
```rust
let end = 5;
let mut sum = 0;
for i in 1..(end + 1) {
sum += i;
}
```
## References
- The exercise for this section is located in `exercises/02_basic_calculator/07_for`
## Further reading
- [`for` loop documentation](https://doc.rust-lang.org/std/keyword.for.html)
[^iterator]: Later in the course we'll give a precise definition of what counts as an "iterator".
For now, think of it as a sequence of values that you can loop over.
[^weird-ranges]: You can use ranges with other types too (e.g. characters and IP addresses),
but integers are definitely the most common case in day-to-day Rust programming.

View File

@@ -0,0 +1,112 @@
# Overflow
The factorial of a number grows quite fast.
For example, the factorial of 20 is 2,432,902,008,176,640,000. That's already bigger than the maximum value for a
32-bit integer, 2,147,483,647.
When the result of an arithmetic operation is bigger than the maximum value for a given integer type,
we are talking about **an integer overflow**.
Integer overflows are an issue because they violate the contract for arithmetic operations.
The result of an arithmetic operation between two integers of a given type should be another integer of the same type.
But the _mathematically correct result_ doesn't fit into that integer type!
> If the result is smaller than the minimum value for a given integer type, we refer to the event as **an integer
> underflow**.
> For brevity, we'll only talk about integer overflows for the rest of this section, but keep in mind that
> everything we say applies to integer underflows as well.
>
> The `speed` function you wrote in the ["Variables" section](02_variables.md) underflowed for some input
> combinations.
> E.g. if `end` is smaller than `start`, `end - start` will underflow the `u32` type since the result is supposed
> to be negative but `u32` can't represent negative numbers.
## No automatic promotion
One possible approach would be automatically promote the result to a bigger integer type.
E.g. if you're summing two `u8` integers and the result is 256 (`u8::MAX + 1`), Rust could choose to interpret the
result as `u16`, the next integer type that's big enough to hold 256.
But, as we've discussed before, Rust is quite picky about type conversions. Automatic integer promotion
is not Rust's solution to the integer overflow problem.
## Alternatives
Since we ruled out automatic promotion, what can we do when an integer overflow occurs?
It boils down to two different approaches:
- Reject the operation
- Come up with a "sensible" result that fits into the expected integer type
### Reject the operation
This is the most conservative approach: we stop the program when an integer overflow occurs.
That's done via a panic, the mechanism we've already seen in the ["Panics" section](04_panics.md).
### Come up with a "sensible" result
When the result of an arithmetic operation is bigger than the maximum value for a given integer type, you can
choose to **wrap around**.
If you think of all the possible values for a given integer type as a circle, wrapping around means that when you
reach the maximum value, you start again from the minimum value.
For example, if you do a **wrapping addition** between 1 and 255 (=`u8::MAX`), the result is 0 (=`u8::MIN`).
If you're working with signed integers, the same principle applies. E.g. adding 1 to 127 (=`i8::MAX`) with wrapping
will give you -128 (=`i8::MIN`).
## `overflow-checks`
Rust lets you, the developer, choose which approach to use when an integer overflow occurs.
The behaviour is controlled by the `overflow-checks` profile setting.
If `overflow-checks` is set to `true`, Rust will **panic at runtime** when an integer operation overflows.
If `overflow-checks` is set to `false`, Rust will **wrap around** when an integer operation overflows.
You may be wondering—what is a profile setting? Let's get into that!
## Profiles
A [**profile**](https://doc.rust-lang.org/cargo/reference/profiles.html) is a set of configuration options that can be
used to customize the way Rust code is compiled.
Cargo provides two built-in profiles: `dev` and `release`.
The `dev` profile is used every time you run `cargo build`, `cargo run` or `cargo test`. It's aimed at local
development,
therefore it sacrifices runtime performance in favor of faster compilation times and a better debugging experience.
The `release` profile, instead, is optimized for runtime performance but incurs longer compilation times. You need
to explicitly request via the `--release` flag—e.g. `cargo build --release` or `cargo run --release`.
> "Have you built your project in release mode?" is almost a meme in the Rust community.
> It refers to developers who are not familiar with Rust and complain about its performance on
> social media (e.g. Reddit, Twitter, etc.) before realizing they haven't built their project in
> release mode.
You can also define custom profiles or customize the built-in ones.
### `overflow-check`
By default, `overflow-checks` is set to:
- `true` for the `dev` profile
- `false` for the `release` profile
This is in line with the goals of the two profiles.
`dev` is aimed at local development, so it panics in order to highlight potential issues as early as possible.
`release`, instead, is tuned for runtime performance: checking for overflows would slow down the program, so it
prefers to wrap around.
At the same time, having different behaviours for the two profiles can lead to subtle bugs.
Our recommendation is to enable `overflow-checks` for both profiles: it's better to crash than to silently produce
incorrect results. The runtime performance hit is negligible in most cases; if you're working on a performance-critical
application, you can run benchmarks to decide if it's something you can afford.
## References
- The exercise for this section is located in `exercises/02_basic_calculator/08_overflow`
## Further reading
- Check out ["Myths and legends about integer overflow in Rust"](https://huonw.github.io/blog/2016/04/myths-and-legends-about-integer-overflow-in-rust/)
for an in-depth discussion about integer overflow in Rust.
[^catching]: You can try to catch a panic, but it should be a last resort reserved for very specific circumstances.

View File

@@ -0,0 +1,43 @@
# Case-by-case behavior
`overflow-checks` is a blunt tool: it's a global setting that affects the whole program.
It often happens that you want to handle integer overflows differently depending on the context: sometimes
wrapping is the right choice, other times panicking is preferable.
## `wrapping_` methods
You can opt into wrapping arithmetic on a per-operation basis by using the `wrapping_` methods[^method].
For example, you can use `wrapping_add` to add two integers with wrapping:
```rust
let x = 255u8;
let y = 1u8;
let sum = x.wrapping_add(y);
assert_eq!(sum, 0);
```
## `saturating_` methods
Alternatively, you can opt into **saturating arithmetic** by using the `saturating_` methods.
Instead of wrapping around, saturating arithmetic will return the maximum or minimum value for the integer type.
For example:
```rust
let x = 255u8;
let y = 1u8;
let sum = x.saturating_add(y);
assert_eq!(sum, 255);
```
Since `255 + 1` is `256`, which is bigger than `u8::MAX`, the result is `u8::MAX` (255).
The opposite happens for underflows: `0 - 1` is `-1`, which is smaller than `u8::MIN`, so the result is `u8::MIN` (0).
You can't get saturating arithmetic via the `overflow-checks` profile setting—you have to explicitly opt into it
when performing the arithmetic operation.
## References
- The exercise for this section is located in `exercises/02_basic_calculator/09_saturating`
[^method]: You can think of methods as functions that are "attached" to a specific type.
We'll cover methods (and how to define them) in the next chapter.

View File

@@ -0,0 +1,105 @@
# Conversions, pt. 1
We've repeated over and over again that Rust won't perform
implicit type conversions for integers.
How do you perform _explicit_ conversions then?
## `as`
You can use the `as` operator to convert between integer types.
`as` conversions are **infallible**.
For example:
```rust
let a: u32 = 10;
// Cast `a` into the `u64` type
let b = a as u64;
// You can use `_` as the target type
// if it can be correctly inferred
// by the compiler. For example:
let c: u64 = a as _;
```
The semantics of this conversion are what you expect: all `u32` values are valid `u64`
values.
### Truncation
Things get more interesting if we go in the opposite direction:
```rust
// A number that's too big
// to fit into a `u8`
let a: u16 = 255 + 1;
let b = a as u8;
```
This program will run without issues, because `as` conversions are infallible.
But what is the value of `b`?
When going from a larger integer type to a smaller, the Rust compiler will perform
a **truncation**.
To understand what happens, let's start by looking at how `256u16` is
represented in memory, as a sequence of bits:
```text
0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0
| | |
+---------------+---------------+
First 8 bits Last 8 bits
```
When converting to a `u8`, the Rust compiler will keep the last 8 bits of a `u16`
memory representation:
```text
0 0 0 0 0 0 0 0
| |
+---------------+
Last 8 bits
```
Hence `256 as u8` is equal to `0`. That's... not ideal, in most scenarios.
In fact, the Rust compiler will actively try to stop you if it sees you trying
to cast a literal value which will result in a truncation:
```text
error: literal out of range for `i8`
|
4 | let a = 255 as i8;
| ^^^
|
= note: the literal `255` does not fit into the type `i8` whose range is `-128..=127`
= help: consider using the type `u8` instead
= note: `#[deny(overflowing_literals)]` on by default
```
### Recommendation
As a rule of thumb, be quite careful with `as` casting.
Use it _exclusively_ for going from a smaller type to a larger type.
To convert from a larger to smaller integer type, rely on the
[*fallible* conversion machinery](../05_ticket_v2/13_try_from) that we'll
explore later in the course.
### Limitations
Surprising behaviour is not the only downside of `as` casting.
It is also fairly limited: you can only rely on `as` casting
for primitive types and a few other special cases.
When working with composite types, you'll have to rely on
different conversion mechanisms ([fallible](../05_ticket_v2/13_try_from)
and [infallible](../04_traits/08_from)), which we'll explore later on.
## References
- The exercise for this section is located in `exercises/02_basic_calculator/10_as_casting`
## Further reading
- Check out [Rust's official reference](https://doc.rust-lang.org/reference/expressions/operator-expr.html#numeric-cast)
to learn the precise behaviour of `as` casting for each source/target combination,
as well as the exhaustive list of allowed conversions.