This is the original place I made these notes.
I'm moving them over to rust-notes.alanwsmith.com
Scratchpad 2
fn main() { println!("Hello, World"); }
Scratchpad
SOURCE CODE
CODE RUNNER
Step By Step
Step By Step Test
This is the first prototype of the viewer. Next step is to highlight lines that changed.
NOTE: These are in reverse order of how they were created so I don't have to keep scrolling down
TODO: Come up with the list of things you can do for references:
immutable - immutable mutable - immutable mutable - mutable
and that you can't do
immutable - mutable
Immutable -> Immutable
fn main() { let alfa = String::from("apple"); widget(&alfa); println!("alfa is {alfa}") } fn widget(value: &String) { println!("widget got {value}") }
Mutable -> Mutable
fn main() { let mut alfa = String::from("apple"); widget(&mut alfa); println!("alfa is {alfa}"); } fn widget(value: &mut String) { value.push_str("-pie") }
Mutable -> Immutable
fn main() { let mut alfa = String::from("apple"); widget(&alfa); println!("alfa is {alfa}") } fn widget(value: &String) { println!("widget got {value}") }
Immutable -> Mutable (will crash)
fn main() { let alfa = String::from("apple"); widget(&mut alfa); } fn widget(value: &mut String) { println!("widget got {value}") }
This crashes as expected because it's not a reference
fn main() { let alfa = String::from("apple"); widget(alfa); println!("alfa is {alfa}") } fn widget(value: String) { println!("widget got {value}") }
Other situaltions:
combinging mutalbe and immutable references.
Hello, World - Your Turn
I learn best by doing. For programming that means typing code examples into an editor. The second feature of the site is designed to do just that. You'll find Source Code blocks paired with Code Runners editors across the site. They let you enter and run the code without having to install Rust yourself.
Typing slows me down in beneficial ways. I focus more on the the code. That helps me understand and remember how things work. The approach can take some getting used to. Stick with it for a bit to see if you start to see the benefit.
Here's the first one. It's the same "Hello, World" as the example on the prior page. Type it in and run it to see the output.
SOURCE CODE
fn main() {
println!("Hello, World");
}
CODE RUNNER
The Status Line
Learning to read error message is a crital part of programming. We'll cover them. But, hitting one because you made a typo in a tutorail is a waste of time, energy, and motivation.
You may have noticed the "Status" line in the Code Runner on the previous pages. It's designed to eliminate typos by highlighting them as soon as they occur and show you what character to use.
If you've ever entered a huge block of tutorial code only to be hit with an indecipherable error because of an unrelated typo, you know why I built it.
Quick Notes
A few quick notes before we continue:
-
This site is a work in progress. It's built from my actual, in-progress notes. It only goes as far as I've made it learning the language.
-
The content is designed for folks who have a little programming experience. If you know what variables, loops, conditionals, and functions are you'll be fine.
-
Examples are intentionally sort. That often means doing things that would be silly in useful programs. For example, defining a variable that's used only once on the following line. The goal is to show how to use something not when to use it.
-
The Code Runners have a "Disable Status Line" button. Hit that to prevent the status line from warning you about typos if you want to change code to play with the code samples.
Variables
Basic Rust variables are created using this formula:
- The
let
keyword - A name
- The
=
sign - The value
- A
;
character
The values for a string of text looks like this:
String::from("apple")
. Using that and alfa
for
the name we can create a variable like this:
let alfa = String::from("apple");
In Rust, setting variables is called "binding".
So, the above line binds a String
with the
text "apple" to the variable alfa
.
Printing Variables
Most examples on the site print something.
We'll do that using println!()
.
println!()
uses what are called "format
strings" as templates for output. Putting
the name of a variable inside {}
curly brackets
in a format string outputs its value. For example,
if we have a variable named alfa
we can print
it like this:
println!("alfa is {alfa}");
A full program looks like this:
fn main() {
let alfa = String::from("apple");
println!("alfa is {alfa}");
}
which outputs:
alfa is apple
Step By Step
Printing Variables - Your Turn
Here's the same code to try yourself.
SOURCE CODE
fn main() {
let alfa = String::from("apple");
println!("alfa is {alfa}");
}
CODE RUNNER
Multiple Variables
Multiple variables can be printing in
the same println!()
expression by
adding more {}
curly brackets with
the desired variable names in them.
For example, here's a program that creates three variables and prints them:
fn main() {
let alfa = String::from("apple");
let bravo = String::from("berry");
let charlie = String::from("cherry");
println!("{alfa} {bravo} {charlie}");
}
The output is:
apple berry cherry
Step By Step
Alternate Style
There will be times when we need to use an alternate syntax for printing. Instead of this:
println!("alfa is {alfa}");
the alternate style uses an empty set of {}
curly brackets inside the quotes with
the variable name outside them and separated
by a comma like this:
println!("alfa is {}", alfa);
A full program looks like this:
fn main() {
let alfa = String::from("apple");
println!("alfa is {}", alfa);
}
which outputs:
alfa is apple
TODO
- add note about why that change is necessary
Multiple Variables - Alternate Syntax
The alternate syntax uses multiple empty {}
curly bracket placeholders and the variables
outside the quotes as before. The first variable
goes into the first {}
. The second variable
goes into the second {}
and so on.
It looks like this:
fn main() { let alfa = String::from("apple"); let bravo = String::from("berry"); let charlie = String::from("cherry"); println!("{} {} {}", alfa, bravo, charlie); }
which produces the same output:
apple berry cherry
Mutable Variables
Rust variables are immutalbe by default. That means
you can't change them after they've been set. The
mut
keyword makes them mutable allowing them
to be changed after they are created.
Here's an example of creating a default (immutable) variable:
let alfa = String::from("apple");
And this is how to create one using the mut
keyword
to make it mutable:
let mut alfa = String::from("apple");
.push_str()
One way to change a mutable String
is with
.push_str()
. It adds content
onto the end of an existing string. For example,
this adds "pie" to the end of the "apple"
String
let mut alfa = String::from("apple");
alfa.push_str("pie");
Step By Step
Mutable Variables - Your Turn
SOURCE CODE
fn main() {
let mut alfa = String::from("apple");
alfa.push_str("pie");
println!("alfa is {alfa}");
}
CODE RUNNER
Variables Names With _ Underscores
When the Rust compiler is checking a program to make sure it's valid it also checks for unused variables. TODO: Show an example where the warning for an unused variable shows up.
Adding an _
underscore as the first character
of the variable name tells the compiler
that we're expecting the variable to be unused
so it shoudn't output the warning. This
is helpful during the development process
to avoid warnings when code isn't fully
complete.
We'll be using the underscore in some of the examples to keep the code short without causing the warning.
Functions
Each program we make will have a main()
funciton.
It's the first things that gets executed.
We can create other functions as well. We'll use three types of functions with the following properties:
- Does not accept arguments - Has no return value
- Accepts arguments - Has no return value
- Does not accept arguments - Does have a return value
- Accepts arguments - Does have a return value
The formulas for each of the three types are on the next three pages. Don't worry too much about memorizing them. We'll use each plenty of times.
Functions With No Arguments And No Return Value
The formula for functions that take no arguments and return no values is:
- The
fn
keyword - A name
- An empty set of
()
parenthesis - A set of
{}
curly brackets surrounding the function's code block
For example:
fn widget() {
println!("this is widget");
}
Calling functions without arguments is done using its
name followed by empty ()
, like:
widget();
Step By Step
Your Turn
SOURCE CODE
fn main() {
widget();
}
fn widget() {
println!("this is widget");
}
CODE RUNNER
Functions That Take Arguments But Have No Return Value
The formula for functions that do take
arguments without returning a value is must
the same as functions that don't. The
difference is that the ()
parens are
populated to let the function know about
the incoming data
- The
fn
keyword - A name
- Parenthesis with argument details. For example:
(thing: String)
- A set of
{}
curly brackets surrounding the function's code block
Step By Step
Your Turn
SOURCE CODE
fn main() {
let alfa = String::from("apple");
widget(alfa);
}
fn widget(thing: String) {
println!("widget got {thing}");
}
CODE RUNNER
Takes No Arguments But Has A Return Value
In addition to receiving data, functions can
also return values back to the code
that called them. Difining those functions is
done by adding ->
along what will be send back
behindg the parenthesis after the name. For example
widget() -> String
The parts of the function look like this:
- The
fn
keyword - A name
- An empty set of
()
parenthesis. - The
->
symbol followed by what type of data will be returned - A set of
{}
curly brackets surrounding the function's code block
Step By Step
Your Turn
SOURCE CODE
fn main() {
let alfa = widget();
println!("alfa got {alfa}");
}
fn widget() -> String {
let bravo = String::from("berry");
bravo
}
CODE RUNNER
Functions That Take Arguments And Have Return Values
Step By Step
Your Turn
Ownership
NOTE: This is my understanding of how ownership based on The Rust Book. I need to have it vetted by someone who knows more about Rust.
A fundamental feature of Rust is that every value has an "owner". When we bind a value to a variable like this:
let alfa = String::from("apple");
that variable becomes the owner of the value.
In the case that means alfa
now owns the
String
of "apple". You can think of an "owner"
like a wrapper around the value.
As long as a varible is owner of a value it
can use it. For example, if we call this
after setting alfa
like we did above:
println!("alfa is {alfa}");
it will output:
alfa is apple
becuase alfa
owns the String
made
from "apple".
Step 1
Let's go through the process of creating a variable and binding a value to it to get a better understading of ownership. We'll start off by splitting this line in half:
let alfa = String::from("apple");
The let alfa
on the left side of our equal sign
is what's responsible for creating the variable
with our specified name.
Adding in the String::from("apple")
to complete
the expression creates the value, binds it to
alfa
, and makes alfa
its owner in the process.
Step 2
When we want to use the value of the String
we
don't access it directly. We get it from through
the variable.
For exaple, if we do:
println!("alfa has {alfa}");
Rust sees the request for alfa
and accesses
the variable to get the value it's bound to
and returns it for the output:
alfa is apple
Here's what that looks like:
Step 3
Now let's make a new variable called
bravo
and bind our exsiting alfa
variable to it.
As before, the let bravo
on the
left side of the eaqual sign is what's
responsible for making the variable
What Doesn't Happen
Next we'll complete our expression with this:
let bravo = alfa;
Becasue alfa
became the owner of our String
value
when we did this:
let alfa = String::from("apple");
It's natural to think bravo
would become
the owner of alfa
when we completed our
expression with let bravo = alfa;
If that were the case, we'd get something that looks like this:
We would then be able to get at our String
value from both alfa
and bravo
. (alfa
would be direct and bravo
would go through
alfa
).
But, that's not what happens.
Step 4
TODO: Can variables have owners? And does this page use the right language?
Let's look at what does happen starting with a review of Step 3 where:
- The
alfa
variable has been created - The
String::from("apple")
value has been created and bound toalfa
making the variable its owner - We've made the first half of the expression
to bind
alfa
tobravo
Our illustration looked like this:
Now we can complete the expression with:
let bravo = alfa;
That assignment doesn't make bravo
the
owner of alfa
. Instead the value that's
insdie alfa
gets moved into bravo
and
bravo
becomes the owner.
Step 5
Using bravo
gives us the String
value
output of "apple" the same way alfa
did
before the move.
If we combine and run all the code we've use so far everything works.
SOURCE CODE
fn main() {
let alfa = String::from("apple");
println!("alfa has {alfa}");
let bravo = alfa;
println!("bravo has {bravo}");
}
CODE RUNNER
Step By Step
Your Turn
Hitting A Moved Error
Where things go wrong is if we try to
use alfa
again. Values can only
have one owner. When we did let bravo = alfa;
it transferred ownership of the String
from
alfa
to bravo
.
Without ownership, alfa
can't work with the
String
any more`
Trying to use it looks like this:
Here's the full example of the code where
we try to access alfa
with this line
println!("alfa has {alfa}");
Since the String
was moved to bravo
we get an error that we'll discuss next.
SOURCE CODE
fn main() {
let alfa = String::from("apple");
let bravo = alfa;
println!("alfa has {alfa}");
}
CODE RUNNER
Your Turn
Moving Error Details
TKTKTKT - Write up the details of this error.
This is the error.
Compiling playground v0.0.1 (/playground)
error[E0382]: borrow of moved value: `alfa`
--> src/main.rs:8:23
|
2 | let alfa = String::from("apple");
| ---- move occurs because `alfa` has type `String`, which does not implement the `Copy` trait
...
5 | let bravo = alfa;
| ---- value moved here
...
8 | println!("alfa has {alfa}");
| ^^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
5 | let bravo = alfa.clone();
| ++++++++
For more information about this error, try `rustc --explain E0382`.
error: could not compile `playground` due to previous error
References
References offer a way to access a value
from multiple variables without changing
ownership. They're made by putting the
&
symbol in front of the original
variable that holds the value.
For example this code moves ownership of the
String
on the bravo = alfa
line.
let alfa = String::from("apple");
let bravo = alfa;
If we tried to access alfa
after doing
that we'd get the borrow of moved value
error.
Adding the &
like this means the
value isn't moved to bravo
and
that alfa
retains ownership.
let alfa = String::from("apple");
let bravo = &alfa;
We can now use the value from both variables.
Setp By Step
Example
Here's an example using a reference that outputs:
alfa is apple
bravo is apple
SOURCE CODE
fn main() {
let alfa = String::from("apple");
let bravo = &alfa;
println!("alfa is {alfa}");
println!("bravo is {bravo}");
}
CODE RUNNER
Step 1
The first step is to create an initial variable with the standard line:
let alfa = String::from("apple");
We'll use a slightly different illustration
this time where the String
value has
as side exposed.
Step 2
Now we can create our reference with the &
character like this:
let bravo = &alfa;
We'll represent the reference this way where
alfa
still owns the String
but bravo
is connected to it as well.
Working Example
Here's a refined version of our previous example that didn't work:
fn main() {
let alfa = String::from("apple");
let bravo = alfa;
println!("alfa has {alfa}");
println!("bravo has {bravo}");
}
We can get the code working with a reference by changing the line
let bravo = alfa;
to:
let bravo = &alfa;
Here's the full sample which outputs:
alfa has apple
bravo has apple
SOURCE CODE
fn main() {
let alfa = String::from("apple");
let bravo = &alfa;
println!("alfa has {alfa}");
println!("bravo has {bravo}");
}
CODE RUNNER
Multiple References
Variables are immutalbe by default. So are references. One benefit of that since we can be sure they won't change we can add as many references as we like.
Here's an illustration using two new
variables charlie
and delta
that
reference the original alfa
value.
And here's the code showing it working. It outputs:
alfa has apple
bravo has apple
charlie has apple
delta has apple
SOURCE CODE
fn main() {
let alfa = String::from("apple");
let bravo = &alfa;
let charlie = &alfa;
let delta = &alfa;
println!("alfa has {alfa}");
println!("bravo has {bravo}");
println!("charlie has {charlie}");
println!("delta has {delta}");
}
CODE RUNNER
Step By Step
Your Turn
Cheatsheet
NOTE: These are draft notes.
TODO: Figure out where to put this in the book.
Here's the high level:
Create An Immutable Variable
let alfa = String::from("apple");
Create A Mutable Variable
let mut alfa = String::from("apple");
Create A Struct
struct Widget {
alfa: String,
bravo: String
}
Create A Function With No Arguments Or Return Value
fn widget() {
println!("this is widget");
}
Create A Function That Has One Argument Reference But No Return Value
fn widget(thing: &String) {
println!("widget to {}", thing);
}
Create A Function With Two Arguments But No Return Value
fn widget(thing1: &String, thing2: &String) {
println!(
"widget got {} and {}",
thing1,
thing2
);
}
Create A Function With A Return Value But No Arguments
fn widget() -> String {
let alfa = String::from("apple");
alfa
}
Create A Function With A Return Value And An Argument Reference
fn widget(thing: &String) -> String {
println!("widget got {}", thing);
let alfa = String::from("apple");
alfa
}
Create A Function That Takes A Mutable Reference Argument
fn widget(thing: &mut String) {
thing.push_str("additional characters")
}
Drafts
All the pages that follow are rough drafts and sometimes little more than collections of notes. Feel free to look around keeping that in mind.
Errors: Result and panic!()
There are two categories of errors in Rust programs and expressions the correspond with them:
- Errors the program can't recover from which
are triggered by
panic!()
- Errors the program can recover from which is
tied to a
Result
These are draft notes
TODO: Switch over to using ENV Vars instead of the file system.
fn main() { panic!("goes boom"); }
fn main() { let alfa = vec![ 3, 5, 7 ]; alfa[100]; }
Errors - Result
From the book:
"in production code, most Rustaceans choose
expect
rather than unwrap
to give more
context about what happens.
Result<T, E>
is defined as
enum Result<T, E> {
Ok(T),
Err(E)
}
This panics
use std::fs::File; fn main() { let the_file_result = File::open("hello.txt"); let the_file = match the_file_result { Ok(file) => file, Err(error) => panic!("{:?}", error) }; println!("Got {:?}", the_file) }
Instead of the match, you can use .unwrap()
.
This throws a panic if it hits the error.
That doesn't have to be the case, you can
do somethiing else as long as it returns
a proper value (TBD on how to do that)
use std::fs::File; fn main() { let _the_file = File::open("hello.txt") .unwrap_or_else(|error| { panic!("Error {:?}", error) }); }
This will panic because there is no _or_else
.
use std::fs::File; fn main() { let _the_file = File::open("hello.txt").unwrap(); }
Using .expect()
let you define an error
message. So the value gets set and then
will get an error with the message if
something goes wrong. (TODO: Figure out
what the difference is between the value
returned. i.e. do .unwrap()
and
.expect()
set the same value.
(Note, this didn't give the .expect()
error message, need to look more into
that)
use std::fs::File; fn main() { let _the_file = File::open("hello.txt") .expect("The error happened"); }
From the book:
// I think this actualy makes a new file // on the playground so it doesn't panic.
use std::fs::File; use std::io::ErrorKind; fn main() { let greeting_file_result = File::open("hello.txt"); let greeting_file = match greeting_file_result { Ok(file) => file, Err(error) => match error.kind() { ErrorKind::NotFound => match File::create("hello.txt") { Ok(fc) => fc, Err(e) => panic!("Could not create file {:?}", e) }, other_error => { panic!("Could not open file {:?}", other_error) } } }; }
Propigating Errors
This is the first example they put in but they
say they'll do a different way next. (i.e.
this is the manual way they are using to show
some details on how things work. (no main
so not sure it if compiles)
From the book
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Results<String, io:Error> { let username_file_result = File::open("hello.txt"); let mut username_file = match username_file_result { Ok(file) => file, Err(e) => return Err(e) }; let mut username = String::new(); match username_file.read_to_string(&mut username) { Ok(_) => Ok(username), Err(e) => Err(e), } } }
Here's the shorter version:
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Resutls<String, io:Error> { let mut username_file = File::open("users.txt")?; let mut username = String::new(); username_file.read_to_string(&mut username)?; Ok(username) } }
And even shorter. This is a better way to show things. Should be able to explain it directly.
NOTE: Talks about the "from" casting that happens
via the ?
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Results<String, io::Error> { let mut username = String::new(); File::open("users.txt")?.read_to_string(&mut username)?; Ok(username) } }
And then finally for the actual use case if you need to read something in:
#![allow(unused)] fn main() { use std::fs; use std::io; fn read_username_from_file() -> Result<String, io::Error> { fs.read_to_string("users.txt"); } }
The ?
does an early return.
The ?
can only be used for functions whose return
type is compatable. For example, you can't use one
in main
because there is no Result return type.
You can use an Option
, Result
, or something
that implements FromResidaul
From the book:
The error message also mentioned that ? can be used with
Option
#![allow(unused)] fn main() { fn last_character_of_first_line(text: &str) -> Option<char> { text.lines().next()?.chars().last() } }
This function returns Option
It is possible to return a Result<(), Box<dyn Error>>
from main()
TODO: Look at Box<dyn Error>
If main returns a Result with a value of 0
that means
things went well. Any other value indicates a problem
Trying to use env vars.
.is_ok()
returns true
if a value is
OK()
and false
if it's not.
This just checks to see if an envvar exists
and sets alfa
as true
or false
use std::env; fn main() { let alfa = env::var("FORCE_ERROR").is_ok(); println!("alfa is {alfa}"); }
use std::env; fn main() { let alfa = String::from("test_var"); let bravo = check_var(&alfa); println!("bravo is {bravo}"); } fn check_var(key: &String) -> String { match env::var(key) { Ok(value) => value, Err(e) => String::from("no_value") } }
use std::env; fn main() { let alfa = String::from("test_var"); let bravo = check_var(&alfa); println!("bravo is {bravo}"); } fn check_var(key: &String) -> String { env::var(key).expect("no var set. panicing") }
panic!() Errors
A panic!()
happens when the program tries
to do something it's not capable of and
doesn't have a mechanism to recover from.
For example, manually trying to access
an index location of a Vec
that doesn't
exist causes a panic.
So when you try to do this:
fn main() { let alfa = vec![ String::from("apple"), String::from("berry") ]; let bravo = &alfa[100]; println!("bravo is {bravo}"); }
The program panics and dumps this to the output:
Compiling playground v0.0.1 (/playground)
Finished dev [unoptimized + debuginfo] target(s) in 1.44s
Running `target/debug/playground`
thread 'main' panicked at 'index out of bounds: the len is 2 but the index is 100', src/main.rs:7:16
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
These errors are different than the ones weve been getting
so far. Our earlier programs weren't able to compile at all
so there was no danger of shipping the program because the
compiler wouldn't allow it to be built. With panic!()
errors, the program is able to complie and make the
acutal program file. So, it's possible to ship a program
with a panic!()
bug in it.
Identifying panic!() Errors
There are two quick ways to tell the difference in the error messages. To see them let's look at the output of a program that tries. to change an immutalbe value:
fn main() { let alfa = String::from("apple"); println!("alfa is {alfa}"); alfa = String::from("berry"); println!("alfa is {alfa}"); }
That output looks like this (which will refer to as the compiling error)
Compiling playground v0.0.1 (/playground)
error[E0384]: cannot assign twice to immutable variable `alfa`
--> src/main.rs:4:3
|
2 | let alfa = String::from("apple");
| ----
| |
| first assignment to `alfa`
| help: consider making this binding mutable: `mut alfa`
3 | println!("alfa is {alfa}");
4 | alfa = String::from("berry");
| ^^^^ cannot assign twice to immutable variable
For more information about this error, try `rustc --explain E0384`.
error: could not compile `playground` due to previous error
And here's another copy of our panic!()
error from the the previous
page (which we'll refer to as the panic error)
Compiling playground v0.0.1 (/playground)
Finished dev [unoptimized + debuginfo] target(s) in 1.44s
Running `target/debug/playground`
thread 'main' panicked at 'index out of bounds: the len is 2 but the index is 100', src/main.rs:7:16
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
The first way to tell the difference between the compiling and
panic errors is with the first three lines. Both errors start
of with Compiling...
. Then the compiling error indicates an
with an error number on the next line (e.g. error[E0384]
)
while the panic error shows Finished...
on line two and
Running...
on the third line. Those two lines indicate that
the compiler was able to do it's job and start to run the program.
The second way to tell the difference between the two types
of errors is at the end of teh error message. The compiler
error ends with error: could not compile...
identifies
the core problem we're discussing. For the panic error, the
next to last line is thread 'main' panicked at...
with
more text desciging exactly where the panic occurred.
Causing panic!() Errors
panic!()
errors can be caused by tring to do
something the program can't handle like the
way we tried to access an index larger than
the available values in a Vec
. We can
also trigger them directly by using
the panic!()
expression.
For example, this code will compile and run:
fn main() { let alfa = String::from("apple"); println!("alfa is {alfa}"); }
Adding a panic line causes a crash:
fn main() { let _alfa = String::from("apple"); panic!("break here"); }
Result Type For Error Handling
The Result type is used to return values.
It can contain either and Ok
or an
Err
. The basic signature is:
enum Result<T, E> {
Ok(T),
Err(E),
}
The T
stands for type
, and E
stands for
error
. Those are generic types which means
that we can put any type in the slot. The
first T
and E
in Result<T, E>
setup
the result of enum to be able to use them
(TODO: write up generic types before this)
The Ok(T)
turns into whatever type
the process that made it needs to send.
For example, the code below is used
to get the string value from an
Environmental Variable. But, the string
doesn't come in directly. Instead
the env::var
returns a Result
that
contains either Ok(String)
or
Err(VarError)
.
So, to get to the string we have to
examine the Result
with match
to see if it contains Ok
or Err
Here's an example
#![allow(unused)] fn main() { use std::env; let key = "HOMEx"; let returnValueAsResult = env::var(key); match returnValueAsResult { Ok(value) => { println!("{} is {}", key, value); } Err(error) => { println!("{} - {}", key, error); } } }
#![allow(unused)] fn main() { use std::env; let key = "NON_EXISTANT_KEY"; match env::var(key) { Ok(val) => println!("{key}: {val:?}"), Err(e) => println!("couldn't interpret {key}: {e}"), } }
Step By Step
Result And Option
TODO: Make a page showing both Result and Option and doing some comprison of the two
Function Argument References
References are required to send the value of a variable to a function and still be able to use the original variable afterwards.
That looks like this:
SOURCE CODE
fn main() {
let alfa = String::from("apple");
widget(&alfa);
println!("alfa is {alfa}");
}
fn widget(thing: &String) {
println!("widget got {thing}");
}
CODE RUNNER
With Variables
We can also create a reference in a new variable the send it to the function. Since we'll define the new varaible as a refernce like this:
let bravo = &alfa
We don't need to send it to use &
to
make it a reference again when we send
it to the widget()
function. That is,
instead of this:
widget(&bravo);
We do this:
widget(bravo);
Here'e the code:
SOURCE CODE
fn main() {
let alfa = String::from("apple");
let bravo = &alfa;
widget(bravo);
println!("alfa is {alfa}");
println!("bravo is {bravo}");
}
fn widget(thing: &String) {
println!("widget got {thing}");
}
CODE RUNNER
Error Example
Here's the same thing, but without &
which
shows the error.
SOURCE CODE
fn main() {
let alfa = String::from("apple");
println!("call widget next");
widget(alfa);
println!("alfa is {alfa}");
}
fn widget(value: String) {
println!("widget got {value}");
}
CODE RUNNER
Mutable Function Argument References
TKTKTKT
TODO: Explain this line by line
SOURCE CODE
fn main() {
let mut alfa = String::from("apple");
widget(&mut alfa);
println!("alfa is {alfa}");
}
fn widget(value: &mut String) {
value.push_str("-pie");
}
CODE RUNNER
Structs
Structs allow you to group data together. They're
defined with the struct
keyword followed by
the name to use then {}
curly brackets that
contain the details. The content inside the
curly brackets defined what are known as "fields"
and the type of data they can hold.
Here's the definition for a struct
named
Widget
that contains two fields alfa
and bravo
. Each one of the fields is
setup to hold a String
struct Widget {
alfa: String,
bravo: String
}
Using a struct
is done by assigning it
to a variable. Doing so is called making
an instance. That's done like this:
let thing = Widget {
alfa: String::from("apple"),
bravo: String::from("bettey")
}
Accessing the values inside an instance of a struct is does using the varialbe name plus a dot followed by the name of the field. For example:
println!("{thing.alfa}");
Structs - Step By Step
TODO: Make sure to note that the let
expression needs a ;
at the end of it
Step By Step
Your Turn
SOURCE CODE
struct Widget {
alfa: String
}
fn main() {
let thing = Widget {
alfa: String::from("apple")
};
println!("{}", thing.alfa);
}
CODE RUNNER
Mutalbe Struct Instances
Struct instances are immutalbe by default.
They can be made mutable with the same
mut
keyword we've used before. For example,
instead of
let thing = Widget {
alfa: String::from("apple")
}
We'd use this
↓↓↓
let mut thing = Widget {
alfa: String::from("apple")
}
Mutalbe Struct Instances - Step By Step
Step By Step
Your Turn
Passing Structs To Functions
Step By Setp
Passing Structs To Functions
SOURCE CODE
struct Widget {
color: String
}
fn main() {
let alfa = Widget {
color: String::from("red")
};
report_on(&alfa);
}
fn report_on(thing: &Widget) {
println!("Color {}", thing.color);
}
CODE RUNNER
Passing Mutalbe Structs To Functions
Step By Step
Passing Mutable Structs To Functions
SOURCE CODE
struct Widget {
color: String
}
fn main() {
let mut alfa = Widget {
color: String::from("red")
};
update(&mut alfa);
println!("Color {}", alfa.color)
}
fn update(thing: &mut Widget) {
thing.color = String::from("green")
}
CODE RUNNER
Option Type
fn main() { let mut alfa = vec![ String::from("apple"), ]; let bravo = alfa.pop(); let item = alfa.pop(); match item { Some(value) => { println!("got {value}"); } None => { println!("got nothing"); } } }
fn main() { let mut alfa = vec![ String::from("apple"), ]; check_value(alfa.pop()); check_value(alfa.pop()); } fn check_value(value: Option<String>) { match value { Some(value) => { println!("got {}", value); } None => { println!("got nothing"); } } }
Example using a funciton
enum Widget { Alfa(String), Bravo } fn main() { let thing = Widget::Alfa( String::from("apple") ); check_thing(thing); } fn check_thing(value: Widget) { match value { Widget::Alfa(x) => { println!("alfa has {}", x); } Widget::Bravo => { println!("bravo has nothing"); } } }
Option Type
#![allow(unused)] fn main() { }
Options are returned by lots of things to let you know if there is a valid value or not.
More stuff on this page:
https://doc.rust-lang.org/book/ch06-02-match.html
Your Turn
struct Widget { thing: Option<String> } fn main() { let alfa = Widget { thing: Some( String::from("apple") ) }; match alfa.thing { Some(value) => { println!("thing is {}", value); } None => { println!("no value"); } } }
struct Widget { thing: Option<String> } fn main() { let bravo = Widget { thing: None }; match bravo.thing { Some(value) => { println!("thing is {}", value); } None => { println!("no value"); } } }
Struct Methods
Struct methods are like functions, but that are attached
to structs. They are put inside an impl
keyword code
block that has the same name as the struct they are
for. Here we are using an empty struct and adding
a method that prints out "the quick fox". (Notice
that the argument &self
is being passed to the
fn
. That is always the case for methods. (TODO:
talk more about that)
Note that methods are called with ()
.
struct Widget; impl Widget { fn do_output(&self) { println!("the quick fox"); } } fn main() { let thing = Widget; thing.do_output(); }
Using fields
This is done by using &self
which
is alwasy the first argument passed in
struct Widget { alfa: bool, } impl Widget { fn do_output(&self) { println!("alfa is {}", &self.alfa); } } fn main() { let thing = Widget { alfa: true }; thing.do_output(); }
In the more advanced section, point out that
&self
is shorthand for self: &Self
that Rust
provides out of the box.
Talks about using methods instead of functions as some orginaztion and keeping stuff together.
Passing values to methods
struct Widget; impl Widget { fn show_value(&self, value: i32) { println!("Value is {}", value); } } fn main() { let alfa = Widget; alfa.show_value(7); }
Associated Functions
these are like methods, but don't have '&self'
Maybe push this to later in the book.
String::from()
is an associated function.
They are often used to make a new instance of the thing
(e.g. with Widget::new
) by returning Self
The new
is not special or a reserved keyword or
otherwise build into the language.
NOTE: Things are normally writtin in the order:
struct
, impl
, main
but this works and is
easier for me to following when I'm making
learning.
The Self
in the fn
and return value are
aliases to what's named in impl
in this
case Widget
. You can replace Self
with Widget
and it'll still work.
fn main () { let thing = Widget::new(7); println!("thing.alfa {}", thing.alfa); println!("thing.bravo {}", thing.bravo); } impl Widget { fn new(load_value: i32) -> Self { Self { alfa: load_value, bravo: load_value, } } } struct Widget { alfa: i32, bravo: i32, }
Associated function also provide for
name spcing. Here to stucts are given
an associated function with the name
my_value
. Since both are called with
their struct names think get namespaced
and they don't conflict.
fn main() { Alfa::show_message(); Bravo::show_message(); } impl Alfa { fn show_message() { println!("alfa") } } impl Bravo { fn show_message() { println!("bravo") } } struct Alfa; struct Bravo;
Multiple impl blocks
You can make multilpe impl
blocks. The book
says there's not a real reason to do that for the
most part. There will be a later case with
generics types and traits. But kick this out
until then.
struct Widget; impl Widget { fn alfa(&self) { println!("alfa"); } } impl Widget { fn bravo(&self) { println!("bravo"); } } fn main() { let thing = Widget; thing.alfa(); thing.bravo(); }
Enums
Purpose of an email is to be one of a specific
type and nothing else. (TODO: Show structs
as enum types.)
TODO: Show how you have to cover all the options. And how you can do that with the catch all.
Show how the turn values from match must be the same if you are assinging it to a value.
Show a version where the stuff inside the emum value is a struct of some other type.
Your example:
enum Color { Red, Green, Blue } fn main() { let alfa = Color::Blue; match alfa { Color::Red => { println!("Red like fire"); } Color::Green => { println!("Green like grass"); } Color::Blue => { println!("Blue like the sky"); } } }
enum Color { Red(String), Green(String), Blue(String) } fn main() { let alfa = Color::Blue(String::from("navy")); match alfa { Color::Red(name) => { println!("{name} is red like fire"); } Color::Green(name) => { println!("{name} is green like grass"); } Color::Blue(name) => { println!("{name} is blue like the sky"); } } }
enum Color { Red(String), Green(String), Blue(String) } fn main() { let alfa = Color::Blue(String::from("navy")); let bravo = match alfa { Color::Red(name) => name, Color::Green(name) => name, Color::Blue(name) => format!("thing {name}") }; println!("bravo is {bravo}"); }
enum Color { Red(String), Green(String), } fn main() { let alfa = Color::Red(String::from("apple")); match alfa { Color::Red(name) => runRed(&name), Color::Green(name) => runGreen(&name), }; } fn runRed(value: &String) { println!("got {value}") } fn runGreen(value: &String) { println!("got {value}") }
enum Color { Red(String), Green(String), } fn main() { let alfa = Color::Red(String::from("apple")); let bravo = match alfa { Color::Red(name) => runRed(&name), Color::Green(name) => runGreen(&name), }; println!("bravo is {bravo}") } fn runRed(value: &String) -> String { format!("a {}", value) } fn runGreen(value: &String) -> String { format!("a {}", value) }
Original Notes
NOTE: It's possibe that you don't want to
show code until you show the if let
or the
match stuff for how to get something out.
Enums let you define something as one out of a possible set of values.
Anything that's valid as a stuct is also valid as an enum.
these are the base examples from the book, which I don't really understand becuase they don't really show how to use them.
enum IpAddrKind { V6, V4 } fn main() { let four = IpAddrKind::V4; println!("here"); }
That sets the type to ipAddrKind
overall.
This lets us do:
fn route(ip_kind, IpAddrKind) {}
The book goes on to show this
enum IpAddr { V4(String), V6(String) } fn main() { let home = IpAddr::V4(String::from("127.0.0.1")); let loopback = IpAddr::V4(String::from("::1")); }
TODO: Add impl function being assocaited with an enum. This is based off this: https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html Need to check and make sure this is a standard way to do stuff (becuse there wasn't a full example in the book.)
NOTE: This is not a good example.
enum Widget { Alfa, Bravo, } impl Widget { fn show_value(&self) { println!("Widget"); } } fn main() { let thing1 = Widget::Alfa; let thing2 = Widget::Bravo; thing1.show_value(); thing2.show_value(); }
This is an attempt to make an example that's easier to understand what's going on.
#[derive(Debug)] enum Widget { Alfa { value: i32 }, Bravo { value: f32} } fn main() { let thing1 = Widget::Alfa {value: 7}; let thing2 = Widget::Bravo {value: 3.4}; println!("here {:?}", thing1.value); }
Vecs
vec
stands for Vector. The basic details of a
vec
are:
- They hold a collection of values (like an array)
- All the values they hold must the same type (e.g.
String
) - It's possible to add and remove items from the
vec
Creating a vec
can be done with with vec![]
. For example:
let alfa = vec![
String::from("apple"),
String::from("berry"),
String::from("cherry")
];
Reading values in a vec
can be done with using the
zero based index/offest of the item to retrieve.
let bravo = &alfa[2]
The &
in &alfa[2]
means that we get a reference
to the value so that ownership isn't transferred.
Full example:
fn main() { let alfa = vec![ String::from("apple"), String::from("berry"), String::from("cherry") ]; let bravo = &alfa[2]; println!("bravo is {bravo}"); }
If the vec
is mutable, adding more elements can be
done with .push()
like this:
alfa.push(String::from("date");
fn main() { let mut alfa = vec![ String::from("apple"), String::from("berry"), String::from("cherry") ]; alfa.push(String::from("date")); let bravo = &alfa[3]; println!("bravo is {bravo}"); }
TODO: Cover .get()
which returns an Option<&T>
once
your figure out how those work.
This is the example from The Rust Book
#![allow(unused)] fn main() { let alfa = vec![ String::from("apple"), String::from("berry"), String::from("cherry") ]; let bravo = alfa.get(2); match bravo { Some(bravo) => { println!("bravo is {}", bravo); } None => { println!("No value at index 2"); } } }
Here's the same thing but using .get()
to request index 3
which doesn't exist.
The program doesn't crash. It shows the
None
arm of the match statement.
#![allow(unused)] fn main() { let alfa = vec![ String::from("apple"), String::from("berry"), String::from("cherry") ]; let bravo = alfa.get(100); match bravo { Some(bravo) => { println!("bravo is {}", bravo); } None => { println!("No value at index 100"); } } }
This is an example of the panic/crash if
you try to call an index that doesn't
exist with the &alfa[]
format.
#![allow(unused)] fn main() { let alfa = vec![ String::from("apple"), String::from("berry"), String::from("cherry") ]; let bravo = &alfa[100]; }
Note that when you use the &alfa[2]
format
to get a value the program will panic and crash
if the index is higher than the number of
items availabe.
Maybe that means you should use get
but only
put it in after you talk about Option<&T>
?
Or, maybe you leave this hear and address it later.
Like variables, having a mutable refernce
to a vec
means trying to add an immutable
one won't be allowed. For example, pulling
out one value via a reference then trying
to update the vec
by adding a new number
#![allow(unused)] fn main() { let mut alfa = vec![ String::from("apple"), String::from("berry"), String::from("cherry") ]; let bravo = &alfa[2]; alfa.push( String::from("date") ); println!("bravo is {bravo}"); }
The same scoping behavior that
applies to variables applies to vecs
.
If the last time bravo
is used is
before the .push()
to alfa everything
will work because bravo
goes out
of scope which means the reference
disappears.
#![allow(unused)] fn main() { let mut alfa = vec![ String::from("apple"), String::from("berry"), String::from("cherry") ]; let bravo = &alfa[2]; println!("bravo is {bravo}"); alfa.push( String::from("date") ); }
Iterating over a vector:
#![allow(unused)] fn main() { let alfa = vec![ String::from("apple"), String::from("berry"), String::from("cherry") ]; for bravo in &alfa { println!("bravo is {bravo}"); } }
Mutable version
#![allow(unused)] fn main() { let mut alfa = vec![ String::from("apple"), String::from("berry"), String::from("cherry") ]; for bravo in &mut alfa { bravo.push_str("pie") } for charlie in alfa { println!("charlie is {charlie}") } }
TODO: Check in the *i += 50
derefernce
format on the "Stroing Lists of Values
with Vectors" page
Using Enums to hold multiple types.
#![allow(unused)] fn main() { enum Holder { Widget(String), Thing(String) } let holders = vec![ Holder::Widget(String::from("alfa")), Holder::Thing(String::from("bravo")) ]; for item in &holders { match item { Holder::Widget(value) => { println!("got widget with {value}"); } Holder::Thing(value) => { println!("got thing with {value}"); } } } }
Strings
Create a string with text you already know:
let alfa = String::from("apple");
Create an empty string. Probably want to make it mutable so you can add stuff to it.
let mut alfa = String::new();
Add text
alfa.push_str("pie");
Adding a single character with .push()
alfa.push("s");
Concationation -
Use format!()
which is like println!()
but it makes a new string instead of printing
to output.
#![allow(unused)] fn main() { let alfa = String::from("apple"); let bravo = String::from("berry"); let charlie = format!("{}{}", alfa, bravo); println!("{alfa} {bravo} {charlie}"); }
at some point put in the details about using
+
and how ownership works. This is for
a later chapter. For now just stick with
format!()
. Note that ownership of
alfa
gets moved to charlie
so it
can't be used in the println!()
#![allow(unused)] fn main() { let alfa = String::from("apple"); let bravo = String::from("berry"); let charlie = alfa + &bravo; println!("{bravo} {charlie}"); }
Iterating over a string with .chars()
#![allow(unused)] fn main() { let alfa = String::from("apple"); for character in alfa.chars() { println!("character is {character}"); } println!("alfa is {alfa}"); }
TODO: Go over why &alfa[0..4]
is tricky
because with UTF-8 that's 2 characters
in Russian.
Strings are always valid UTF-8
.replace
makes a new string, but it doesn't
take ownership so alfa
is still
available
#![allow(unused)] fn main() { let alfa = String::from("the quick fox"); let bravo = alfa.replace("quick", "slow"); println!("alfa is {alfa}"); println!("bravo is {bravo}"); }
.contains()
TODO: Figures out when to show as_str()
stuff.
#![allow(unused)] fn main() { let alfa = String::from("the quick fox"); let bravo = String::from("quick"); if alfa.contains(bravo.as_str()) { println!("found {bravo}"); } else { println!("did not find {bravo}"); } }
Strings Original Text
We've seen two data types so far: i32
and bool
.
The first represents a number and the second is
a value that's either true
or false
. Now let's
start playing with text. To do that we'll use the
String
data type.
We only need to put the value we want to use on
the right side of the =
sign to create i32
and bool
type values. The String
type is
a little more complex. It uses this pattern:
String::from("brown");
Creating a String
and using it looks like
this:
SOURCE CODE
fn main() {
let color = String::from("brown");
println!("The quick {color} fox");
}
CODE RUNNER
Mutable Strings
Like the other Rust types, String
variables are
immutalbe by default. They can be made mutable with
the same mut
keyword as the other types as well.
We can do things like add more text to a String
if it's mutable. That can be done with the
method:
.push_str("brown fox")
For example, this prints out
the quick brown fox
SOURCE CODE
fn main() {
let mut alfa = String::from("the quick ");
alfa.push_str("brown fox");
println!("{}", alfa);
}
CODE RUNNER
To Examine
Truncate strings:
#![allow(unused)] fn main() { let mut alfa = String::from("apple"); alfa.truncate(2); println!("alfa is {alfa}"); }
Pop - removes the last value and returns it. (Seems to return it into a Some/None option)
#![allow(unused)] fn main() { let mut alfa = String::from("apple"); let bravo = alfa.pop(); match bravo { Some(value) => { println!("alfa {alfa} bravo {value}"); } None => { println!("alfa {alfa} bravo is None"); } } }
There's .remove()
but it talks about a byte
position which seems like not the happy
path way to go. Same with .remove_matches()
,
.insert()
, and .insert_matches()
.len()
- Returns the length of this String, in
bytes, not chars or graphemes. In other words,
it might not be what a human considers the length
of the string.
`.is_empty() - Returns true if this String has a length of zero, and false otherwise.
.clear()
- Truncates this String, removing all
contents.
.chars()
Returns an iterator over the chars of a string slice.
It’s important to remember that char represents a Unicode Scalar Value, and might not match your idea of what a ‘character’ is. Iteration over grapheme clusters may be what you actually want. This functionality is not provided by Rust’s standard library, check crates.io instead.
.split_whitespace()
Splits a string slice by whitespace.
The iterator returned will return string slices that are sub-slices of the original string slice, separated by any amount of whitespace.
‘Whitespace’ is defined according to the terms of the Unicode Derived Core Property White_Space. If you only want to split on ASCII whitespace instead, use split_ascii_whitespace.
.lines(&self)
.contains
.find()
Returns the byte index of the first character of this string slice that matches the pattern.
Returns None if the pattern doesn’t match.
.rfind()
Returns the byte index for the first character of the last match of the pattern in this string slice.
Returns None if the pattern doesn’t match.
.split and .rsplit
An iterator over substrings of this string slice, separated by characters matched by a pattern.
The pattern can be a &str, char, a slice of chars, or a function or closure that determines if a character matches.
.split_inclusive
.split_terminator
.splitn and .rsplitn
.split_once and .rsplit_once
.matches
.matches_indices
.trim
Returns a string slice with leading and trailing whitespace removed.
.trim_matches
.strip_prefix
Returns a string slice with the prefix removed.
If the string starts with the pattern prefix, returns substring after the prefix, wrapped in Some. Unlike trim_start_matches, this method removes the prefix exactly once.
If the string does not start with prefix, returns None.
is_ascii(&self)
Checks if all characters in this string are within the ASCII range.
eq_ignore_ascii_case
make_ascii_uppercase
.replace()
Replaces all matches of a pattern with another string.
replace creates a new String, and copies the data from this string slice into it. While doing so, it attempts to find matches of a pattern. If it finds any, it replaces them with the replacement string slice.
.to_lowercase()
.repeat()
Creates a new String by repeating a string n times.
Hash Maps
They aren't in the prelude. They must
be added with use std::collections::HashMap;
to make them available.
From the book
#![allow(unused)] fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); let team_name = String::from("Blue"); let score = scores.get(&team_name).copied().unwrap_or(0); println!("score {score}") }
NOTE: Tried to setup the value with a string like this but it got an error:
scores.insert(String::from("Blue"), 10);
I think that's because .copied()
is for i32
.
Overall, need to walk through that style syntax.
String
keys and values get moved into the
hashmap.
It's possible to use references
with
lifetimes
Overwriting a value (from the book)
#![allow(unused)] fn main() { use std::collections::HashMap; let mut widget = HashMap::new(); widget.insert(String::from("alfa"), 3); widget.insert(String::from("alfa"), 7); println!("{:?}", widget) }
Only add a values if it doesn't alrady exist.
#![allow(unused)] fn main() { use std::collections::HashMap; let mut widget = HashMap::new(); widget.entry(String::from("alfa")).or_insert(3); widget.entry(String::from("alfa")).or_insert(7); println!("{:?}", widget) }
This is from the book. Need to better understand it (and maybe come up with a refined example)
#![allow(unused)] fn main() { use std::collections::HashMap; let text = "hello world wonderful world"; let mut map = HashMap::new(); for word in text.split_whitespace() { let count = map.entry(word).or_insert(0); *count += 1; } println!("{:?}", map); }
Prototype 1
Source Code
Function Moves
Using a variable as an argument
to a function applies the same type
of ownership move. This works right
now because we don't try to access
alfa
again after sending it
to the function.
SOURCE CODE
fn main() {
let alfa = String::from("apple");
widget(alfa);
}
fn widget(value: String) {
println!("widget got {value}");
}
CODE RUNNER
Function Move Errors
However, if we try to use alfa
again by adding this line it'll
break with the same type of error
as before.
println!("alfa has {alfa}");
SOURCE CODE
fn main() {
let alfa = String::from("apple");
widget(alfa);
println!("alfa has {alfa}");
}
fn widget(value: String) {
println!("widget got {value}");
}
CODE RUNNER
if Expressions
if
expressions are used to determine
if a section of code should be run or not.
They are created with:
- The
if
keyword - A condition to check
- A block of code to run if the condition is true.
The condition in the example below checks to see if the
number 3
is less than the number 4
using the <
math
symbol like this:
3 < 4
Since the 3
is less than 4
the condition is
true so we'll see the output the condition is true
.
SOURCE CODE
fn main() {
if 3 < 4 {
println!("the condition is true");
}
}
CODE RUNNER
If/Else
We can provide an alternate block of code to run for
situations where the condition in the if
expression
is not true
. This is done with an else
expression.
The below example checks to see if 8
is less than 7
with:
8 < 7
That's not a true statement so if
sees the condition
as false
. Since if
only runs its code block if
the condition is true
it gets passed over. The
code in the else
block is run instead and we see
the output:
the condition is false
SOURCE CODE
fn main() {
if 8 < 7 {
println!("the condition is true");
} else {
println!("the condition is false");
}
}
CODE RUNNER
Comparison Operators
We used <
to check if one number was
less than another like this:
if 1 < 2 {
println!("The comparison is true");
}
The <
symbol is called an "operator". It
performs a comparison operation and give
us a result.
The <
looks the same as you see it
in everyday math. Some of the other
comparison operators look a little different:
Operator | Description |
---|---|
< | Less than |
<= | Less than or equal to |
> | Greater than |
>= | Greater than or equal to |
== | Equal to |
!= | Not equal to |
Here's what using ==
to check if two
numbers have the same value looks like:
SOURCE CODE
fn main() {
if 7 == 8 {
println!("They match");
} else {
println!("They don't match");
}
}
CODE RUNNER
Variables With If Expressions
Let's use variables in the condition section of our if/else expression. The first step is to bind the values to our variables.
let alfa = 7;
let bravo = 8;
Then, we'll replace the numbers in the condition
section of the if
statement with the variable
names. So, this:
if 7 == 8 {
...
}
Becomes this:
if alfa == bravo {
...
}
Here's the code for the full example which outputs:
They don't match
SOURCE CODE
fn main() {
let alfa = 7;
let bravo = 8;
if alfa == bravo {
println!("They match");
} else {
println!("They don't match");
}
}
CODE RUNNER
println! Variables
Binding values to variables let us use them repeatedly. For example, we can improve the output of our program by including the numbers that match or don't match.
We'll do this by using the variables in
println!()
the same way we have before.
The only difference this time is that
we're putting two variables in the format
string instead of one.
SOURCE CODE
fn main() {
let alfa = 7;
let bravo = 8;
if alfa == bravo {
println!("{alfa} matches {bravo}");
} else {
println!("{alfa} does not match {bravo}");
}
}
CODE RUNNER
Data Types
Rust has "data types". Each one defines a different
kind of data. Every value in Rust has to fit
into one of those types. For example, numbers
without decimal points have a type of i32
.
One way to think about it is like asking someone what kind of dog they have.
What kind of dog is Charlie?
A golden retriever
We can ask a similar question about values in Rust:
What type of variable is alfa?
An i32
The i32 Type
The first type we'll talk about is i32
.
It's a number without decimal places.
We've been using it without calling its
type out by name.
When we need to think about types we
can now say that we're binding an
i32
value of 7
to the varailbe
name alfa
SOURCE CODE
fn main() {
let alfa = 7;
println!("alfa is {alfa}");
}
CODE RUNNER
Function Arguments
Functions can receive values from other parts of the program when they are called.
To do this, Rust needs to know what type of
vaule the function can accept. We
specify that in the ()
parenthesis that
follow the functions name in the definition.
For our example, we'll use:
(value: i32)
value
is a variable name that the
incoming parameter gets bound to inside
the function. That's followed by a :
that acts as a separator and then
the type of value the function expects
(i32
in this case).
To use this, we'll change our function from:
fn alfa() {
...
}
To
fn alfa(value: i32) {
...
}
Sending a value to the function is done
by putting it inside the ()
parenthesis
when after the name of the fuction in the
call.
So this:
alfa();
Becomes this when we pass the value 7
(which is an i32
)
alfa(7);
Here's a full version of the program that outputs:
call alfa next
alfa got 7
SOURCE CODE
fn main() {
println!("call alfa next");
alfa(7);
}
fn alfa(value: i32) {
println!("alfa got {value}");
}
CODE RUNNER
Program: Conditional Functions
We've covered a lot of gound: variables,
if/else, comparison operators, functions, and
the i32
data type. Each of the examples we
used was scoped down as much as possible to
focuse on just the concept at hand. Now,
let's write a larger program with everything
we've learned that does a little more.
Here's what we'll end up with:
- A
check_numbers
function that takes twoi32
values. - An
if/else
expression that determines if the first value is less than the seond println!()
outputs that use the values- A set of
i32
variables in ourmain
function - Calls to the
check_numbers
function with the sets of variables.
Here's the source which outputs:
4 is less than 9
7 is not less than 3
SOURCE CODE
fn main() {
let alfa = 4;
let bravo = 9;
check_numbers(alfa, bravo);
let charlie = 7;
let delta = 3;
check_numbers(charlie, delta);
}
fn check_numbers(value1: i32, value2: i32) {
if value1 < value2 {
println!("{value1} is less than {value2}");
} else {
println!("{value1} is not less than {value2}");
}
}
CODE RUNNER
Immutable Variables
Our Contional Function program uses four variables:
let alfa = 4;
let bravo = 9;
and
let charlie = 7;
let delta = 3;
Another approach is to change the values
of alfa
and bravo
instead of creating charlie
and
delta
. We can do that, but we need to change
the way we create alfa
and bravo
first because
Rust variables are immutalbe
by default. That means
that once we bind a value to them we can't change it.
For example, run this and you'll get an error that we'll discuss on the next page.
SOURCE CODE
fn main() {
let alfa = 7;
println!("alfa is {}", alfa);
alfa = 9;
println!("alfa is {}", alfa);
}
CODE RUNNER
Assigning Twice Error
When we created a variable with:
let alfa = 7;
and then tried to change it's value with this:
alfa = 9;
We got this error:
Compiling playground v0.0.1 (/playground)
error[E0384]: cannot assign twice to immutable variable `alfa`
--> src/main.rs:6:3
|
3 | let alfa = 7;
| ----
| |
| first assignment to `alfa`
| help: consider making this binding mutable: `mut alfa`
...
6 | alfa = 9;
| ^^^^^^^^ cannot assign twice to immutable variable
For more information about this error, try `rustc --explain E0384`.
error: could not compile `playground` due to previous error
Rust's error messages are verbose, but great. They give us nice, direct statements about what's wrong. Take this line from our error.
error[E0384]: cannot assign twice to immutable variable `alfa`
We made a first assignment to alfa
in this default way
which make it immutable:
let alfa = 7;
Later, we tried to update the value in the variable by
assining 9
to it like this.
alfa = 9;
Rust won't let us do that since alfa
is immutalbe so we get
that error message which tells us what's happening.
Something that's great about Rust is that error messages often contian recommendations on how to fix what went wrong. Take this line for example:
help: consider making this binding mutable: `mut alfa`
It suggestions not only that we make alfa
mutable, but it
also shows up how.
Mutable Variables
Rust variables are immutalbe by default. That means
you can't change them after they've been set. The
mut
keyword makes them mutable allowing them
to be changed after they are created.
Here's an example of creating a default (immutable) variable:
let alfa = String::from("apple");
And this is how to create one using the mut
keyword
to make it mutable:
let mut alfa = String::from("apple");
Program: Conditional Functions V2
Here's a minor update to our function to use mutalbe variables by changing this:
let alfa = 4;
let bravo = 9;
check_numbers(alfa, bravo);
let charlie = 7;
let delta = 3;
check_numbers(charlie, delta);
To this:
let mut alfa = 4;
let mut bravo = 9;
check_numbers(alfa, bravo);
alfa = 7;
bravo = 3;
check_numbers(alfa, bravo);
SOURCE CODE
fn main() {
let mut alfa = 4;
let mut bravo = 9;
check_numbers(alfa, bravo);
alfa = 7;
bravo = 3;
check_numbers(alfa, bravo);
}
fn check_numbers(value1: i32, value2: i32) {
if value1 < value2 {
println!("{value1} is less than {value2}");
} else {
println!("{value1} is not less than {value2}");
}
}
CODE RUNNER
For Loops
Rust has for
loops which allow us to run the same
block of code multiple times.
The formula for a for
loop that runs through a range
of numbers is:
- The
for
keyword - A variable name to hold the value of the number on each loop
- The
in
keyword - The range of numbers
- The code block to run each time though the loop
The range
has the format of:
- The starting number
- Two dots followed by an equal sign (i.e.
..=
) - The ending number
For example:
1..=5
And here's a full example that outputs:
alfa is 1
alfa is 2
alfa is 3
alfa is 4
alfa is 5
SOURCE CODE
fn main() {
for alfa in 1..=5 {
println!("alfa is {alfa}");
}
}
CODE RUNNER
With Variables
We can also use variables for the starting and ending numbers of the range. Instead of:
1..=5
We set start
and end
variables then
use them with:
start..=end
Here's the updated version that also outputs:
alfa is 1
alfa is 2
alfa is 3
alfa is 4
alfa is 5
SOURCE CODE
fn main() {
let start = 1;
let end = 5;
for alfa in start..=end {
println!("alfa is {alfa}");
}
}
CODE RUNNER
Arithmetic Operators
Rust lets you do math through arithmetic operators
. They are:
Operator | Description |
---|---|
+ | Addition |
- | Subtraction |
* | Multiplication |
/ | Division |
% | Remainder |
Addition, subtraction, and multiplication all work the
way you'd expect on i32
numbers. For example, this
will give us values of 9
, -1
, and 20
for the
three variables.
SOURCE CODE
fn main() {
let alfa = 4 + 5;
let bravo = 4 - 5;
let charlie = 4 * 5;
println!("alfa is {}", alfa);
println!("bravo is {}", bravo);
println!("charlie is {}", charlie);
}
CODE RUNNER
Division Operator
When used with i32
type values the /
operator for division works differently
than you might expect. For example,
dividing 9
by 2
on a calculator
yields 4.5
. Since i32
values don't
have decimal points we only get the
portion on the left side.
Running this will output:
alfa is 4
SOURCE CODE
fn main() {
let alfa = 9 / 2;
println!("alfa is {}", alfa);
}
CODE RUNNER
Remainder Operator
The remainder operator gives us the value that's left over after putting one number into another as many times as possible. (TODO: Look up the math term for this)
For example, 2
goes into 9
four times
with a remainder of 1
. So, this code
outputs:
alfa is 1
SOURCE CODE
fn main() {
let alfa = 9 % 2;
println!("alfa is {alfa}");
}
CODE RUNNER
Arithmetic Operators With Comparison Operators
The value returned by an arithmetic expressoin can be used to set a value like we've already seen:
let alfa = 4 + 9;
They can also be used directly as one half
of an if
expression with a conditional
operator.
For example, instead of:
#![allow(unused)] fn main() { if 7 < 10 { ... } }
We could do:
#![allow(unused)] fn main() { if 3 + 4 < 10 { ... } }
We could even use arithmetic operations on both
sides of the =
sign like this example that
outputs:
is the value more?
no
SOURCE CODE
fn main() {
println!("is the value more?");
if 3 + 4 > 5 + 5 {
println!("yes");
} else {
println!("no");
}
}
CODE RUNNER
Comparison Operators With Variables
We can also use variables for the values to compare.
is the value less?
no
SOURCE CODE
fn main() {
let alfa = 7;
let bravo = 3;
let charlie = 9;
println!("is {alfa} + {bravo} less than {charlie}?");
if alfa + bravo < charlie {
println!("yes");
} else {
println!("no");
}
}
CODE RUNNER
Using The Remainder
The %
remainder operator is surprigingly useful
in combination with the comparison operators. One
example is using it to divide content into
groups.
For exmaple, if we use %
with an incrementing
number on the left side and 3
on the right side
it will return zero for everything that's
divisible by three with no remainder. If we
run a comparison against that value in an
if statement we can process every third value
differently.
Take this code for example that outputs:
1 <----
2 <----
----> 3
4 <----
5 <----
----> 6
7 <----
8 <----
----> 9
10 <----
SOURCE CODE
fn main() {
let start = 1;
let end = 10;
let target = 3;
for alfa in start..=end {
if alfa % target == 0 {
println!("----> {alfa}")
} else {
println!("{alfa} <----")
}
}
}
CODE RUNNER
Using The Remainder V2
Here's the same thing, but using 4
for
the value on the right side:
The output changes from:
1 <----
2 <----
----> 3
4 <----
5 <----
----> 6
7 <----
8 <----
----> 9
10 <----
To:
1 <----
2 <----
3 <----
----> 4
5 <----
6 <----
7 <----
----> 8
9 <----
10 <----
To this by just chaning the single target number
from 3
to 4
.
SOURCE CODE
fn main() {
let start = 1;
let end = 10;
let target = 4;
for alfa in start..=end {
if alfa % target == 0 {
println!("----> {alfa}")
} else {
println!("{alfa} <----")
}
}
}
CODE RUNNER
Booleans
We've used a bunch of i32
type values. It's
time to introduce our next data type: bool
The bool
data type stands for boolean.
A value that can be either true
or
false
and nothing else.
Boolean values are bound to variables like this:
let alfa = true;
let bravo = false;
The if
statements we've been using
check to see if the condition is true
or not. We can use bool
values
SOURCE CODE
fn main() {
let mut alfa = true;
let mut counter = 1;
while alfa == true {
println!("counter is {}", counter);
if counter == 5 {
alfa = false;
}
counter = counter + 1;
}
}
CODE RUNNER
In Conditionals
if
statements check a condition to see
if it's true or false. We've been using
expressions like 3 < 4
for as those
conditions. We can also use bool
type
variables.
For example, this binds the bool
value
false
to the alfa
variable then uses
it as the condition in the if
statement.
The output is:
alfa is false
SOURCE CODE
fn main() {
let alfa = false;
if alfa {
println!("alfa is true");
} else {
println!("alfa is false");
}
}
CODE RUNNER
Function Return Values
We've set up functions to receive argumets by putting a name and type inside the parenthesis after the function name:
fn alfa(value: i32) {
println!("alfa got {}", value);
}
So far, all the functions have printed
something out when called. Now we'll
get them to return data to whatever
called them. Setting that up is done by adding
->
after the parenthesis followed by
the type of data that will be returned.
The data that gets returned is generated
by the last expression in the function's
code block. For example, this double_number
function takes an i32
value and multiplies
it by 2
as it's sending it back.
SOURCE CODE
fn main(){
let alfa = double_number(5);
println!("alfa is {}", alfa);
}
fn double_number(value: i32) -> i32 {
value * 2
}
CODE RUNNER
Different Types
Here's another look at the privous function:
fn double_number(value: i32) -> i32 {
value * 2
}
It takes an i32
:
double_number(value: i32)
And returns an i32
:
-> i32
The input type and the output type
don't have to match. In fact, return
values can be sent from functions
that don't receive any arguments.
Here we create a function that
takes no arguments and always
returns true
.
SOURCE CODE
fn main() {
let alfa = get_true();
println!("alfa is {}", alfa);
}
fn get_true() -> bool {
true
}
CODE RUNNER
Returning From if
A function that always returns
true
isn't particually useful. A
more realistic example is a function
that returns true
based on some
condition. This example uses
a function to determine if a value
is above 5.
SOURCE CODE
fn main() {
let alfa = 7;
let bravo = is_over_five(alfa);
println!("{} is over five? {}", alfa, bravo);
}
fn is_over_five(value: i32) -> bool {
if value > 5 {
true
} else {
false
}
}
CODE RUNNER
Functions As Conditions
We can use the return values as conditions:
SOURCE CODE
fn main() {
let alfa = 7;
if check_number(alfa) {
println!("got true");
} else {
println!("got false");
}
}
fn check_number(value: i32) -> bool {
if value == 7 {
true
} else {
false
}
}
CODE RUNNER
Program 3 NAME TKTKTKT
TODO: Make a version of this with a Vec that returns both the bool and the i32.
SOURCE CODE
fn main() {
let start = 1;
let end = 100;
let splitter = 8;
for counter in start..=end {
if should_process(counter, splitter) {
let how_many_left = count_how_many_left(counter, end);
println!("at {counter} with {how_many_left} remaining");
}
}
}
fn should_process(value1: i32, value2: i32) -> bool {
if value1 % value2 == 0 {
true
} else {
false
}
}
fn count_how_many_left(value1: i32, value2: i32) -> i32 {
value2 - value1
}
CODE RUNNER
Scopes
TKTKTK Come up with better descriptions and diagrams for scope.
Rust uses scopes to define what variables are availalbe in different parts of applicaitons.
One way to think about scopes is like one big box a collection of smaller boxes inside it. Each smaller box can have more boxes inside of them and so on and so forth. A value stored in a box is avaialble to that box and any box surround it on the path up to the top box. But, boxes on the sides can't access it.
> global 1
> global:main 2 fn main() {
> global:main 3 println!("in main");
> global:main 4 }
> global 5
Difficulty Increasing
A quick heads: we're getting into sections that deal with the more conceptually complicated parts of Rust. They're regarded as the harder part of learning the language.
Going through them multiple times before things click happenes more often than not.
Illustrations Incoming
My handwriting is chicken scratch. The upcoming illustrations are drafts. They'll get replaced once I've got things more finalized.
References Old
Mutable References
Mutable references are created using &mut
instead
of &
to make the reference. The first variable
must be created with mut
too. When we make
modifications to the reference the value
change shows up in the alfa
variable too.
The steps for this example are:
- Create a mutable variable named
alfa
with aString
bound to it - Print
alfa
to display the value - Create a variable named
bravo
that has a mutable reference toalfa
- Update the value in
bravo
with.push_str()
- Print out
bravo
to display its value - Print out
alfa
again to show that the value updated there too
The output of this code is:
alfa has apple
bravo has applepie
alfa has applepie
SOURCE CODE
fn main() {
let mut alfa = String::from("apple");
println!("alfa has {alfa}");
let bravo = &mut alfa;
bravo.push_str("pie");
println!("bravo has {bravo}");
println!("alfa has {alfa}");
}
CODE RUNNER
A Single Value
TKTKTKT
SOURCE CODE
fn main() { let mut alfa = String::from("widget"); println!("alfa is {alfa}"); { let bravo = &mut alfa; println!("bravo is {bravo}"); bravo.push_str("-thing"); println!("bravo is {bravo}"); } println!("alfa is {alfa}"); }
CODE RUNNER
Using In A Function
This is how to pass a reference to a function where you don't loose ownership
SOURCE CODE
fn main() {
let alfa = String::from("apple");
println!("alfa is {alfa}");
show_value(&alfa);
println!("alfa is {alfa}");
}
fn show_value(param: &String) {
println!("show_value got {param}");
}
CODE RUNNER
Changing In A Function
SOURCE CODE
fn main() { let mut alfa = String::from("widget"); println!("alfa is {alfa}"); update_value(&mut alfa); println!("alfa is {alfa}"); } fn update_value(value: &mut String) { value.push_str("-thing"); }
CODE RUNNER
A Function Error
Ownership And Moving
Every value in Rust has an "owner". For example, this
statement assigns a String
with the value of apple
to the variable alfa
. That makes alfa
the owner
of the String
.
let alfa = String::from("widget");
If we create a new variable called bravo
by binding
the value of alfa
to it, the ownership of the String
passes from alfa
to bravo
. So this works and will output
alfa contains widget
bravo contains widget
SOURCE CODE
fn main() {
let alfa = String::from("widget");
println!("alfa contains {alfa}");
let bravo = alfa;
println!("bravo contains {bravo}");
}
CODE RUNNER
Moving Strings Error
Moving a value means it's no longer in the place
it was before. (That is, it's really a move
and
not a copy
)
So, when the value of alfa
gets moved into
bravo
it's no longer available in alfa
. Trying
to use it throws an error like this example:
SOURCE CODE
fn main() {
let alfa = String::from("widget");
println!("alfa contains {alfa}");
let bravo = alfa;
println!("bravo contains {bravo}");
println!("alfa contains {alfa}");
}
CODE RUNNER
Cloning And Referencing
Trying to access a value after we moved it to a new variable resulted in an error message that contined this line:
help: consider cloning the value if the performance cost is acceptable
Cloning is done using the .clone()
method. Doing so
changes this:
let bravo = alfa;
into this:
let bravo = alfa.clone();
When we clone, it makes a new copy of
the String
that's bound to alfa
and then
binds that new copy to bravo
. Since the value
wasn't moved out of alfa
we can still use it
like this sample which outputs:
alfa is widget
bravo is widget
alfa is widget
SOURCE CODE
fn main() {
let alfa = String::from("widget");
println!("alfa contains {alfa}");
let bravo = alfa.clone();
println!("bravo contains {bravo}");
println!("alfa contains {alfa}");
}
CODE RUNNER
Cloning For Separate Values
Cloning the value allows us to modify
alfa
and bravo
independently (assuming
we've made them mutable with the mut
keyword).
The out we'll get is:
alfa contains widget
bravo contains widget
alfa contains widget-alfa
bravo contains widget-bravo
SOURCE CODE
fn main() {
let mut alfa = String::from("widget");
let mut bravo = alfa.clone();
println!("alfa contains {alfa}");
println!("bravo contains {bravo}");
alfa.push_str("-alfa");
bravo.push_str("-bravo");
println!("alfa contains {alfa}");
println!("bravo contains {bravo}");
}
CODE RUNNER
Binding Values From if
if
statements are expressions. That means they return
a value. So, using one on the right side of an =
sign
is completely valid.
For example:
SOURCE CODE
fn main() {
let alfa = true;
let bravo = if alfa { 1 } else { 0 };
println!("bravo is {bravo}")
}
CODE RUNNER
Must Be The Same Type
TODO: Put in note earlier about calling the
branches arms
Using an if/else
expression to determine a
variable to bind directly to a variable requires
both the if
arm and the else
arm to provide
the same type. So you can have two i32
values
like this:
let alfa = true;
let bravo = if alfa { 1 } else { 0 };
or two bool
values like this:
let alfa = true;
let bravo = if alfa { true } else { false };
but you can't mix them like this which will give an error:
SOURCE CODE
fn main() {
let alfa = true;
let bravo = if true { 1 } else { false };
println!("bravo is {bravo}");
}
CODE RUNNER
Mismatched Type Error
TKTKTKT - Fill in the details on this error.
Here's the error from the prior page:
Compiling playground v0.0.1 (/playground)
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:3:36
|
3 | let bravo = if true { 1 } else { false };
| - ^^^^^ expected integer, found `bool`
| |
| expected because of this
For more information about this error, try `rustc --explain E0308`.
error: could not compile `playground` due to previous error
Sandbox
SOURCE CODE
fn main() { let alfa = String::from("quick"); let bravo = alfa; println!("{alfa}"); }
fn main() { let alfa = String::from("quick"); widget(alfa); println!("{alfa}"); } fn widget(value: String) { println!("{value}"); }
While Loops
Rust provides while
loops in addition
to for
loops. Mutable variables work
well with them by acting as counters.
In this case we'll set a mutable
variable named counter
for a while
loop that checks to see if it's less
than or equal to 5
.
Everytime through 1
add 1
to the counter with += 1
which takes the current value and
adds one to it.
SOURCE CODE
fn main() {
let mut counter = 1;
while counter <= 5 {
println!("counter is {}", counter);
counter = counter + 1;
}
}
CODE RUNNER
Variables In println!
The code from the prior page includes this line:
println!("alfa is {alfa}");
which produced this output:
alfa is 7
The characters first piece of text quoted inside
println!()
is called a "format string". Format
strings can output basic text like we saw with the
original println!("Hello, World")
example. They can
also output the value of variables like we're
doing with alfa
above.
The process works by adding {}
curly brackets
with the name of the variable inside it. In our
case this is:
{alfa}
Here's another example where we set the variable
bravo
to 12 then print it out.
fn main() {
let bravo = 12;
println!("bravo is {bravo}");
}
The String Type
The i32
type allows us to drop in a
number on the first side of the =
sign with no extra fanfare.
let alfa = ;
The String
type requires more work.
Creating a String
variable looks like
this:
let alfa = String::from("Hello");
String are defined like this:
let alfa = String::from("Hello");
With i32
we could simply drop in the number on
the right side of the =
, like:
let alfa = 7;
the much longer String::from("Hello")
. The first
word String
defines the type. Then, from()
is
where we pass in the piece of text we want to
String to be made of.
SOURCE CODE
fn main() {
let alfa = String::from("Hello");
println!("alfa is {}", alfa);
}
CODE RUNNER
Vec
Else If Expressions
There's a third option we can add beside
if
and else
. It's called else if
. Using
else if
creates a new if
statement that
only runs if the thing before it was false.
Assignment Operators
That while example added a new type of expression we haven't seen yet:
counter += 1;
The +=
is an "assignment operator". They're
like the comparison operators we saw earlier (like
<
, >=
and ==
). But, instead of telling
us if the expression they are in is true, assignments
operators do math on the values on either side.
For example, the +=
adds whatever is on the
right side to the value on the left. So, this
will give us an output of
1
2
3
SOURCE CODE
fn main() {
let mut alfa = 1;
println!("{}", alfa);
alfa += 1;
println!("{}", alfa);
alfa += 1;
println!("{}", alfa);
}
CODE RUNNER
Assignment Operator Examples
The different assignment operators are:
Operator | Description |
---|---|
+= | Addition |
-= | Subtraction |
*= | Multiply |
/= | Division |
%= | Remainder |
Addition, subtraction, and multiplication
behave like everyday math. Here we'll
make an initial set of varilbes and
set them to 3
. Then we'll add, subtract,
and multiple them with 4
using the assignment
operators. That will give us:
7
-1
12
SOURCE CODE
fn main() {
let mut alfa = 3;
let mut bravo = 3;
let mut charlie = 3;
alfa += 4;
println!("{}", alfa);
bravo -= 4;
println!("{}", bravo);
charlie *= 4;
println!("{}", charlie);
}
CODE RUNNER
Division
The /=
operator for division works a little
differently than everyday math. Dividing i32
numbers that go into each other evently works
as expected. For example, 6
divided by 2
will result in 3
.
However, dividing numbers that don't go into each other evenly results in just the full number of times it can (TODO: Look up the math terms for that.)
Using 2
and 5
doesn't give us 2.5
, it
gives us just the part on the left side of
the decimal (i.e. 2
)
SOURCE CODE
fn main() {
let mut alfa = 5;
alfa /= 2;
println!("alfa is {}", alfa);
}
CODE RUNNER
Remainder
We use the %=
(remainder) assignment
gives us the remainder from a division
operation. For example, dividing 9
by 2
results in 2
going into 9
four times with a remainder of 1
. That
1
is what the %=
will give us.
SOURCE CODE
fn main() {
let mut alfa = 9;
alfa %= 2;
println!("alfa is {}", alfa);
}
CODE RUNNER
Data Types
Rust's design requires every value have a "type" assigned to. The "type" of a value is what you'd used to answer a question about what kind of thing it is. For example, if you were asking someone about their dog:
What kind of dog is Charlie?
A golden retriever
And, in a similar manner when discussing a Rust program:
What type of variable is alfa?
A number with a decimal point
Except we wouldn't use the term "a number with a
decimal point". We'd use f32
. That's what Rust
uses to set the type of a value to a "floating-point
number" (which is a number that has a decimal point
compared to an "integer" which doesn't).
As we saw in the previous chapter, types are assigned
to variables when they are created. We used i32
in the prior examples for integers. We could have used
f32
for floating-point numbers just as easily:
For example:
SOURCE CODE
fn main() {
let alfa: f32 = 3.4;
println!("Value {}", alfa);
}
CODE RUNNER
Implicit And Explicit
In all our previous examples we've set the
type of our variable explicitly by adding
a :
after the name along with the type
(e.g. i32
). Rust has the ability to
guess the type of some variables so
that's not always necessary. When we do
that it's called an "implicit" type
assignment and it looks like this:
let alfa = 7;
That turns our formula for defining a variable into this:
- The
let
keyword - The optional
mut
keyword - A name for the variable (e.g.
alfa
) - A
:
separator if we're explicitly setting the data type - An optional data type
- The
=
sign - The value to bind to it (e.g.
7
) - A
;
the ends the definition
In this example, both alfa
and bravo
have an i32
type. Alfa is defined
explicitly. Bravo is defined implicitly.
fn main() {
let alfa: i32 = 7;
let bravo = 9;
println!("Alfa {} - Bravo {}", alfa, bravo)
}
Using Explicit Types
Setting data types implicitly like this:
let alfa = 7;
or explicitly like this:
let alfa: i32 = 7;
are both frequency occurrences in Rust programs. While the implicit version is shorter to type and easier to read we'll be using the explicit approach for a few reasons:
- To get used to the idea that every value has a type
- To use a consistent format in our examples
- Because you can always use explicit compared to implicit types which only work sometimes.
The Number Types
We've seen both f32
for floating point numbers
that have a decimal points and i32
for integers
that don't. There's a third type of number
called u32
which is for "unsigned integer".
A regular i32
integer can be both positive
and negative where the negative numbers have
a "-
" sign in front of them. When we say
a u32
is unsigned, it means it can't have
a "-
" sign. That it turn means it can't
be negative.
This brings us to the 32
that follows the
f
, i
, and u
in the types. It
determines how big a number can be used for
the variable.
Here's the values for floating point numbers
type | lowest number | highest number |
---|---|---|
f8 | TKTKTKT | TKTKTKT |
f16 | TKTKTKT | TKTKTKT |
f32 | TKTKTKT | TKTKTKT |
f64 | TKTKTKT | TKTKTKT |
f128 | TKTKTKT | TKTKTKT |
Here's the values for integers
type | lowest number | highest number |
---|---|---|
i8 | TKTKTKT | TKTKTKT |
i16 | TKTKTKT | TKTKTKT |
i32 | TKTKTKT | TKTKTKT |
i64 | TKTKTKT | TKTKTKT |
i128 | TKTKTKT | TKTKTKT |
And, here's the values for unsigned integers
type | lowest number | highest number |
---|---|---|
u8 | TKTKTKT | TKTKTKT |
u16 | TKTKTKT | TKTKTKT |
u32 | TKTKTKT | TKTKTKT |
u64 | TKTKTKT | TKTKTKT |
u128 | TKTKTKT | TKTKTKT |
TODO
-
Fill in the high/low values
-
Add notes about
arch
Booleans
Rust has another fundamental data type called "Boolean".
A boolean is either true or false. It
can't be anything else. They use the
bool
keyword and are assigned like
this:
let bravo: bool = false;
Used in a full program it looks like this:
SOURCE CODE
fn main() {
let alfa: bool = true;
println!("Value {}", alfa);
}
CODE RUNNER
Compound Data Types
The floating-point, integer, and boolean data types we've covered each hold a single value. Collectively, they're called "scalar" types (TODO: find where the term scalar comes from)
Rust has another category of types called "compound" types. They hold multiple values instead of being constrained to a single one.
Compound data types come in two flavors: Tuples and Arrays.
Tuples
Tuples are containers that hold other values of different types.
Creating a tuple variable looks different than
making one for a scalar data type.
Instead of using a specific keyword after the variable's
name (like i32
in let alfa: i32 = 58
), tuple definitions
use a pair of parenthesis with the types it's
going to contain between them.
To the right side of the equal sign the values to assign are placed in a corresponding set of parenthesis.
For example, defining a tuple with three integers looks like this:
let alfa: (i32, i32, i32) = (5, 7, 9);
Making one with a float and a boolean looks like this:
let bravo: (f32, bool) = (37.9, false);
Tuple Indexes
Getting values out of a tuple is done using the variable's name followed by a dot and the "index" number for the position we're after.
For example:
alfa.2
The index position represents a counter
that starts at the beginning of the tuple
and goes up one for each position. But, the
first number is zero instead of one. So,
the first position is 0
, the second position
is 1
, etc...
Here's an example creating a tuple that holds an
i32
, a f32
, and a bool
and then prints
them out.
SOURCE CODE
fn main() {
let alfa: (i32, f32, bool) = (99, 234.5, false);
println!("1st {}", alfa.0);
println!("2nd {}", alfa.1);
println!("3rd {}", alfa.2);
}
CODE RUNNER
Indexes And Offsets
The name for the way these index numbers work is "zero based index". The way I've learned to think about them is as a position offset.
We always start with the first item position.
If we want the value of that item it doesn't
require moving. That translates, to zero offset
from which means index of 0
.
The second number is one away from the start.
So, we access it with an index of 1
.
To get to the third number requires moving
two steps from the start. That gives us
and index of 2
Regardless of the way you think about it, working with zero based indexes can take some time to get used to. Even folks who've been programming for years screw it up sometimes.
Functions
Every program we've run so far has consisted of
the single main
function. It's definition
starts with fn
followed by the name of the
function with some parenthesis (i.e. main()
),
then the opening and closing {}
curly braces
that wrap the code that gets executed when
the function is called.
We can add more functions to our programs as well using the same approach. For example, here's the definition of a function named "alfa" that prints out "I am alfa":
fn alfa() {
println!("I am alfa");
}
The function is used by "calling it". The syntax for that is the name of the function followed by parenthesis:
alfa();
Here's a full program where we
print I am main
inside the main function
then call the alfa
function which
then prints I am alfa
SOURCE CODE
fn main() {
println!("I am main");
alfa()
}
fn alfa() {
println!("I am alfa");
}
CODE RUNNER
Function Parameters
We can also send data to functions for them to use. In order to do this we have to let the functions know a few things. To start with, those are:
- That data will be coming in
- What type of data it will be
- A variable name to use to hold the incoming data
The way we do that is to assign a variable inside
the parenthesis after the function name. The format
is similar to what we've used to assigning variable
so far but we remove the let
, =
, and the value.
So instead of this:
let alfa: i32 = 3
we'd pull out just this part:
alfa: i32
To use that in a function named widget
we'd write this:
fn widget(alfa: i32) {
println!("I am alfa");
}
TODO: Combine this with the next page since it doesn't really make sense to show the incoming parameter but not use it.
Using Function Parameters
Our widget function defined a parameter
called alfa
of type i32
. In order
to use it, we have to pass it in when
we call the function. This is done
by putting the value we want to use
inside the parenthesis when we call
the function. For example:
widget(7);
Once we do that we can use the value
vai the alfa
variable we set inside
the function definition.
For example:
fn main() {
println!("This is main");
widget(7);
}
fn widget(alfa: i32) {
println!("This is widget with {}", alfa);
}
Multiple Function Parameters
Defining functions that take more than one parameter is done by adding more in the parentheses that are separated by commas like this:
widget(alfa: i32, bravo: f32)
Here's a full program:
SOURCE CODE
fn main() {
println!("This is main");
widget(7, 3.4);
}
fn widget(alfa: i32, bravo: f32) {
println!("This is widget");
println!("Alfa {} - Bravo {}", alfa, bravo);
}
CODE RUNNER
Using Multiple Parameters
Parameters don't have to be kept independent
when we pass them into a function. For example,
if we use to i32
integers we can add them
together in a new variable and print it. In
this case we'll use a function named
do_addition
. The output of the program
will be:
This is main
do_addition made 8
SOURCE CODE
fn main() {
println!("This is main");
do_addition(3, 5);
}
fn do_addition(alfa: i32, bravo: i32) {
let sum = alfa + bravo;
println!("do_addition made {}", sum);
}
CODE RUNNER
Function Return Values
The widget
function in the previous
examples received a value from main
but didn't pass anything back. Setting
up to do that is a matter of defining
the type of data the function will
return. This is done by adding
a ->
and data type after the function
name and associated ()
for the
incoming parameters.
The format without any incoming
parameters that returns an i32
integer looks like this:
widget() -> i32
Here's what everything looks like in a program. It will output:
widget is 10
SOURCE CODE
fn main() {
println!("widget is {}", widget());
}
fn widget() -> i32 {
5 + 5
}
CODE RUNNER
Binding Return Values
The values return from functions can
be bound to variables. For example,
if we create a variable named alfa
of type i32
we can set it to
the return value of a widget
function that has the same time.
Here's a program which outputs:
alfa is 9
SOURCE CODE
fn main() {
let alfa = widget();
println!("alfa is {}", alfa);
}
fn widget() -> i32 {
3 + 6
}
CODE RUNNER
Return Expressions
The widget
function in the last example
looked like this:
fn widget() -> i32 {
3 + 6
}
I bring that up to point out that there isn't
a ;
after the 3 + 6
. This is different
from all the other lines we've seen
in functions so far.
There are three reasons for this:
- Rust functions return the last expression at the end of their code block
- The
;
ends expressions. - Rust has a default return type that we'll discuss on the next page.
What all that means is if we did this:
3 + 6;
instead of:
3 + 6
we'd be end the expression that did
the addition. That expression is
what provides the i32
value that the
function is setup to return based off
its definition. So, we'd end up with an
error.
Run the code again with the ;
in place
to see what the error looks like and we'll
talk about it on the next page.
SOURCE CODE
fn main() {
let alfa = widget();
println!("alfa is {}", alfa);
}
fn widget() -> i32 {
3 + 6;
}
CODE RUNNER
Return Expression Errors
(TODO: Split this into multiple pages)
Here's the error message from the prior page where we intentionally put a semi-colon at the end of an expression that caused an error.
Compiling playground v0.0.1 (/playground)
error[E0308]: mismatched types
--> src/main.rs:6:16
|
6 | fn widget() -> i32 {
| ------ ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
7 | 3 + 6;
| - help: remove this semicolon to return this value
For more information about this error, try `rustc --explain E0308`.
error: could not compile `playground` due to previous error
The Rust error message are great but they can be pretty overwhelming at first. Let's to through it piece by piece to get a better idea of what happened.
*****
Compiling playground v0.0.1 (/playground)
This is a note from the rust compiler letting us know what it's working on. The site uses Rust Playground to compile and run code. This is the name it uses on its side.
*****
error[E0308]: mismatched types
--> src/main.rs:6:16
TKTKTK
*****
6 | fn widget() -> i32 {
| ------ ^^^ expected `i32`, found `()`
TKTKTK
*****
6 | fn widget() -> i32 {
| ------ ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
We've added two lines to the two in the prior description.
TKTKTK
*****
7 | 3 + 6;
| - help: remove this semicolon to return this value
TKTKTK
*****
For more information about this error, try `rustc --explain E0308`.
error: could not compile `playground` due to previous error
TKTKTK
Functions Review
This chapter covered:
- Functions with no parameters
- Functions with parameters
- Using parameters
- Functions with return values
- Binding return values
Here's a review the combines everything. It outputs
sum is 12
SOURCE CODE
fn main() {
let sum = do_addition(5, 7);
println!("sum is {}", sum);
}
fn do_addition(alfa: i32, bravo: i32) -> i32 {
let response = alfa + bravo;
response
}
CODE RUNNER
if Expressions
if
expressions are used to determine
if a section of code should be run or not.
They are created with:
- The
if
keyword - A condition to check
- A block of code to run if the condition is true.
The condition in the example below checks to see if the
number 3
is less than the number 4
using the <
math
symbol like this:
3 < 4
Since the 3
is less than 4
the condition is
true so we'll see the output the condition is true
.
SOURCE CODE
fn main() {
if 3 < 4 {
println!("the condition is true");
}
}
CODE RUNNER
false Conditions In if Expressions
The condition that was checked in the prior
example (i.e. 3 < 4
) was true. Because
of that, Rust ran the code inside the
code block and printed our "3 is less than 4"
output. The block of code won't run if we end
up with an if
condition that's false instead of
true.
In this example we'll do another check. This time
will do one that's false. We'll also add a couple
println!()
expressions to help see what's
going on. Specifically, we'll see the output
from the two println!()
expressions that are
outside the if
. But, we won't see the one
inside the if
. It'll looks like this:
alfa
charlie
SOURCE CODE
fn main() {
println!("alfa");
if 3 > 4 {
println!("bravo");
}
println!("charlie");
}
CODE RUNNER
else Expressions
The value of a condition in an if
expression
must be either true
or false
. If the
condition's value is true
the code block
gets executed. If the condition is false
it doesn't.
Executing an alternate code block when an if
condition is false
is done with an else
expression.
This example sets up an if
statements that
runs if 5 is less than 4. We'll follow it with
an else
expression that will get run since
evaluating 5 < 4
is false
The output will be:
5 is not less than 4
SOURCE CODE
fn main() {
if 5 < 4 {
println!("5 is less than 4");
} else {
println!("5 is not less than 4");
}
}
CODE RUNNER
else if Expressions
TODO: Make an earlier example showing using a variable for the condition so you don't have to explain it here.
Rust also provides an else if
expression. It goes
in between if
and else
expressions and gets
a condition to check like the initial if
statement
does.
SOURCE CODE
fn main() {
let value = 5;
if value < 3 {
println!("alfa");
} else if value < 7 {
println!("bravo");
} else {
println!("charlie");
}
}
CODE RUNNER
Multiple if else Expressions
SOURCE CODE
fn main() {
let value = 7;
if value <= 5 {
println!("alfa");
} else if value <= 6 {
println!("bravo");
} else if value <= 7 {
println!("charlie");
} else if value <= 8 {
println!("delta");
} else {
println!("echo");
}
}
CODE RUNNER
Function Example
SOURCE CODE
fn main() {
let value = 5;
if is_value_seven(value) {
println!("yes");
} else {
println!("no");
}
}
fn is_value_seven(check_value: i32) -> bool {
check_value == 7
}
CODE RUNNER
Binding Variables
NOTE: Since we're using the if
with let
it must have a ;
after the last part
of the expression.
SOURCE CODE
fn main() {
let alfa = true;
let bravo = if alfa {
0
} else {
1
};
println!("bravo is {}", bravo);
}
CODE RUNNER
Binding Values Must Be The Same
This will break via: https://doc.rust-lang.org/book/ch03-05-control-flow.html
because the values from the two arms of the if
aren't the
same type. This will throw an error that we'll look at
on the next page.
SOURCE CODE
fn main() {
let alfa = if 3 <= 4 {
0
} else {
false
};
println!("alfa is {}", alfa);
}
CODE RUNNER
Incompatible Types
TODO: Build out this page with the error from the prior page.
SOURCE CODE
Compiling playground v0.0.1 (/playground)
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:6:5
|
3 | let alfa = if 3 <= 4 {
| ______________-
4 | | 0
| | - expected because of this
5 | | } else {
6 | | false
| | ^^^^^ expected integer, found `bool`
7 | | };
| |___- `if` and `else` have incompatible types
For more information about this error, try `rustc --explain E0308`.
error: could not compile `playground` due to previous error
CODE RUNNER
While Loops
The condition check is built in.
SOURCE CODE
fn main() {
let mut alfa = 1;
while alfa <= 5 {
println!("alfa is {}", alfa);
alfa += 1;
}
}
CODE RUNNER
Assignments From While Loops
TODO: Figure out if you can do this or not.
SOURCE CODE
fn main() { let mut alfa = 1; let bravo = while alfa <= 5 { println!("alfa is {}", alfa); alfa += 1; alfa * 2 }; println!("bravo is {}", bravo); }
CODE RUNNER
For Loops
TODO: Switch to using a Vec instead of and array probably since Vecs are what will have been used elsewhere.
SOURCE CODE
fn main() {
let alfa = [5, 10, 15, 20];
for item in alfa {
println!("item is {}", item)
}
}
CODE RUNNER
Using Ranges In For Loops
SOURCE CODE
fn main() { for alfa in (1..10) { println!("alfa is {}", alfa) } }
CODE RUNNER
Error Messages In Tutorials
Getting hit with a typo error when you're trying to learn something else is a waste of time, energy, and motivation.
The Status line is there to prevent type errors, but we won't ignore error messages. Learning to read them is a critical part of learning a programming language.
Error messages can come from two categories of mistakes:
- Doing something wrong
- Typing something wrong
We'll look at both types, but we'll do it intentionally using Source Code blocks specificaly designed to demonstrate them.
The first thing is to point out that "wrong" in this context is not bad. It's more like when you walk up to a door and try to open it by pushing instead of pullling. You tried to do the wrong thing, but there's nothing bad about it. You do it another way until you find something that works.
We'll look at both types of error messages
Understanding error messages is a critical part of learning a language. But, they're best studied explicitly. During a tutorial, error messages from
Messing With The Code
The Status line typo warnings won't prevent you from running code. You can fiddle with the examples and run them to see what happens even though the warning will let you know you've drifted away from the example source code.
I've got a feature on the road map to disable the typo catcher. That'll make playing with the examples more friendly since it won't be throwing the warning at you when you change things.
Preamble
There are a few other things to cover before we get started with the code.
The Content
-
These are my actual, in-progress notes on learning Rust. As such, the site is very much a work in progress. It only goes far as I've made it in my studies.
-
The content is designed for folks who have at least some programming experience. If you know what variables, functions, loops, and conditionals are, you'll be fine.
The Examples
-
Examples are kept intentionally sort. That often means doing things that would be silly in a useful program. For example, defining a variable that's used only once on the following line. The goal is to show how to use something not when to use it.
-
Examples use the NATO phonetic alphabet (alfa, bravo, charlie, etc...). The words have no meaning beyond being labels or acting as content.
Typing It In
-
The approach of typing in examples may feel weird and frustrating at first if you've never done it. Give it 5-10 pages to see if you start to find it useful.
-
Of course, you can also read the site like a book without typing anything in if you learn better that way.
Now, let's get started with a better look at Hello, World.
The String Type
TODO: Figure out if you want to do string literals before this or not. I'm thinking not so that we only have to think about a single string type for the early part of the book. Push the literals to later.
Example showing how to add more text to a mutable String
SOURCE CODE
fn main() {
let mut alfa = String::from("Hello");
println!("alfa is {}", alfa);
alfa.push_str(", World");
println!("alfa is {}", alfa);
}
CODE RUNNER
Moving String
Trying to do this won't work (via https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html)
fn main() {
let alfa = String::from("widget");
let bravo = alfa;
println!("alfa {} - bravo {}", alfa, bravo);
}
We'll look at the error message on the next page, but for now, here's what will work.
TKTKTK
SOURCE CODE
fn main() {
let alfa = String::from("widget");
let bravo = alfa.clone();
println!("alfa {} - bravo {}", alfa, bravo);
}
CODE RUNNER
TODO: Write up how integers can be copied with:
let alfa = 7;
let bravo = alfa;
Functions Placeholder
This is a place holder until these function pages are sorted.
Passing Values In Functions
passing a variable into a function works the same way as assigning it to a new variable.
this throws an error:
fn main() { let alfa = String::from("apple"); widget(alfa); println!("alfa {}", alfa); } fn widget(incoming: String) { println!("Got {}", incoming) }
TKTKTKT - Talk about how integers can work
this is okay:
fn main() { let alfa = 7; widget(alfa); println!("alfa {}", alfa); } fn widget(incoming: i32) { println!("Got {}", incoming) }
You can pass a string and get one back if you want to do that.
But check out references next. Maybe don't get into passing back right now and just focus on references.
Function References
You can pass values by reference so the functions don't take ownership.
TKTKTK The act of using a reference is called borrowing.
SOURCE CODE
fn main() { let alfa = String::from("apple"); widget(&alfa); println!("alfa {}", alfa) } fn widget(incoming: &String) { println!("incoming: {}", incoming) }
CODE RUNNER
Function References Can't Be Modified By Default
TKTKTKT
This won't work by default. You have to do the mutalbe version which is on the next page.
SOURCE CODE
fn main() { let alfa = String::from("widget"); attempt_change(&alfa); } fn attempt_change(value: &String) { value.push_str("update"); }
TODO: Show the error message.
Mutable Function References
It is possible to change values in a function if you make the variable mutable and pass a mutable reference
SOURCE CODE
fn main() { let mut alfa = String::from("widget"); println!("alfa {}", alfa); update_value(&mut alfa); println!("alfa {}", alfa); } fn update_value(value: &mut String) { value.push_str(" updated"); }
CODE RUNNER
Only One Mutable Reference
You can only make one mutable reference to a variable.
This will fail:
SOURCE CODE
fn main() { let mut alfa = String::from("widget"); let bravo = &mut alfa; let charlie = &mut alfa; println!("bravo {}", bravo); println!("charlie {}", charlie); }
TODO: show the error message and explain it on the next page.
Scope For Mutalbe References
NOTE: this can probably be moved to a later part of the book.
This is the earlier example that doesn't work. If we move the println for bravo up it does work.
TODO: There is scope
stuff that comes in to play but there's
no explicit scope here so I'm guessing
rust sees that bravo
isn't going to
be used anymore after charlie is set
so it uses that for scope?
fn main() { let mut alfa = String::from("widget"); let bravo = &mut alfa; println!("bravo {}", bravo); let charlie = &mut alfa; println!("charlie {}", charlie); }
Look more and the example on this page.
https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html
Mutable And Immutable References Can't Be Combined
TODO: Make this a page:
You can have an immutable reference to a mutable variable.
SOURCE CODE
fn main() {
let mut alfa = String::from("widget");
let bravo = &alfa;
println!("bravo {}", bravo)
}
CODE RUNNER
Testing examples:
Mutable references to immutable variables throw an error. Show this error.
fn main() { let alfa = String::from("widget"); let bravo = &mut alfa; println!("bravo {}", bravo); }
TODO
Things to add to the functions and references.
From: https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html
You can make multiple immutable references to a mutable value.
fn main() { let mut alfa = String::from("widget"); let bravo = &alfa; let charlie = &alfa; println!("bravo {}", bravo); println!("charlie {}", charlie); }
You can't have a mutable reference to a mutable variable at the same time as you already have an immutable one.
fn main() { let mut alfa = String::from("widget"); let bravo = &alfa; let charlie = &alfa; let delta = &mut alfa; println!("alfa {}", alfa); println!("bravo {}", bravo); println!("charlie {}", charlie); println!("delta {}", delta); }
This was referred earlier I think, but this is confirmation that if rust sees that the values isn't going to be used anymore it releases the reference (TODO: Figure out the actual term for that) and you can use it:
fn main() { let mut alfa = String::from("widget"); println!("alfa {}", alfa); let bravo = &alfa; println!("bravo {}", bravo); let charlie = &alfa; println!("charlie {}", charlie); let delta = &mut alfa; println!("delta {}", delta); }
You can't make a new variable in a function and then return it. When the function is done, the original value disappears. Rust, prevents this type of thing from happening. If you try to run this code, you'll see the error (TKTKTKT review the error).
fn main() { let alfa = widget(); println!("alfa {}", alfa); } fn widget() -> &String { let bravo = String::from("hello"); &bravo }
This works though.
fn main() { let alfa = widget(); println!("alfa {}", alfa); } fn widget() -> String { let bravo = String::from("hello"); bravo }
Rules of references:
- At any given time, you can have either one mutable reference or any number of immutable references.
- References must always be valid.
This shows an early return in a function
fn main() { let alfa = limit_to_five(3); let bravo = limit_to_five(10); println!("alfa {} bravo {}", alfa, bravo); } fn limit_to_five(value: i32) -> i32 { if value > 5 { return 5 } value }
Slices TODO
TODO: I often get wires crossed. Mutability and References are not dependent on each other. Specifically, the idea of an immutable reference throws me. Potentially write something up about that.
from: https://doc.rust-lang.org/book/ch04-03-slices.html
It's labeled as The Slice Type
Baisc example:
fn main() { let alfa = String::from("the quick fox"); let bravo = &alfa[4..9]; println!("alfa {}", alfa); println!("bravo {}", bravo); }
If you're starting at zero, you don't need to put the zero. These are equivalent.
fn main() { let alfa = String::from("the quick fox"); let bravo = &alfa[0..3]; let charlie = &alfa[..3]; println!("bravo {}", bravo); println!("charlie {}", charlie); }
The same applies for the end.
fn main() { let alfa = String::from("the quick fox"); let bravo = &alfa[9..13]; let charlie = &alfa[9..]; println!("bravo {}", bravo); println!("charlie {}", charlie); }
There's also a note about array slices here: https://doc.rust-lang.org/book/ch04-03-slices.html
It looks like this:
fn main() { let alfa = [3, 5, 7, 9]; let bravo = &alfa[1..3]; assert_eq!(bravo, &[5, 7]); }
but maybe wait until vecs.
Also, we haven't seen assertions yet, and I don't totally get the refernce to the stand alone array. or, more to the point, why it's a reference.
This is from the "Other Slices" secition. Need to revisit once there's a better understanding of the parts.
Also, we haven't seen assertions yet, and I don't totally get the refernce to the stand alone array. or, more to the point, why it's a reference.
This is from the "Other Slices" secition. Need to revisit once there's a better understanding of the parts.
Look at: https://doc.rust-lang.org/book/ch04-03-slices.html
For the stuff about literals too and how you can pass them into functions wiht
#![allow(unused)] fn main() { fn widget(value: &str) -> &str {} }
That lets you pass in both &String
and
&str
values.
This is an example where you can pass literal references, and String references, and String slices (which are references by definition)
fn main() { let alfa = String::from("the quick fox"); let bravo = "the lazy dog"; widget(&alfa); widget(&alfa[4..9]); widget(&bravo); widget(&bravo[4..8]); } fn widget(value: &str) { println!("Value is {}", value); }
Question: does this mean that you should pretty
much always use &str
instead of &String
for
function parameters?
String literals
Look at the string literals part of the bottom of this page:
https://doc.rust-lang.org/book/ch04-03-slices.html
Tuple Structs
These are like regular structs but they don't have named fields. The values are positional with only the type defined.
struct Widget(i32, f32, bool); fn main() { let alfa = Widget(3, 7.0, true); println!("alfa.1 is {}", alfa.1); }
Tuple Structs are useful for setting a type and passing data to a function without the need for a full struct.
struct Widget (i32, f32, bool); fn main() { let alfa = Widget(3, 7.0, true); process_widget(alfa); } fn process_widget(widget_input: Widget) { println!("Value is {}", widget_input.1) }
Unit-Like Structs
NOTE: Move this to later after we've discussed the Unit type.
These structs can be used to setup a type that implements traits but holds no data. There will be a later example of that that should be combined with this.
(This will have no output as it is right now)
struct Widget; fn main() { let alfa = Widget; }
Debug Derived Trait
This is from lower on this page:
https://doc.rust-lang.org/book/ch05-02-example-structs.html
TODO: Make sure you've already talked about
the debug print format {:?}
and !dbg
and how !dbg
takes ownership and then returns
it and prints to stderr
You can't print a struct directly.
struct Widget { alfa: bool } fn main() { let thing = Widget { alfa: true }; println!("thing is {}", thing); }
TODO: Show that error
The {:?}
debug syntax won't work
either.
struct Widget { alfa: bool } fn main() { let thing = Widget { alfa: true }; println!("thing is {:?}", thing); }
TODO: Show the "error[E0277]: Widget
doesn't implement
Debug
" error.
If you add the outter attribute
for debugging with:
#[derive(Debug)]
it'll allow for {:?}
or {:#?}
to output a basic representation of the struct.
(The {:#?}
is the same thing as{:?}
with a little
nicer formatting and is that's shown here)
#[derive(Debug)] struct Widget { alfa: bool } fn main() { let thing = Widget { alfa: true }; println!("thing is {:#?}", thing); }
Using !dbg
Probably you should just show this for the
debuggin stuff instead of {:?}
in
println!()
for the default way to do
debugging. TODO: look into the differences
to see about that, but almost certianly
move the {:?}
to a later section.
NOTE: this isn't showing up on the playground
possibly because only stdout
and not stderr
is returned? Need to look into that.
#[derive(Debug)] struct Widget { alfa: bool } fn main() { let thing = Widget { alfa: true }; dbg!(thing); }
Use dbg! Around Expressions
You can use around expressions. This means
you can do things like this (TODO: figure
out if dbg!
can output on the rust playground.)
fn main() { let alfa = 3; let bravo = 5; let charlie = dbg!(alfa * bravo); println!("charlie is {}", charlie); }
match
The enum type lets us define a set of available options and ensure that it must contain one of them and that it can't contain more than one. (That is, it contains exactly one)
match
expressions provide a way to figure out
which one of those options the enum contains
and to do something based on that. For example,
here's an enum with two empty options called
Alfa
and Bravo
.
enum Widget {
Alfa,
Bravo
}
We can create a variable with an enum that's
using Bravo
like this:
let thing = Widget::Bravo;
We then use match
to check thing
match thing {
}
The check is done with arms
which
are all the different options
from the enum type. In our case,
that's Alfa
and Bravo
. Because
we're accessing them through the
variable, we have to include the
Widget
name of the enum as well
which gives us:
Widget::Alfa
And
Widget::Bravo
So, we use those two items as the
things to match on. For each one
we have a =>
that points to a code
block to run that's contained in
the {}
curly brackets.
enum Widget { Alfa, Bravo } fn main() { let thing = Widget::Bravo; match thing { Widget::Alfa => { println!("it's alfa"); } Widget::Bravo => { println!("it's bravo"); } } }
Step By Step
Passing Values
The options in enums can hold data too.
This is how to get at that.
enum Widget { Alfa(String), Bravo } fn main() { let token = String::from("apple"); let thing = Widget::Alfa(token); match thing { Widget::Alfa(value) => { println!("alfa with {value}"); } Widget::Bravo => { println!("bravo by itself"); } } }
match
Example from the book (with some more code so it actually does something)
enum Coin { Penny, Nickel, Dime, Quarter, } fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } } fn main() { let my_coin = Coin::Nickel; println!("I have {} cents", value_in_cents(my_coin)); }
Make a note that you can put {}
for the code blocks,
but it's usually not done if it fits on one line.
This is a basic way to use match to do something based off the type of enum that got created.
enum Widget { Alfa, Bravo } fn display_variant(value: Widget) { match value { Widget::Alfa => { println!("kind is Widget::Alfa"); }, Widget::Bravo => { println!("kind is Widget::Bravo"); } } } fn main() { let thing1 = Widget::Alfa; display_variant(thing1); let thing2 = Widget::Bravo; display_variant(thing2); }
Here's anothe example where you pull values
out and bind them so you can use them. This
is one way to get data out of an enum. (I
think if let
is another way. Not sure if there
are other ways.)
enum Widget { Alfa, Bravo(String), Charlie { value: i32 }, } fn show_report(item: Widget) { match item { Widget::Alfa => { println!("It's Alfa"); }, Widget::Bravo(text) => { println!("It's Bravo with {}", text); }, Widget::Charlie { value } => { println!("It's Charlie with {}", value); }, } } fn main() { let item1 = Widget::Alfa; show_report(item1); let item2 = Widget::Bravo(String::from("hello")); show_report(item2); let item3 = Widget::Charlie { value: 7 }; show_report(item3); }
Scope
Scope example that works, this we'll see it break on the next page.
SOURCE CODE
fn main() {
let alfa = 1;
{
let bravo = 2;
println!("bravo {}", bravo);
}
println!("alfa {}", alfa);
}
CODE RUNNER
Scope Error
Intentional error. Look at the message on the next page.
SOURCE CODE
fn main() {
let alfa = 1;
{
let bravo = 2;
println!("bravo {}", bravo);
}
println!("alfa {} - bravo {}", alfa, bravo);
}
CODE RUNNER
Scope Error Message
TKTKTK
Compiling playground v0.0.1 (/playground)
error[E0425]: cannot find value `bravo` in this scope
--> src/main.rs:10:40
|
10 | println!("alfa {} - bravo {}", alfa, bravo);
| ^^^^^ not found in this scope
For more information about this error, try `rustc --explain E0425`.
error: could not compile `playground` due to previous error
Guildlines
These are not hard rules. They are guidelines. Staying from them when it makes sense makes total sense.
-
Get to code that actually does something as fast as possible
-
Make the initial explinations as short as possible
-
Focus on how not why at the start.
-
Maybe: cycle through concepts (e.g varaibles with numbers, then if/then, then variables with booleans, then if/else., the variables with Strings, then if/else if/else)
-
You don't have to explain everything all at once. (e.g. you don't have to explain floats in order to talk about integers and you don't have to talk about the bits an integer can take up or the signed vs unsigned versions when you first start.)
-
Run off the idea that folks will go through the entire book. So, you don't need to explain everything all at once. (see also the other note about that)
-
Only show one way to do something (e.g. with ranges instead of showing
(1..10)
and then explaining that if you want to actually get to ten, just show(1..=10)
. -
Don't worry too much about explaing the syntax. That is: Show. Don't Tell.
-
Avoid talking about what you'll show later. Wait until you get there. (This is a continuation of show don't tell)
-
Avoid statements like "which we haven't talked about yet". If the person thinks about it at all, they'll know that's the case so there's littl point in mentioning it.
-
Don't dig into every possible option at the start with edge cases. e.g. "In Rust, the
main()
function is used to kick things off" works find without having to. "In most programs, except for these types when its... whatever") It's like taking an 80/20 thing where the statement needs to be accurate but not all inclusive. Details of the different cases can be dealt with later. -
You can also use lanage like "basic variables are defined like" instead of "variables are defined like".
Work In Progress
This site is a work in progress. Everything prior to this page is in pretty good shape for a first iteration.
The pages that follow contain drafts, and scratch notes and aren't in order. They'll shift around and solidify. You're welcome to check them out, but please keep that in mind.
TODO List
Stream
-
[] Read a file to string
-
[] Look at
nom
for parsing -
[] Add button to disable live typo detection.
-
Add two buttons into the Code Runner that let you click them to decide to Type or to decide to Copy and Run the code directly.
-
Move the run button below the Code Runners
-
Add spacing to the code lines so you don't have to put spacer lines between all of them.
-
Add a button to copy the text automatically, maybe you could make that the thing where when you click in you click inside for copy the text or type the text. (because you have to click in anyway unless you tab in so you should handle tabbing in too)
-
Add styles to
txt
code fences. -
Turn off whatever is overriding the default scroll bars.
-
See if you can turn off whatever is making the TOC sidebar nav jump to center the items so that it stays with where you had it. (Probably requrie setting a cookie and reading it?)
Other
-
[] Show how to read this type of thing from the docs
pub fn read_dir<P: AsRef<Path>>(path: P) -> Result<ReadDir>
-
[] Show a
match
expression that returns a value assuming that's possible. -
Verify the font styling in
mutable-variables/index.html
works cross browser -
Add section about how to read the docs
-
Verify that the generic term statement can be used to describe expressions (e.g. "an
if
statement is an expression") -
Is it possible to make the book dynamic in some way, even if it's just manually. Like a setting where you can select if you've got any programming experience or not and if not you get extra text and maybe some variations in the main text.
-
What if there was a spaced repetition thing built into the site with programs to run that helped work on the parts you weren't as sure about.
-
Put in fun things to do and see for the different parts that you go through as options?
-
What if there were different versions of the book with different exmample variable names (e.g. if you want to have animals, or colors, or nato alphabet) Or, maybe not different versions, but like settings so you could control things.
-
Maybe don't use anything that's stored on the stack for the first part of the book? Introduce String and Vec instead of string literals and arrays. That way you don't have to talk about what has to be borrowed and what doesn't require it.
-
Add note about how things can move to differnt lines and still be the same expression or statement.
-
Add something so if you hit the run button when there's nothing in the code running it asks you if you want to type stuff in or copy it directly?
-
Add a button or link to copy the source code into the code runner directly. (do that in addtion to the run check to make sure there's code in the code editor)
-
Make an early example of doing
2+2
in the variables chapter. -
Figure out where to put Statements and Expressions.
-
Scope
-
Comments
-
Rust does not try to convert non-boolean types to booleans
-
String literals
-
Come up with some example programs to use as full examples at the end of each section.
-
Talk about the Rust Prelude: https://doc.rust-lang.org/std/prelude/index.html
-
Talkd about the Rust editions
-
Talk about the
&str
slice type. -
Talk about derived traits.
-
Talk about how structs and enums are keys to stuff.
Put in a note about if you get this error it probably means you didn't put code in the code runner:
#![allow(unused)] fn main() { Compiling playground v0.0.1 (/playground) error[E0601]: `main` function not found in crate `playground` | = note: consider adding a `main` function to `src/main.rs` For more information about this error, try `rustc --explain E0601`. error: could not compile `playground` due to previous error }
Comman error:
Compiling playground v0.0.1 (/playground)
error[E0615]: attempted to take value of method do_output
on type Widget
--> src/main.rs:15:9
|
15 | thing.do_output;
| ^^^^^^^^^ method, not a field
|
help: use parentheses to call the method
|
15 | thing.do_output();
| ++
For more information about this error, try rustc --explain E0615
.
error: could not compile playground
due to previous error
Using ()
instead of {}
Compiling playground v0.0.1 (/playground)
error: expected identifier, found keyword true
--> src/main.rs:16:28
|
16 | let thing = Widget(alfa: true);
| ^^^^ expected identifier, found keyword
|
help: escape true
to use it as an identifier
|
16 | let thing = Widget(alfa: r#true);
| ++
error: invalid struct
delimiters or fn
call arguments
--> src/main.rs:16:15
|
16 | let thing = Widget(alfa: true);
| ^^^^^^^^^^^^^^^^^^
|
help: if Widget
is a struct, use braces as delimiters
|
16 | let thing = Widget { alfa: true };
| ~ ~
help: if Widget
is a function, use the arguments directly
|
16 - let thing = Widget(alfa: true);
16 + let thing = Widget(true);
|
error[E0070]: invalid left-hand side of assignment --> src/main.rs:8:16 | 8 | &self.alfa = false | ---------- ^ | | | cannot assign to this expression
For more information about this error, try rustc --explain E0070
.
error: could not compile playground
due to 3 previous errors
Statements and Expressions
From: https://doc.rust-lang.org/book/ch03-03-how-functions-work.html
Statements are instructions that perform some action and do not return a value. Expressions evaluate to a resultant value. Let’s look at some examples.
Constants
There's another type of variable besides mutable and immutable. They're called "constants".
Constants are like immutable variables with a few extra criteria. We've covered enough to get the idea for the first three:
- They are defined using the
const
keyword instead oflet
- The
mut
keyword can't be used when creating them - Names should always be in UPPER_SNAKE_CASE by convention.
These next three criteria require some knowledge we haven't gotten to yet, We'll touch on them in the next few pages.
- Constants can be declared in global scope (which will see on the next page)
- They must have a type annotation (e.g. the
i32
in the example below which is coming up). - They must be set to something determined at compile time (which we'll also get to shortly)
fn main() {
const ALFA: i32 = 100;
println!("Alfa {ALFA}");
}
CODE RUNNER
TODO
- Examine moving constants to a points after you've talked about scope and types. Probably that makes the most sense.
Constants In The Global Scope
Here's the previous example we used to demonstrate a constant.
fn main() {
const ALFA: i32 = 100;
println!("Alfa {ALFA}");
}
The const
and println!()
statements are
between the {
and }
of the main()
function.
The terminology used for this is to say
those statements are in the main()
function's "scope".
Scope is like a one-way wrapper where:
- Anything that's in a surround scope can be be accessed by what's inside the inner scope, but
- Anything inside the scope can't be accessed by anything outside of it.
The main()
function's scope is clearly delimited by
the {
and }
. There's another scope surrounding it
in our program that's invisible. It's what's called the
"global" scope. It begins at the start of the source code
and goes all the way to the end encompassing everything
in between.
What this means is that we can move the assignment for our
ALFA
constant above the definition of the main()
function like this:
SOURCE CODE
const ALFA: i32 = 100;
fn main() {
println!("Alfa {ALFA}");
}
CODE RUNNER
Types Required
TODO: Fill out this page with notes on types being required.
TBD on the content based of if this gets moved to after types have been covered or if it stays before them.
Compile Time
TODO: Fill out this page with notes on how stuff need to be defined in a way that it can be determined at compile time.
Shadowing Variables
Immutable Variables Can't Be Changed
Trying to update the value of an immutable
variable results in an error. This code sets
an initial immutable value with let alfa = 3
and then tries to update it with alfa = 5
which
causes an error.
Click Run to see an example of what that looks like.
#[allow(unused_variables)] #[allow(unused_assignments)] fn main() { let alfa = 3; alfa = 5; }
Shadowing Variables Works
While you can't update an immutable variable, you can define a new one with the same name. This is called shadowing.
Enter and run this code for an example
fn main() {
let alfa = 3;
println!("The value is {alfa}");
let alfa = 5;
println!("The value is {alfa}");
}
CODE EDITOR
TODO
- Come up with examples of when you might want to shadow a variable (changing type might be one, but I don't know yet why you would do that instead of making a new variable)
Shadowing Variables In Scope
Shadowing a variable only applies to the scope that the shadowing happens in. If you bind a variable in one scope, then shadow it in a child scope the new version of the variable will be used in that scope. For example:
fn main() {
let alfa = 3;
println!("The value is {alfa}");
{
let alfa = 5;
println!("The value is {alfa}");
}
}
The value is 3
The value is 5
If you then exist the child scope and use the variable name in the original scope it returns to the original version.
fn main() {
let alfa = 3;
println!("The value is {alfa}");
{
let alfa = 5;
println!("The value is {alfa}");
}
println!("The value is {alfa}");
}
Enter this program and run it to see the output.
CODE EDITOR
Shadowing Variables To Change Type
Shadowing variables allows you to
change their type. That's something you can't
do with a regular mutable variable (i.e. one
defined with let mut
). For example, this code
tries to change from a string to a number.
Run it and you'll see the error message.
fn main() { let mut alfa = "example"; alfa = 7; }
Using shadowed variables to change the type in a way that works looks like this:
fn main() {
let alfa = "example";
let alfa = 7;
println!("The value is {alfa}");
}
CODE EDITOR
Characters
NOTE: This was originally right after booleans in
the early Data Types chapter. I'm moving it
out for now because I rarely see char
used
independently. We'll address in a later section.
The char
type in Rust holds a single character. Variables
of the char
type are defined with single quotes. They can
be set implicitly like this:
let alfa = 'a';
Or explicitly like this:
let bravo: char = 'b';
Here's the explicit version in a full program:
SOURCE CODE
fn main () {
let charlie: char = 'c';
println!("Value {charlie}");
}
CODE RUNNER
Destructuring Tuples
https://doc.rust-lang.org/book/ch03-02-data-types.html#the-tuple-type
Adding Values To Tuples
Arrays
NOTE: Arrays are covered in chapter 3 of The Rust Book. I'm putting them later based off the sentence "If you're unsure whether to use an array or a vector, chances are you should use a vector.
Arrays are like tuples. They're containers that hold a collection of values. Arrays are different from tuples in two ways:
-
Every value the contain must be of the same type.
-
The number of items in an array is set when the array is created and cannot be changed.
Arrays are created with square brackets. The format the defines them is the type followed by a semicolon then the number of items in the array. The format looks like this for an array of three integers.
let alfa: [i32, 3] = [3, 5, 7];
Accessing the elements of an array is done using the name of the array followed by the desired values index number surrounded by square brackets.
println!("Value {}", alfa[0]);
Putting those parts together we get
SOURCE CODE
fn main() {
let alfa: [i32; 3] = [3, 5, 7];
println!("1st {}", alfa[0]);
println!("2nd {}", alfa[1]);
println!("3rd {}", alfa[2]);
}
CODE RUNNER
Statements And Expressions
The widget
function in the last example
looked like this:
fn widget() -> i32 {
5 + 5
}
I bring this up to point out that there is
no ;
after the 5 + 5
. This is different
than all the other lines we've seen
in functions so far. For example:
println!("Hello, World");
The reason for this difference is because Rust functions are made up of two types of things: Statements and Expressions.
From The Rust Book:
- Statements are instructions that perform some action and do not return a value.
- Expressions evaluate to a resultant value.
Said another way, expressions give you something back. Statements don't.
Option Type
#![allow(unused)] fn main() { }
Options are returned by lots of things to let you know if there is a valid value or not.
More stuff on this page:
https://doc.rust-lang.org/book/ch06-02-match.html
Prerequisets
This site assumes you know the following:
- What immutable means.
- What it means to assign a variable.
- What keywords in a language are
TODO
- Provide links for how to learn all the above.
About The Site
DRAFT
I started learning Rust with The Rust Book. It was too much for me. There's too much explanation and not enough code for the way I learn.
Short examples work better for me. Just a few lines of code with no comments intertwined.
Documenting what I'm learning is another big part of how I figure things out. This site combines both things. It's a collection of refined examples based off The Rust Book and Rust By Example with my write ups.
I'm publishing it to help other folks who learn the way I do.
NOTES
- This is a work in progress. It's not even a full first draft yet.
- I'm filling out that pages, but they aren't in order yet.
- Most examples have a CODE EDITOR under them where you can type the code and run it. That's been very helpful for me.
- I've suppressed some warnings that would require making examples more complicated. None of them effect functionality and we'll cover them specifically in their own chapter.
Terminology
Terms to define
- argument
- assign
- bind
- compiled
- execute
- function
- keyword
- main()
- parameter
- run
- statement
Typo Test
fn main() {
println!("alfa");
}
Work In Progress
This site is a build-in-public project. It's still a very early work in progress.
I'm working to make sure everything is accurate, but there's lots of stuff that's out of order and more stuff missing.
All that's to say, it's not ready to go yet, but feel free to poke around.
Other notes to drop in.
-
My source materials are The Rust Programming Language (aka The Rust Book), Rust By Example, Rustlings, and Rust Adventure. I'm pulling what I learn from all those places and making the examples you see here
-
Reading this site like a book without typing is perfectly acceptable. But, I think you'll get the most out of it by actually typing in the examples (and making all the typos that includes)
-
You don't need to have Rust installed to use the site. All the examples are compiled and run directly in the browser via The Rust Playground
Turning Off Warnings
This code produce the two warnings and one error on the previous page.
fn main() {
let alfa = 7;
alfa = 9;
}
All we care about right now is the error message. Rust provides as way to suppress warnings (TODO: find the name of these things, "directives", maybe?)
There are several directives available. (TODO:
link to the other directives). The one we're
interested in right now is #[allow(unused)]
(TODO: Update based on the one with the !
as well and talk about the difference i.e.
#![allow(unused)]
It's applied like this. Give it a shot to see the error message.
SOURCE
#[allow(unused)]
fn main() {
let alfa = 7;
alfa = 9;
}
CODE RUNNER
The Error Message
Here's the error message
Compiling playground v0.0.1 (/playground)
error[E0384]: cannot assign twice to immutable variable `alfa`
--> src/main.rs:4:3
|
3 | let alfa = 7;
| ----
| |
| first assignment to `alfa`
| help: consider making this binding mutable: `mut alfa`
4 | alfa = 9;
| ^^^^^^^^ cannot assign twice to immutable variable
For more information about this error, try `rustc --explain E0384`.
error: could not compile `playground` due to previous error
For now, the two lines we need to look at are:
error[E0384]: cannot assign twice to immutable variable `alfa`
and
| help: consider making this binding mutable: `mut alfa`
Error Messages
TODO: Make a collection of error message showing some of the ways things can go wrong.
Examples to use:
- Not sending a value to a function
- Not putting a
;
after a line in a function that needs it.
See if there's a list of the most common errors folks hit.
Variables - Origian with types
This is the first version of the Variables page that included type defintions. Moving to a version that doesn't have that for now to see how it works
Keeping this here for reference.
Variables in Rust are created using the following structure:
- The
let
keyword - A name for the variable (e.g.
alfa
) - A
:
that acts as a separator - A data type
- The
=
sign - The value to bind to it (e.g.
7
) - A
;
the ends the definition
The data type from item 4 tells rust what
kind of content the variable can hold. For example,
it might be a number, or a letter, or full
sentence. We'll dig into data types in the
next chapter. We'll use i32
(which
stands for a number) until we get there.
Putting it all together we get this:
let alfa: i32 = 7;
Using that line in we can create
a full program that defines the alfa
variable then prints it out.
SOURCE CODE
fn main() {
let alfa: i32 = 7;
println!("The value is {}", alfa);
}
CODE RUNNER
if let
TODO: Do this after match
SOURCE CODE
fn main() {
}
CODE RUNNER
Loops
Moving this back in the stack since while and for loops are much more common
SOURCE CODE
fn main() {
let mut alfa = 1;
loop {
println!("alfa is {}", alfa);
if alfa == 10 {
break;
}
alfa += 1;
}
}
CODE RUNNER
Binding Values
SOURCE CODE
fn main() {
let mut alfa = 1;
let bravo = loop {
println!("alfa is {}", alfa);
if alfa == 10 {
break alfa;
}
alfa += 1;
};
println!("bravo is {}", bravo);
}
CODE RUNNER
Labels
You can add labels like this (not useful yet, but we'll see where they become helpful on the next page)
SOURCE CODE
fn main() {
let mut alfa = 1;
'outter_loop: loop {
println!("alfa is {}", alfa);
if alfa >= 10 {
break 'outter_loop;
}
alfa += 1;
}
}
CODE RUNNER
Nested Loops
Nested loops (one inside of another one) are where labels become useful.
SOURCE CODE
fn main() {
let mut alfa = 1;
'outter_loop: loop {
let mut bravo = 1;
'inner_loop: loop {
if bravo == 4 {
break 'inner_loop;
}
if alfa == 5 {
break 'outter_loop;
}
println!("alfa {} bravo {}", alfa, bravo);
bravo += 1;
}
alfa += 1;
}
}
CODE RUNNER
Enum Type Aliases
NOTE: This is the first type aliases things that showed up in Rust by example. Not sure if it should be moved to a generic type aliases page or not yet.
Aliase can be used if you've got a long name and want to make it easier to use.
NOTE: This doesn't produce any output
enum ThisHasLotsOfLettersAndIsTooLong { Alfa, Bravo, } type Shorter = ThisHasLotsOfLettersAndIsTooLong; fn main() { let thing = Shorter::Alfa; match thing { Shorter::Alfa => println!("Got Alfa"), Shorter::Bravo => println!("Got Bravo"), } }
Shorter names can be done in impl
blocks.
This happens automatically with Self
(which was mentioned before. TBD on if you
need this here too, but probably works to
make it explict if you reference the earlier
stuff with structs or whatever it was)
(Of course, this is only showing the change
inside the impl
. Guess you'd want to do
type
as well for the other one.)
enum ThisHasLotsOfLettersAndIsTooLong { Add, Subtract, } impl ThisHasLotsOfLettersAndIsTooLong { fn run(&self, x: i32, y: i32) -> i32 { match self { Self::Add => x + y, Self::Subtract => x - y, } } } fn main() { let alfa = ThisHasLotsOfLettersAndIsTooLong::Add; println!("Addition {}", alfa.run(3, 4)); let bravo = ThisHasLotsOfLettersAndIsTooLong::Subtract; println!("Subtraction {}", bravo.run(4, 3)); }
Fizz Buzz
Not sure I'm goint to put Fizz Buzz in here or not, but one idea might be to do different versions of it showing different approaches.
SOURCE CODE
fn main() { for count in 1..=20 { if count % 3 == 0 { if count % 5 == 0 { println!("FizzBuzz"); } else { println!("Fizz"); } } else if count % 5 == 0 { println!("Buzz"); } else { println!("{}", count); } } }
CODE RUNNER
Hello, World - The Parts
Let's take a look at the individual parts that make up our "Hello, World" program.
fn main() {
println!("Hello, World");
}
-
fn
is short for function which is what we're defining. -
main
is the name we're assigning to the function. In Rust, themain
function is used to kick things off.Every rust program must have a
main
function. It's the entry point that kicks things off. -
The
()
at the end ofmain()
is where we define arguments that can be passed to the function. They're empty here which meansmain()
doesn't accept any. -
The opening
{
and}
curly bracket set the starting and ending points for the function's code block. -
println!("Hello, World");
is what prints out the text.
We'll dig into more details about each of those elements throughout the site.
A Complete Program
Let's look at the "Hello, World" code again:
fn main() {
println!("Hello, World");
}
Those three lines of code are a complete program. It may be simple, but when you've typed them in you've officially written Rust.