Johdanto

Tietoa kurssista

Tämän kurssin ovat kehittäneet Niklas Halonen ja Matias Zwinger. Kehityksessä ovat auttaneet myös Roope Salmi, Jens Pitkänen ja Tuukka Moilanen.

Kurssin tarkoitus on ensisijaisesti opettaa ohjelmoinnin syvällisiä konsepteja Rust-ohjelmointikielen avulla. Ohjelmointikielet ovat vain työkaluja ongelmien ratkaisuun eikä Rust todellakaan ole aina paras työkalu. Ohjelmistokehittäjän on tärkeämpää ymmärtää miten yleisiä ohjelmoinnin konsepteja voi käyttää hyödyksi ohjelmointikielestä riippumatta.

Tietoa kurssin alustasta

Kurssimateriaali on kirjoitettu mdBook-alustalla, jonka lisäksi diaesityksiä ajetaan revealjs paketilla. Kurssimateriaalin lähdekoodi on avointa ja löytyy GitLabista. Kurssin integroidun kehitysympäristön lähdekoodi löytyy myös GitLabista.

Oppimistavoitteet

Kurssin tavoitteena on oppia seuraavat asiat:

  • Ymmärtää ohjelmoinnin peruskonsepteja syvällisemmin:
    • pattern matching
    • muuttuvuus
    • tietorakenteet, kuten Vec ja HashMap
    • muistinhallinta
    • osoittimet
    • staattinen ja dynaaminen tyypitys
  • Osata lukemaan ohjelmointiaineistoa, kuten rustdoc-dokumentaatiota ja Rust-lähdekoodia
  • Osata itsenäisesti etsimään tietoa ongelman ratkaisemiseksi
  • Osata käyttää kehitysympäristöä hyödyksi ohjelman muokkaamisessa ja debuggauksessa
  • Ymmärtää sekä manuaalisen, että automaattisen testaamisen käsitteet ja merkityksen ohjelmistokehityksessä
  • Ymmärtää siistin lähdekoodin merkityksen
  • Osata käyttää ulkoisia sekä itsetekemiä kirjastoja ohjelmissa
  • Tietää ohjelmoinnin terminologiaa suomeksi sekä englanniksi:
    • konekieli (machine code)
    • kääntäjä (compiler)
    • pino (stack)
    • keko (heap)
  • Osata funktionaalisen ohjelmoinnin paradigman mukaisia konsepteja:
    • Korkeamman asteen funktio
    • Muuttumattomuus (immutability)
    • Geneerisyys
    • Algebrallinen tietotyyppi
  • Osata imperatiivisen ohjelmoinnin paradigman mukaisia konsepteja:
    • Käskyt
    • Suorituksen ohjaus ehtolauseilla
    • Tiedon muuttamista paikallaan
  • Ymmärtää Rustin tyyppijärjestelmän ero muista ohjelmointikielistä:
    • Periytyminen (inheritance)
    • Piirteet (traits)
    • Nollakustannusabstraktiot ja -tyypit (zero-cost abstractions and types)
  • Ymmärtää Rustin omistajuusparadigman hyödyt ja haitat
  • Osata kollaboroivasti kehittää toimivaa Rust-lähdekoodia esimerkiksi Gitin avulla

Pidetyt kurssit

  • Otaniemen lukiolla 2022 keväällä (versio 0.5.0)

Tämä dokumentti on tarkoitettu ohjeeksi opiskelijoille, jotka eivät ehkä päässeet tunnille. Asiat käydään tunnilla läpi

Viikko 1

Kehitysympäristö

Kurssilla on käytössä VSCode-pohjainen verkkokehitysalusta. Lue lisää

Rustin asennus

Rustin asentaminen tapahtuu rustup-ohjelmalla, joka asentaa Rust-kääntäjän lisäksi cargon. Asenna rustup.rs.

Halutessasi voit lukea kirjan kappaleen Rustin asentamisesta.

Rustin kirja

The Rust Programming Language, tunnetaan myös nimellä The Book, on Steve Klabnikin ja Carol Nicholsin kirjoittama Rustin virallinen opetusmateriaali.

Kirja on Rust-koodarille, kuin MAOL datalle. Kirja sisältää oppia kaikista Rustin tärkeimmistä ominaisuuksista ja paljon yleistä tietoa ohjelmoinnista yleensä. Kirja on tämän kurssin tärkein lähde, ja sitä luetaan joka viikolla.

Rust-kurssi

#![allow(unused)]
fn main() {
fn start_course() {
    println!("Course starting countdown:");
    for i in (1..=3).rev() {
      println!("{}", i);
    }
}

start_course();
}

Kysymyksiä

  • Mitä ohjelmointikieliä osaat?
  • Miksi haluat oppia Rustia?
  • Oletko käyttänyt Rustia ennen?

Käytännöt

  • Tunnit ma ja ke 15-16
    • ma pidetään luennot
    • ke tehdään tehtäviä
  • Tehtäviä tehdään sekä tunnilla että itsenäisesti
  • Ei kokeita, ei arvosanaa
  • Kurssiprojekti

~Rust kerho~

  • Peruttu

Kurssimateriaali

otafablab.gitlab.io/rust-lukiokurssi


Rustin asennus

Tehtävät

Tehtävistä yleensä kurssilla

Kurssilla tehdään tehtäviä tunneilla, mutta niitä varmasti riittää myös tehtäväksi itsenäisesti. Tehtäviä ei ole jaettu tuntitehtäviin ja kotitehtäviin vaan jako riippuu mitä tunnilla saa aikaan.

guessing_game

https://doc.rust-lang.org/book/ch02-00-guessing-game-tutorial.html

Kirjan toisessa kappaleessa päästään vauhtiin kirjoittamalla komentorivipeli.

guessing_gamea ei tarvitse palauttaa.

Yhteenveto

Ensimmäisellä viikolla tavoitteena oli tutustua Rustiin guessing gamen avulla.

Viikkopalaute

Joka viikolla voi ja kannattaa täyttää palautelomake, jonka avulla opettajat voi päätellä mitä pitäisi opettaa enemmän

Anna palautetta miten sinulla meni tämän viikon aiheet

Viikko 2

Toisen viikon tehtävät tehdään otarustlings-tehtäväalustalla.

otarustlings käyttöohje löytyy sen docs.rs sivulta

Tavoitteet

  • ymmärtää muuttujien luomisen
  • tietää eri primitiivityyppejä
  • tietää syntaksin yleisesti
  • tietää mistä etsiä tietoa
  • on haju omistajuudesta

Muuttujat ja tyypit


  • Kurssimateriaali

doc.fablab.rip

  • Liittykää Telegram:

lyli.fi/rust


let

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {}", guess);
}

use std::io;

fn main() {
    // ...

    let mut guess = String::new();

    io::stdin();

    // ...
}
from std import io

def main():
    # ...

    guess = String()

    io.stdin()

    # ...

use

// use std::io;

fn main() {
    // ...

    let mut guess = String::new();

    std::io::stdin();

    // ...
}

mut

#![allow(unused)]
fn main() {
let guess = String::new();

std::io::stdin()
    .read_line(&mut guess)
    .expect("Failed to read line");
}

#![allow(unused)]
fn main() {
error[E0596]: cannot borrow `guess` as mutable, as it is not declared as mutable
 --> a.rs:5:20
  |
2 |     let guess = String::new();
  |         ----- help: consider changing this to be mutable: `mut guess`
...
5 |         .read_line(&mut guess)
  |                    ^^^^^^^^^^ cannot borrow as mutable

error: aborting due to previous error
}

fn

#![allow(unused)]
fn main() {
fn hello(name: &str) {
    println!("Hello, {}", name);
}
}

#![allow(unused)]
fn main() {
fn bad_max(x: i32, y: i32) -> i32 {
    let maximum;
    if x > y {
        maximum = x;
    } else {
        maximum = y;
    }
    return maximum;
}

fn better_max(x: i32, y: i32) -> i32 {
    let maximum = if x > y {
        x
    } else {
        y
    };
    maximum
}
}

if-lauseke

#![allow(unused)]
fn main() {
fn best_max(x: i32, y: i32) -> i32 {
    if x > y { x } else { y }
}
}

Scope

#![allow(unused)]
fn main() {
let x = 5;
let y = 3;

if x > y {
    let diff = x - y;
}
println!("{}", diff);
}

Scope

#![allow(unused)]
fn main() {
error[E0425]: cannot find value `diff` in this scope
 --> /tmp/mdbook-zzWDbB/week2/slides.md:164:16
  |
9 | println!("{}", diff);
  |                ^^^^ not found in this scope

error: aborting due to previous error
}

Scope ratkaisu?

#![allow(unused)]
fn main() {
let x = 5;
let y = 3;

let mut diff;
if x > y {
    diff = x - y;
}
println!("{}", diff);
}

Scope ratkaisu?

#![allow(unused)]
fn main() {
error[E0381]: borrow of possibly-uninitialized variable: `diff`
  --> /tmp/mdbook-HR2wgB/week2/slides.md:198:16
   |
10 | println!("{}", diff);
   |                ^^^^ use of possibly-uninitialized `diff`
   |

error: aborting due to previous error
}

let x = 5;
{               // näkyvissä: x
    let y = 3;
    {           // näkyvissä: x, y
        if x > y {
            let diff = x - y;
            {   // näkyvissä: x, y, diff
                println!("{}", diff);
            }
        }       // diff pudotetaa
    }
}               // y pudotetaan

Omistajuus

#![allow(unused)]
fn main() {
let x = String::from("hello");
let y = x;
println!("{}", x);
}

#![allow(unused)]
fn main() {
error[E0382]: borrow of moved value: `x`
 --> /tmp/mdbook-7pr3zz/week2/slides.md:208:16
  |
3 | let x = String::from("hello");
  |     - move occurs because `x` has type `String`,
  |       which does not implement the `Copy` trait
4 | let y = x;
  |         - value moved here
5 | println!("{}", x);
  |                ^ value borrowed here after move

error: aborting due to previous error
}

Lainaus

#![allow(unused)]
fn main() {
let mut s = String::from("hello");
// push_str lainaa `s` mutablena
s.push_str(", world!");
println!("{}", s);
}

Rust pähkinänkuoressa

Rust in a nutshell


Otarustlings

cargo install otarustlings # might take a while
cd Desktop
otarustlings init

bongo

Tehtävät

Tehtävät tehdään otarustligs ohjelmaa käyttäen.

Toisen viikon tehtävät ovat kansiossa week2.

Lue otarustlings ohje

Tätä varten tarvitse Rustin työkalut

Yhteenveto

Toisella viikolla käytiin Rustin syntaksia ja pääpiirteitä läpi.

Uusia asioita olivat:

  • tyypit
  • skooppi
  • funktioiden syntaksi
  • omistajuus ja lainaaminen
  • Rustin erilaisuus muihin kieliin nähden

Vanhoja asioita joita kerrattiin olivat:

  • muuttujat
  • cargo new, cargo run
  • komentorivin käyttö

Harjoitustunti

  • viikko 1 palaute
  • jotain lisäihmettelyä skoopista
  • ajonaika vs kääntöaika
  • paniikki
  • ajonaikana ei voi tulla tyyppivirhettä
  • stringin metodit, &mut String
  • literaalit, string literal vs Strinh
  • static tuple godbolt

Jotain lisäihmettelyä skoopista

#![allow(unused)]
fn main() {
let a = 1;
let a = 2;
{
    let mut a = 3;
    dbg!(a);
    for _ in 0..4 {
        a += 1;
        dbg!(a);
        let a = "kana";
        dbg!(a);
    }
    dbg!(a);
}
dbg!(a);
}

Ajonaika vs kääntöaika

#![allow(unused)]
fn main() {
const A: i32 = 5;
}
const fn add(a: i32, b: i32) -> i32 {
    a + b // + is const fn
}

const A: i32 = add(5, 5);

fn main() {
    dbg!(A);
}

Ajonaika vs kääntöaika

#![allow(unused)]
fn main() {
const FACTS: String = String::from("Rust > Python");
}

Ajonaika vs kääntöaika

#![allow(unused)]
fn main() {
const FACTS: &'static str = "Rust > Python";
}

Viikkopalaute

Joka viikolla voi ja kannattaa täyttää palautelomake, jonka avulla opettajat voi päätellä mitä pitäisi opettaa enemmän

Anna palautetta miten sinulla meni tämän viikon aiheet

Esimerkkiratkaisut

01-variables

fn main() {
    let mut x = 3;
    println!("Number {}", x);
    x = 5; // don't change this line
    println!("Number {}", x);
}

02-variables

fn main() {
    let number = "T-H-R-E-E"; // don't change this line
    println!("Spell a Number : {}", number);
    let number = 3;
    println!("Number plus two is : {}", number + 2);
}

03-variables

fn main() {
    let x: i32 = 5;
    println!("Number {}", x); // Prints: "Number 5"
}

04-variables

const NUMBER: i32 = 3;
fn main() {
    println!("Number {}", NUMBER);
}

05-functions

fn main() {
    call_me(3);
}

fn call_me(num: i32) {
    for i in 0..num {
        println!("Ring! Call number {}", i + 1);
    }
}

06-if

#![allow(unused)]
fn main() {
pub fn bigger(a: i32, b: i32) -> i32 {
    if a > b { a } else { b }
}

// Don't mind this for now
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ten_is_bigger_than_eight() {
        assert_eq!(10, bigger(10, 8));
    }

    #[test]
    fn fortytwo_is_bigger_than_thirtytwo() {
        assert_eq!(42, bigger(32, 42));
    }
}
}

07-if

#![allow(unused)]
fn main() {
// Step 1: Fix the type mismatch and tests `foo_for_fizz` and `default_to_baz`
// Step 2: Get the bar_for_fuzz!

pub fn fizz_if_foo(fizzish: &str) -> &str {
    if fizzish == "fizz" {
        "foo"
    } else if fizzish == "fuzz" {
        "bar"
    } else {
        "baz"
    }
}

// No test changes needed!
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn foo_for_fizz() {
        assert_eq!(fizz_if_foo("fizz"), "foo")
    }

    #[test]
    fn default_to_baz() {
        assert_eq!(fizz_if_foo("literally anything"), "baz")
    }

    #[test]
    fn bar_for_fuzz() {
        assert_eq!(fizz_if_foo("fuzz"), "bar")
    }
}
}

08-ownership

fn main() {
    let vec0 = Vec::new();

    let mut vec1 = fill_vec(vec0);

    println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);

    vec1.push(88);

    println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
}

fn fill_vec(vec: Vec<i32>) -> Vec<i32> {
    let mut vec = vec;

    vec.push(22);
    vec.push(44);
    vec.push(66);

    vec
}

09-ownership

// Make me compile only by reordering the lines in `main()`, but without
// adding, changing or removing any of them.

fn main() {
    let mut x = 100;
    x += 1000;
    let y = &x;
    // Note: += is defined by the trait AddAssign:
    // https://doc.rust-lang.org/std/ops/trait.AddAssign.html
    println!("{} == {}", y, x);
}

bonus-ownership

// Make me compile using shadowing: Add a `let` statement.
// No reordering of them lines is necessary.

fn main() {
    let x = 100;
    let y = &x;
    let x = x + 1000;
    println!("1000 + {} == {}", y, x);
}

Viikko 3

Tavoitteet

  • tietää teknisiä yksityiskohtia eri tietorakenteista, kuten Vec, String ja &[T]
  • tietää pinon ja keon merkityksen ohjelman toiminnalle
  • ymmärtää omistajuuden säännöt
  • tietää lainaamisen säännöt
  • tietää mitä prosessit ovat ja miten käyttöjärjestelmä liittyy asiaan
  • tietää mikä on ohjelmatiedosto ja konekieli
  • osaa etsiä tietoa Rustista

Muisti ja lainaus


Omistajuuden säännöt

  • Jokaisella arvolla on tasan yksi omistaja tietyllä hetkellä
  • Kun omistaja poistuu skoopista, arvo pudotetaan eikä siihen pääse enää käsiksi

Kutsupino (stack)

  • Pino kehyksiä
  • Funktiokutsu työntää kehyksen kutsupinoon ylimmäksi
  • Funktion palautuessa ylin kehys tuhotaan
  • Kääntäjän pitää tietää kääntöaikana jokaisen kehyksen ja niille laitettavan tiedon koko

stack


Kutsupinon lautasmalli

lautasmalli

fn add(x: i32) {
	let y = 10;
}

fn main() {
	let x = 5;
	add(x); // <---
}

lautasmalli

fn add(x: i32) -> i32 {
	let y = 10;
	x + y // <---
}

fn main() {
	let x = 5;
	let z = add(x);
}

Tavut muistissa

u8, i8 u32, i32 u64, i64

Tavut muistissa

usize, isize Osoittimen kokoinen
(8 tavua 64-bittisellä prosessorilla)
*const T Osoitin ptr

Tyypit muistissa

T T Kääntöaikana
tunnettu koko
[T; n] T T T ... n Taulukko, joka sisältää
n elementtiä
[T] ... T T T ... Slice on jatkuva alue muistia ja koostuu
vain tyypin T ilmentymistä. Slicen koko
tiedetään aina ajonaikana

Keko (heap)

  • Dynaaminen muisti

  • Mahdollistaa vapaakokoisten tyyppien tallentamisen toisin kuin pinoon kehykselle

  • Hitaampi kuin stack, koska muisti pitää varata

  • Osa tietorakenteesta (kuten osoitin) sijaitsee aina stackilla

T T T T T T T T T T T T T
Vec<T> ptr capacity len |
T T ... len
capacity

Referenssi

&T ptr | T Pitää viitata validiin
ilmentymään Tstä

Use after free

#![allow(unused)]
fn main() {
let mut data = vec![1, 2, 3];
// get an internal reference
let x = &data[0];

// OH NO! `push` causes the backing storage
// slice of `data` to be reallocated.
// Dangling pointer! Use after free!
data.push(4);

println!("{}", x);
}

(Yleinen virhe C-ohjelmoinnissa)


#![allow(unused)]
fn main() {
error[E0502]: cannot borrow `data` as mutable
	because it is also borrowed as immutable
  --> src/main.rs:8:5
   |
4  |     let x = &data[0];
   |              ---- immutable borrow occurs here
...
8  |     data.push(4);
   |     ^^^^^^^^^^^^ mutable borrow occurs here
9  |
10 |     println!("{}", x);
   |                    - immutable borrow later used here
}

fn main() {
    let s1 = gives_ownership();
    let s2 = String::from("hello");
	
	// s2 is moved into takes_and_gives_back,
	// which also moves its return value into s3.
	// ... in a way s2 was borrowed
    let s3 = takes_and_gives_back(s2);
	// continue using the string ...
}

fn gives_ownership() -> String {
    let some_string = String::from("yours");

	// some_string is returned and
	// moves out to the calling function
    some_string
}

fn takes_and_gives_back(mut a_string: String) -> String {
	// do something with a_string ...
	
    a_string
}

Lainauksen säännöt

  • Jokaisella hetkellä ohjelman suorituksen aikana voi olla...
    • Mikä tahansa lukumäärä muuttumattomia lainauksia &T
    • Tai yksi muuttuva lainaus &mut T
  • Referenssien tulee aina osoittaa validiin Tn instanssiin


Otarustlings tauko


Referenssi sliceen

&[T] ptr len | [T] ... T T ... Slicet on olemassa vain referenssinä

String slice

&str ptr len | ... U T F - 8 ... String slice referenssi
len on pituus tavuina

Ohjelma

  • Konekielelle käännetyt käskyt (instructions)
  • Usein kutsunaan binääriksi
  • Ladataan muistiin, kun se suoritetaan

Prosessi

  • Luodaan ohjelman käynnistyessä
  • Omistaa oman muistialueen
  • Käyttöjärestelmä käynnistää prosessin
  • Prosessissa on aina vähintään yksi säie

htop


Säie

  • Suorittaa konekäskyjä itsenäisesti
  • Joka säikeellä on oma kutsupino
  • Prosessin säikeet jakavat kaikki muut resurssit

Lopuksi

idiomatic rust

Tehtävät

Lue kirjasta kappale 4.1.

otarustlings

Viikon tehtävät tehdään otarustligs ohjelmaa käyttäen.

Tehtävät ovat kansiossa week3.

otarustlingsin voit päivittää komennolla:

cargo install --force otarustlings

Lue otarustlings ohje

Tätä varten tarvitse Rustin työkalut

Yhteenveto

Tällä viikolla opittiin muistinhallinnan yleisiä sekä Rust-spesifejä yksityiskohtia.

Uusia asioita olivat:

  • eri tyyppien esitys muistissa
  • pino (stack) ja keko (heap)
  • Vec<T>-tietueen yksityiskohdat
  • referenssit & ja osoittimet *const
  • taulukot [T, n] ja slicet [T]
  • lainaamisen säännöt

Vanhoja asioita joita kerrattiin olivat:

  • omistajuus
  • skooppi
  • tyypit
  • Vec<T>-tietue
  • Rustin erilaisuus muihin kieliin nähden

Viikkopalaute

Joka viikolla voi ja kannattaa täyttää palautelomake, jonka avulla opettajat voi päätellä mitä pitäisi opettaa enemmän

Anna palautetta miten sinulla meni tämän viikon aiheet

Esimerkkiratkaisut

01-tuples

#![allow(unused)]
fn main() {
//! Use a tuple index operator to access the second element of
//! `numbers`.

#[test]
fn indexing_tuple() {
    let numbers = (1, 2, 3);
    // Create a variable holding the second number of the tuple.
    let second = numbers.1;

    assert_eq!(2, second, "This is not the second number in the tuple!")
}
}

02-tuples

//! Destructure the `cat` tuple so that the println will work.

fn main() {
    let cat = ("Furry McFurson", 3.5);
    let (name, age) = cat;

    println!("{} is {} years old.", name, age); // Don't change this line
}

03-tuples

//! Mutate the tuple so that the test passes

fn swap_tuple((x, y): (i32, i32)) -> (i32, i32) {
    (y, x)
}

fn main() {
    let mut tup = (2, 5);

    // TODO mutate the second element in `tup`
    tup.1 = 4;

    assert_eq!(swap_tuple(tup), (4, 2));
}

04-strings

//! Ok, here are a bunch of values-- some are `String`s, some are string slices:
//! `&str`s. Your task is to call one of these two functions on each value
//! depending on what you think each value is. That is, add either `string_slice`
//! or `string` before the parentheses on each line. There may be multiple
//! solutions.

fn string_slice(arg: &str) {
    println!("{}", arg);
}
fn string(arg: String) {
    println!("{}", arg);
}

fn main() {
    string_slice("blue");
    string("red".to_string());
    string(String::from("hi"));
    string("rust is fun!".to_owned());
    string("nice weather".into());
    string(format!("Interpolation {}", "Station"));
    string_slice(&String::from("abc")[0..1]);
    string_slice("  hello there ".trim());
    string("Happy Monday!".to_string().replace("Mon", "Tues"));
    string("mY sHiFt KeY iS sTiCkY".to_lowercase());
}

05-vectors

#![allow(unused)]
fn main() {
//! Step 1: fix `vec_loop` function's definition to allow using
//! `v.iter_mut()`.
//!
//! Step 2: complete the loop so that each number in the Vec is
//! multiplied by 2.

fn vec_loop(mut v: Vec<i32>) -> Vec<i32> {
    for elem in v.iter_mut() {
        // TODO: Fill this up so that each element in the Vec `v` is
        // multiplied by 2.  Hint: mutating the referent of `&mut i32`
        // requires a dereference.
        *elem *= 2;
    }

    v
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_vec_loop() {
        let v: Vec<i32> = (1..).filter(|x| x % 2 == 0).take(5).collect();
        let ans = vec_loop(v.clone());

        assert_eq!(ans, v.iter().map(|x| x * 2).collect::<Vec<i32>>());
    }
}
}

06-slices

#![allow(unused)]
fn main() {
//! Your task is to take a nice slice into `a` so that the test passes.

#[test]
fn slice_out_of_array() {
    let a = [1, 2, 3, 4, 5];

    let nice_slice = &a[1..4];

    assert_eq!([2, 3, 4], nice_slice)
}
}

07-slices

#![allow(unused)]
fn main() {
//! Your task is to create a `Vec` which holds the exact same elements as in the
//! array `a`. Make me compile and pass the test! Use the `vec!` macro:
//! https://doc.rust-lang.org/std/macro.vec.html

fn array_and_vec() -> ([i32; 4], Vec<i32>) {
    let a = [10, 20, 30, 40];
    let v = vec![10, 20, 30, 40];

    (a, v)
}

#[test]
fn test_array_and_vec_similarity() {
    let (a, v) = array_and_vec();
    assert_eq!(a, v[..]);
}
}

08-slices

//! Note: if your editor complains about `try_into`, it only works on
//! edition 2021 which `otarustlings` supports, so don't mind the
//! error.

fn make_mult_table(num: i32) -> [String; 10] {
    // The _ here tells Rust that it needs to figure it out so that
    // you don't need to. The Vec is required as collect can return
    // any collection that implements the FromIterator trait.
    let vec: Vec<_> = (0..10)
        .into_iter()
        .map(|i| format!("{i} * {num} = {}", i as i32 * num))
        .collect();

    vec.try_into().expect("vector to have exactly 10 elements")
}

fn main() {
    let a = make_mult_table(3);

    // TODO create a reference to the correct element
    let nine = &a[3];

    println!("the array is {:#?}", a);
    assert_eq!(nine, "3 * 3 = 9");
}

09-borrowing

//! You can't change anything except adding or removing references `&`

fn main() {
    let data = "Rust is great!".to_string();

    get_char(&data);

    string_uppercase(data);
}

// TODO Should not take ownership
fn get_char(data: &String) -> char {
    data.chars().last().unwrap()
}

// TODO Should take ownership
fn string_uppercase(mut data: String) {
    data = data.to_uppercase();

    println!("{}", data);
}

10-borrowing

//! Your task is to fix the function `third`.  Hint: the issue is not
//! related to lifetimes.
//!
//! Note: The exercises use strings in array because they are not
//! cheap to copy (unlike integers for example).

// TODO fix "lifetime" errors
fn third(a: &[String]) -> (&[String], &str) {
    (a, &a[2])
}

// Don't edit the following code

fn eat(s: String) {
    println!("{}", s);
}

fn main() {
    let a = [
        "first".to_string(),
        "second".to_string(),
        "third".to_string(),
        "fourth".to_string(),
    ];

    let (b, third) = third(&a);

    println!("{}", third);

    eat(a
        .into_iter()
        .nth(1)
        .expect("a to contain at least two elements"));
}

11-structs

//! Your task is to mutate the point in main to pass the assertion

/// A struct is a statically sized collection of data with keys and values.
struct Point {
    x: i32,
    y: i32,
}

fn display_point(point: &Point) {
    println!("{}, {}", point.x, point.y);
}

fn main() {
    let mut point = Point {
        x: 3,
        y: -5,
    };

    // TODO mutate point's x-coordinate
    point.x = 6;

    display_point(&point);
    assert_eq!(point.x, 6);
}

12-strings

#![allow(unused)]
fn main() {
//! Your task is to capitalize the first letter/character in `string`. Search
//! the Rust standard library documentation for `str::chars`. Note that a String
//! has all the methods of str because of the `Deref` trait. Don't be afraid to
//! search more information online

fn capitalize_first(string: &mut String) {
    // First we take the first character, which can be multiple bytes, but one
    // UTF-8 character only (max 4 bytes). We do nothing and return if there is
    // no first character.

    // For example with "firm" the bytes are
    // string: [u8] = EF AC 81   72 6d
    //                ^^^^^^^^-fi r  m
    if let Some(first) = string.chars().next() {
        // the first character 'fi'
        // first: char = 01 fb 00 00

        // Next we create a string from the characters (yes there can be
        // multiple) which are now in uppercase.
        let first_uppercase = first.to_uppercase().collect::<String>();
        // first_uppercase: [u8] = 46   49
        //                         F    I
        let mut first_chars = first_uppercase.chars();

        if let Some(first_char) = first_chars.next() {
            // Next we take the first character and join it with the rest of
            // the characters after converting them to lowercase
            let first_capitalized = std::iter::once(first_char)
                .chain(first_chars.map(char::to_lowercase).flatten())
                .collect::<String>();

            // Finally we remove the first `len_utf8` bytes from the string and
            // prepend with the bytes from `first_uppercase`.
            // string.replace_range(..first.len_utf8(), &first_uppercase);
            // Behind the scenes the string is converted to vector and spliced with
            // the new data `first_uppercase`
            // https://doc.rust-lang.org/std/vec/struct.Vec.html#method.splice

            string.replace_range(..first.len_utf8(), &first_capitalized);

            // and the string is
            // string: [u8] = 46 69 72 6d
            //                F  i  r  m
        }
    }
}

#[test]
fn letters() {
    let mut string = "testing".to_string();

    capitalize_first(&mut string);

    assert_eq!(string, "Testing");
}

#[test]
fn numbers() {
    let mut string = "1234".to_string();

    capitalize_first(&mut string);

    assert_eq!(string, "1234");
}

#[test]
fn special() {
    let mut string = "山".to_string();

    capitalize_first(&mut string);

    assert_eq!(string, "山");
}

#[test]
fn emoji() {
    let mut string = "❤".to_string();

    capitalize_first(&mut string);

    assert_eq!(string, "❤");
}

#[test]
fn sigma() {
    let mut string = "Σ".to_string();

    capitalize_first(&mut string);

    assert_eq!(string, "Σ");
}

// The hard one
#[test]
fn ligatures() {
    let mut string = "firm".to_string();

    capitalize_first(&mut string);

    assert_eq!(string, "Firm");
}
}

B1-vecdeque

//! This is a bonus exercise.
//! 
//! Your task is to find the right number to rotate the VecDeque by. You are
//! only allowed to change the argument to `rotate_right`

use std::collections::VecDeque;

fn make_uncontiguous_vecdeq() -> VecDeque<i32> {
    let mut vd: VecDeque<_> = (1..=5).chain(1..=5).collect();

    // TODO rotate the deque until both slices are equal
    vd.rotate_right(5);

    vd
}

fn main() {
    let vd = make_uncontiguous_vecdeq();

    let (head, tail) = vd.as_slices();

    assert_eq!(head, tail);
}

B2-traitobj-tuukka

//! This is a bonus exercise (hard)

use std::collections::HashMap;

fn make_vec() -> Vec<(&'static str, i32)> {
    vec![("a",1),("b",2),("c",3)]
}

fn make_hashmap() -> HashMap<&'static str, i32> {
    let mut hm = HashMap::new();
    hm.insert("d", 4);
    hm.insert("e", 5);
    hm.insert("f", 6);
    hm
}

// TODO fill in the blanks
fn print_dyn_iter(iter: &mut dyn Iterator<Item = (&str, i32)>) {
    for (key, value) in iter {
        println!("{key} - {value}")
    }
}

fn main() {
    let mut v = make_vec().into_iter();
    let mut hm = make_hashmap().into_iter();

    print_dyn_iter(&mut v);
    assert_eq!(v.next(), None);

    print_dyn_iter(&mut hm);
    assert_eq!(hm.next(), None);
}

B3-traitobj-tuukka

//! This is a bonus exercise (hard)
//! 
//! Your task is to make the main function not panic. You can only change code
//! inside main.
//! 
//! Part 1: swap the values of v and hm safely with something from
//! https://doc.rust-lang.org/std/mem/index.html
//! 
//! Part 2: fix the next problem ¯\_(°ペ)_/¯

use std::any::Any;
use std::collections::HashMap;

fn make_something() -> Box<dyn Any> {
    Box::new(vec![("a", 1), ("b", 2), ("c", 3)])
}

fn make_something_else() -> Box<dyn Any> {
    let mut hm = HashMap::new();
    hm.insert("d", 4);
    hm.insert("e", 5);
    hm.insert("f", 6);
    Box::new(hm)
}

fn check_type_vec(any: &mut dyn Any) {
    if let Some(vec) = any.downcast_ref::<Vec<(&str, i32)>>() {
        println!("is vector");
    } else {
        panic!("TypeError: Vec expected");
    }
}

fn check_type_hashmap(any: &mut dyn Any) {
    if let Some(hm) = any.downcast_ref::<HashMap<&str, i32>>() {
        println!("is hashmap");
    } else {
        panic!("TypeError: HashMap expected");
    }
}

// You can only edit code in the main function
fn main() {
    let mut v: Box<dyn Any> = make_something();
    let mut hm: Box<dyn Any> = make_something_else();

    std::mem::swap(&mut v, &mut hm);

    // Reborrow is needed to get a reference to the data inside the box
    check_type_hashmap(&mut *v);
    check_type_vec(&mut *hm);
}

Viikko 4

Tällä viikolla tutustutaan erilaisiin tietotyyppeihin ja niiden määrittelemiseen.

Jos et päässyt tunnille, voit itsenäisesti lukea kirjasta kappaleet 5 ja 6.

Tavoitteet

  • ymmärtää erilaisten tietotyyppien käyttötarkoitukset
    • struct
    • enum
  • osata luoda omia tietotyyppejä
  • tietää tyyppiteorian peruskäsitteitä:
    • tietotyyppi
    • yhdistelmätyyppi
    • tyyppikonstruktori
    • tyyppiparametri
    • algebrallinen tietotyyppi
      • tulotyyppi
      • summatyyppi
  • tietää staattisen ja dynaamisen tyypityksen eron
  • osata optionin käyttäminen ja ymmärtää None
  • tietää Resultin tarkoituksen ja funktionaalisen virheenhallinnan
  • tietää piirteiden yleinen tarkoitus
  • osata #[derive]

Algebrallinen tyyppiteoria


Lainauksen säännöt

  • Jokaisella hetkellä ohjelman suorituksen aikana voi olla &T ja &mut T lainauksia...
    • Mikä tahansa lukumäärä muuttumattomia lainauksia &T
    • Tai yksi muuttuva lainaus &mut T
  • Referenssien tulee aina osoittaa validiin Tn instanssiin


Mikä on tyyppi?

  • Tieto luokitellaan tyyppeihin [i32; 5]
  • Myös funktioilla on tyyppi fn(String) -> usize
  • Tyyppi määrittelee ja rajoittaa missä muodossa tieto esiintyy
  • Esitys muistissa
  • Miten funktio on esitetty muistissa?

[i32; 5]

Algebralliset tietotyypit

  • Tulo- ja summatyypit
  • Ergonominen match
  • Konkreetti eikä abstrakti
  • Tietoon pääsee helposti käsiksi,
    koska tiellä ei ole turhia abstraktioita
    • Vertaa luokkiin ja olioihin

Tulotyyppi

T = bool × i32 × Vec<f64>

  • Tulotyypit sisältävät monta ilmentymää kerralla
    • T:n konstruktoiminen vaatii ilmentymän boolista, i32sta ja Vec<f64>sta
  • Tuplet ja structit ovat tulotyyppejä

Summatyyppi

S = A(i32) + B(String)

  • Ilmentymän variantit ovat samaa tyyppiä
  • Varianteilla on uniikki nimi
    • A ja B
  • Enumit ovat summatyyppejä
  • S ei voi sisältää i32 ja String samaan aikaan

struct

#![allow(unused)]
fn main() {
struct Person {
	age: i32,
	name: String,
}
}

Erilaisia structeja

#![allow(unused)]
fn main() {
/// tuple struct
struct S(i32, String);
}
#![allow(unused)]
fn main() {
/// unit struct
struct S;
}

enum

#![allow(unused)]
fn main() {
enum E {
  None,
  Some(i32),
  Person { name: String, age: i32 },
}
}

struct muistissa

struct S { b: B, c: C } struct S(B, C)
C B

enum muistissa

enum E { A, B, C }
Tag "A" A tai Tag "B" B tai Tag "C" C

Missä None?

yobs = {"Python": 1989, "Rust": 2010, "Prolog": 1972}
rust_birth = yobs.get("Rust") # 2010
print(rust_birth + 12)        # 2022

java_birth = yobs.get("Java") # None
print(java_birth + 27)        # Wat?

java_birth = yobs.get("Java") # None
print(java_birth + 27)        # ERROR!!!

TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'

#![allow(unused)]
fn main() {
use std::collections::HashMap;

let yobs = HashMap::from([
    ("Python", 1989),
    ("Rust", 2010),
    ("Prolog", 1972),
]);

let rust_birth = yobs.get("Rust");
println!("{:?}", rust_birth); // Some(2010)
}

HashMap::<K, V>::get

pub fn get(&self, k: &K) -> Option<&V>


let java_birth: Option<&i32> = yobs.get("Java"); // None
println!("{:?}", java_birth.map(|y| y + 12));    // None

Option<T>

#![allow(unused)]
fn main() {
enum Option<T> {
	Some(T),
	None,
}
}

Variantit eivät ole tyyppejä!


match

#![allow(unused)]
fn main() {
enum E {
	/// Unit variant
    None,
	/// Newtype variant
    Some(i32),
	/// Struct variant
    Person { name: String, age: i32 },
}

let e = E::Some(5);
match e {
    E::Some(num) => dbg!(num),
    _ => todo!(),
};
}
[src/main.rs:11] num = 5


λ-funktiot (klosuurit)

#![allow(unused)]
fn main() {
let luvut = (1..12).filter_map(
	|n| if n % 3 == 0 { Some(n * 2) } else { None }
);

for n in luvut {
    println!("{n}");
}
}

Lopuksi

Tehtävät

Lue kirjasta kappale 6.2.

otarustlings

Viikon tehtävät tehdään otarustligs ohjelmaa käyttäen.

Tehtävät ovat kansiossa week4.

otarustlingsin voit päivittää komennolla:

cargo install --force otarustlings

Lue otarustlings ohje

Tätä varten tarvitse Rustin työkalut

Yhteenveto

Tällä viikolla opittiin tietotyypeistä, staattisesta tyypityksestä ja algebrallisten tyyppien eduista.

Uusia asioita olivat:

  • tietotyyppien luominen
  • tyyppiteoria
  • algebrallinen tietotyyppi
  • funktionaalisen virheenkäsittely perusteet, Option virheenkäsittelyssä
  • Miksi None ei ole sama kuin muiden kielten null
  • matchin käyttö ja if let
  • polymorfismin ja piirteiden perusteet

Vanhoja asioita joita kerrattiin olivat:

  • Optionin käyttöä
  • tietotyyppien representaatioita
  • olioita

Viikkopalaute

Joka viikolla voi ja kannattaa täyttää palautelomake, jonka avulla opettajat voi päätellä mitä pitäisi opettaa enemmän

Anna palautetta miten sinulla meni tämän viikon aiheet

Esimerkkiratkaisut

01-enums

#![allow(unused)]
fn main() {
//! Your task is to define the variants of the enumeration. Do not edit `main`.

#[derive(Debug)] // This makes printing the instances with {:?} (debug format) possible
enum Message {
    Quit,
    Echo,
    Move,
    ChangeColor,
}

#[test]
fn _main() {
    println!("{:?}", Message::Quit);
    println!("{:?}", Message::Echo);
    println!("{:?}", Message::Move);
    println!("{:?}", Message::ChangeColor);
}
}

02-structs

#![allow(unused)]
fn main() {
//! Your task is to create the missing structs and fill in the missing parts of
//! the tests. Read the chapter about structs (whether you need help or not)
//! https://doc.rust-lang.org/book/

#[derive(Debug)]
struct ColorClassicStruct {
    name: String,
    hex: String,
}

#[derive(Debug)]
struct ColorTupleStruct<'a>(&'a str, &'a str);

#[derive(Debug)]
struct UnitStruct;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn classic_structs() {
        let green = ColorClassicStruct {
            name: "green".to_string(),
            hex: "#00FF00".to_string(),
        };

        assert_eq!(green.name, "green");
        assert_eq!(green.hex, "#00FF00");
    }

    #[test]
    fn tuple_structs() {
        let green = ColorTupleStruct("green", "#00FF00");

        assert_eq!(green.0, "green");
        assert_eq!(green.1, "#00FF00");
    }

    #[test]
    fn unit_structs() {
        let unit_struct = UnitStruct;
        let message = format!("{:?}s are fun!", unit_struct);

        assert_eq!(message, "UnitStructs are fun!");
    }
}
}

03-enums2

#![allow(unused)]
fn main() {
//! Your task is to fix the `is_quit` function. Make good use of your favorite
//! feature of Rust: `match`. Bonus points if you can do it with `matches!`.

#[derive(Debug)]
enum Message {
    Quit,
    Echo,
    Move,
    ChangeColor,
}

fn is_quit(msg: Message) -> bool {
    match msg {
        Message::Quit => true,
        _ => false,
    }
}

#[test]
fn _main() {
    assert!(is_quit(Message::Quit));
    assert!(!is_quit(Message::Echo));
}
}

04-newtype

#![allow(unused)]
fn main() {
//! Your task is to implement `to_miles` for `Kilometers` and fill in the blank
//! in `main`.

// These structs are called newtype structs. Read more about the newtype design
// pattern:
// https://rust-unofficial.github.io/patterns/patterns/behavioural/newtype.html
struct Miles(f64);
struct Kilometers(f64);

impl Miles {
    fn to_kilometers(self) -> Kilometers {
        Kilometers(1.6 * self.0)
    }
}

impl Kilometers {
    fn to_miles(self) -> Miles {
        Miles(self.0 / 1.6)
    }
}

fn travel(distance: Miles) -> String {
    format!("Travelled {} miles", distance.0)
}

fn travel_kilometers(distance: Kilometers) -> String {
    travel(distance.to_miles())
}

#[test]
fn _main() {
    let msg = travel_kilometers(Kilometers(10.0));

    assert_eq!(&msg, "Travelled 6.25 miles");
}
}

05-derive

#![allow(unused)]
fn main() {
//! Your task is to derive required traits for `Foo`. You might find *the book*
//! helpful: https://doc.rust-lang.org/book/appendix-03-derivable-traits.html

#[derive(Default, Debug, Clone, PartialEq)]
struct Foo {
    numbers: Vec<i32>,
}

#[test]
fn _main() {
    let foo = Foo::default();
    dbg!(foo.clone());

    assert_eq!(foo, Foo { numbers: vec![] })
}
}

06-type-safety

#![allow(unused)]
fn main() {
//! Your task is convert `delete_filesystem` to use DryRun instead of a boolean.
//! Read more about type safety guidelines here
//! https://rust-lang.github.io/api-guidelines/type-safety.html

#[derive(Debug)]
enum DryRun {
    /// Does not commit any changes
    Yes,
    /// Danger: everything will be lost
    No,
}

fn delete_filesystem(dry_run: DryRun) -> bool {
    match dry_run
    {
        DryRun::No => {
            println!("Deleting all files...");
            true
        },
        DryRun::Yes => {
            println!("Not deleting files...");
            false
        }
    }
}

#[test]
fn _main() {
    let deleted = delete_filesystem(DryRun::No);

    assert_eq!(deleted, true);
}
}

07-match

#![allow(unused)]
fn main() {
//! Your task is to implement the match
//!
//! Part 1: Write the match statement in `send_message` to "do the correct
//! stuff".
//!
//! Part 2: You may need to add more messages to the array `messages`.

/// A position in 2D space
#[derive(Debug, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

/// State of the abstract high-level frobnicator
#[derive(Debug)]
struct State {
    location: Point,
    running: bool,
}

/// Messages sent to the [`State`] frobnicator
#[derive(Debug)]
enum Message {
    /// Stops the frobnicator
    Quit,
    /// Moves the frobnicator
    Move { x: i32, y: i32 },
}

impl State {
    fn new() -> Self {
        Self {
            location: Point { x: 0, y: 0 },
            running: true,
        }
    }

    fn send_message(&mut self, msg: Message) {
        match msg {
            Message::Quit => self.running = false,
            Message::Move { x, y } => {
                let x = self.location.x + x;
                let y = self.location.y + y;
                self.location = Point { x, y }
            },
        }
    }
}

#[test]
fn _main() {
    let messages = [
        Message::Move { x: 1, y: 3 },
        Message::Move { x: -2, y: -1 },
        Message::Quit,
    ];

    let mut state = State::new();

    for msg in messages {
        state.send_message(msg);
    }

    assert_eq!(state.location, Point { x: -1, y: 2 });
    assert!(!state.running);
}
}

08-if-let

#![allow(unused)]
fn main() {
//! Your task is to fill in the blanks. Don't change anything outside the `if
//! let`. Check https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html
//! around "Destructuring Structs" for the answer.

enum Message {
    Quit,
    Move { x: i32, y: i32 },
}

#[test]
fn _main() {
    let msg = Message::Move { x: 1, y: 2 };

    let mut x = 2;
    let mut y = 3;

    if let Message::Move { x: a, y: b } = msg {
        x += a;
        y += b;
    }

    assert_eq!(x, 3);
    assert_eq!(y, 5);
}
}

09-match2

#![allow(unused)]
fn main() {
//! Your task is to fill in the match in `execute`.

#[derive(Debug, PartialEq)]
enum Cli {
    Run(Option<String>),
    Rename(String, Option<String>),
}

impl Cli {
    pub fn execute(&self) -> Option<String> {
        use Cli::*; // Makes all variants visible without Cli prefix
        match self {
            Run(None) | Rename(_, None) => None,
            Run(Some(file)) => Some(format!("running {file}").to_string()),
            Rename(a, Some(b)) => Some(format!("renaming {a} to {b}").to_string()),
        }
    }
}

#[test]
fn _main() {
    assert_eq!(
        Cli::Run(Some("ferris.rs".to_string())).execute().unwrap(),
        "running ferris.rs"
    );
    assert_eq!(
        Cli::Rename("ferris.rs".to_string(), Some("corro.c".to_string()))
            .execute()
            .unwrap(),
        "renaming ferris.rs to corro.c"
    );
    assert_eq!(Cli::Rename("/etc/shadow".to_string(), None).execute(), None);
    assert_eq!(Cli::Run(None).execute(), None);
}
}

10-option

#![allow(unused)]
fn main() {
//! Your task is to fix the type errors and initialize the `numbers` array.
//!
//! Read more about Options in the book chapter 6.1
//! https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html

// Do not change this function
fn print_number(maybe_number: Option<u16>) {
    println!("maybe {:?}", maybe_number);
}

#[test]
fn _main() {
    print_number(Some(13));
    print_number(Some(99));

    let mut numbers = [None; 5];

    for iter in 0..5 {
        let number_to_add: u16 = ((iter * 1235) + 2) / (4 * 16);

        numbers[iter as usize] = Some(number_to_add);
    }

    assert_eq!(numbers, [Some(0), Some(19), Some(38), Some(57), Some(77)]);
}
}

11-option-iter

#![allow(unused)]
fn main() {
//! Your task is to fill in all the blanks. Nothing else should need fixing.
//! Read more about iterators and their adaptors in
//! https://doc.rust-lang.org/book/ch13-02-iterators.html If too hard, try
//! harder.

/// Divides `dividend` by `divisor` safely, i.e. never divides by zero. A divide
/// by zero is represented with `None`
fn safe_div(dividend: i32, divisor: i32) -> Option<f32> {
    if divisor == 0 {
        None
    } else {
        Some(dividend as f32 / divisor as f32)
    }
}

/// Returns a list of `Some`s with all integers from -5 to 4 inside
fn list_of_options() -> Vec<i32> {
    (-5..5).into_iter().collect()
}

#[test]
fn _main() {
    let list = list_of_options();

    let pair_fractions: Vec<Option<f32>> = list
        .windows(2)
        .map(|iter| if iter[1] == 0 { None } else { Some(iter[0] as f32 / iter[1] as f32)} )
        .collect();

    use std::convert::identity; // Super helpful morphism, right?

    let sum: f32 = pair_fractions.into_iter().filter_map(identity).sum();

    assert_eq!(sum, 8.0);
}
}

B1-lifetimes

#![allow(unused)]
fn main() {
//! Your task is to fill in the generic parameters to `Person` and fix the
//! dangling reference issue however you wish.
//!
//! Note that a `String` can never be static as it includes an allocation (the
//! crate lazy_static solves that issue, but is not needed here).

#[derive(Debug)]
struct Person<'a> {
    pub name: &'a str,
    pub age: i32,
}

fn make_person() -> Person<'static> {
    let name = "Michael Stevens";

    Person {
        name: &name,
        age: 36,
    }
}

#[test]
fn _main() {
    let michael = make_person();

    assert_eq!(michael.age, 36)
}
}

B2-config-parser

//! Your task is to add missing fields to `Config` which are present in
//! `config.toml` at `exercises/week4/BX-config-parser/config.toml`. You also
//! need to add new structs to accommodate the data.
//!
//! You should start by reading what serde is about: https://serde.rs/.

#![allow(dead_code)] // In this example we don't use the parsed data

use std::{collections::HashMap, fs::read_to_string, net::Ipv4Addr, path::Path};

use serde::Deserialize;
use toml::{from_str, value::Datetime, Value};

#[derive(Debug, Deserialize)]
struct Config {
    title: String,
    owner: Owner,
    database: Database,
    servers: HashMap<String, Server>,
}

#[derive(Debug, Deserialize)]
struct Owner {
    name: String,
    dob: Datetime,
}

#[derive(Debug, Deserialize)]
struct Database {
    enabled: bool,
    ports: Vec<u16>,
    /// A vector of arbitrary toml values, can be heterogeneous
    data: Vec<Value>,
    temp_targets: HashMap<String, f32>,
}

#[derive(Debug, Deserialize)]
struct Server {
    ip: Ipv4Addr,
    role: String,
}

impl Config {
    pub fn parse(path: impl AsRef<Path>) -> anyhow::Result<Self> {
        let path = path.as_ref();
        toml::from_str(&read_to_string(path)?).map_err(anyhow::Error::from)
    }
}

fn main() -> anyhow::Result<()> {
    let config: Config = from_str(&read_to_string("config.toml")?)?;

    println!("the parsed config is: {:#?}", config);

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn name_is_tom() {
        let config = Config::parse("config.toml").unwrap();

        assert!(config.owner.name.starts_with("Tom"));
    }

    #[test]
    fn open_ports() {
        let config = Config::parse("config.toml").unwrap();

        assert_eq!(config.database.ports.len(), 3);
    }

    #[test]
    fn reasonable_temp_targets() {
        let config = Config::parse("config.toml").unwrap();

        assert!(config
            .database
            .temp_targets
            .iter()
            .all(|(_key, &target)| target < 100.0));
    }

    #[test]
    fn server_ips_different() {
        let config = Config::parse("config.toml").unwrap();

        let alpha = config.servers.get("alpha").unwrap();
        let beta = config.servers.get("beta").unwrap();

        assert_ne!(alpha.ip, beta.ip);
    }
}

B3-type-safety-generics

#![allow(unused)]
fn main() {
//! Your task is to implement the three required methods for `Rocket<Launched>`
//! to make the tests pass. Consult the tests for guidance.
//!
//! Bonus bonus task: Add documentation comments to all Rocket's methods.

use std::marker::PhantomData;

const ESCAPE_VELOCITY: u32 = 11_186;

/// A rocket sitting on the ground with at least one cat
#[derive(Debug)]
struct Grounded;
/// A rocket flying in outer space with some velocity
#[derive(Debug)]
struct Launched;
/// A rocket crashed due to undefined behavior with no cat videos left
#[derive(Debug)]
struct Crashed;

#[derive(Debug)]
struct Rocket<Stage = Grounded> {
    stage: PhantomData<Stage>,
    velocity: u32,
    crew: u32,
}

impl Rocket<Grounded> {
    pub fn new() -> Self {
        Self {
            stage: PhantomData,
            velocity: 0, // Not moving
            crew: 1,     // One captain
        }
    }

    pub fn add_crew(&mut self, more_cats: u32) {
        self.crew += more_cats;
    }

    pub fn launch(self) -> Rocket<Launched> {
        assert_eq!(self.velocity, 0); // This is harder to make invariant using types
        Rocket::<Launched> {
            stage: PhantomData,
            velocity: ESCAPE_VELOCITY,
            crew: self.crew,
        }
    }
}

impl Rocket<Launched> {
    pub fn accelerate(&mut self, v: u32) {
        self.velocity += v;
    }

    pub fn decelerate(&mut self, v: u32) {
        self.velocity -= v;
    }

    pub fn try_land(self) -> Result<Rocket::<Grounded>, Rocket::<Crashed>> {
        if self.velocity == 0 {
            Ok(Rocket::<Grounded>
               {
                   stage: PhantomData,
                   velocity: self.velocity,
                   crew: self.crew,
               })
        } else {
            Err(Rocket::<Crashed> {
                   stage: PhantomData,
                   velocity: self.velocity,
                   crew: 0,
            })
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn grounded_rocket() {
        let mut rocket = Rocket::new();
        rocket.add_crew(5);

        assert!(matches!(
            rocket,
            Rocket::<Grounded> {
                crew: 6,
                velocity: 0,
                ..
            }
        ))
    }

    /// Rocket<Launched> should be able to change its velocity with `accelerate`
    #[test]
    fn launched_rocket() {
        let mut rocket = Rocket::new();
        rocket.add_crew(5);

        let mut rocket = rocket.launch();
        rocket.accelerate(100);
        assert!(matches!(
            rocket,
            Rocket::<Launched> {
                crew: 6,
                velocity: 11_286,
                ..
            }
        ))
    }

    /// Rocket<Launched> should be able to `try_land`, which will be successful
    /// if its velocity is zero, otherwise it will crash with no cat videos left
    #[test]
    fn houston_we_have_had_a_problem() {
        let mut rocket = Rocket::new();

        let rocket = rocket.launch();
        let crashed: Result<Rocket, Rocket<Crashed>> = rocket.try_land();
        assert_eq!(crashed.unwrap_err().crew, 0)
    }

    /// Rocket<Launched> should be able to change its velocity with `decelerate`
    #[test]
    fn landing_successful() {
        let mut rocket = Rocket::new();
        rocket.add_crew(5);

        let mut rocket = rocket.launch();
        rocket.decelerate(11_186);
        let landed = rocket.try_land();
        assert!(matches!(
            landed,
            Ok(Rocket::<Grounded> {
                crew: 6,
                velocity: 0,
                ..
            })
        ))
    }
}
}

B4-reqwest

fn _main() {
    let url = "https://fablab.rip";

    println!("Fetching {:?}...", url);

    let res = reqwest::blocking::get(url).unwrap();

    println!("Response: {:?}", res.text().unwrap());
}

B5-closures

#![allow(unused)]
fn main() {
//! Your task is to fix the returned closure from `is_garbage`.

fn is_garbage() -> impl Fn(&str) -> bool {
    let no_garbage = vec!["C", "C++", "Rust"];
    move |lang: &str| -> bool { !no_garbage.contains(&lang) }
}

#[test]
fn _main() {
    let is_garbage = is_garbage();
    assert!(is_garbage("Java"));
    assert!(is_garbage("Python"));
    assert!(is_garbage("TypeScript"));
}
}

B6-closure-traits

#![allow(unused)]
fn main() {
//! Your task is to implement `evaluate` for `Map`, and define the `square`
//! closure.

struct Map<F>
where
    F: Fn(i32) -> i32,
{
    data: Vec<i32>,
    mapper: F,
}

impl<F> Map<F>
where
    F: Fn(i32) -> i32,
{
    fn new(data: Vec<i32>, mapper: F) -> Self {
        Self { data, mapper }
    }

    /// Maps each element in `data` using `mapper` in-place
    fn evaluate(&mut self) {
        for num in self.data.iter_mut() {
            *num = (self.mapper)(*num);
        }
    }

    fn into_vec(self) -> Vec<i32> {
        self.data
    }
}

#[test]
fn _main() {
    let square = |x| x * x;

    let mut map = Map::new(vec![1, 2, 3, 4], square);

    map.evaluate();
    map.evaluate();

    assert_eq!(map.into_vec(), [1, 16, 81, 256]);
}
}

Viikko 5 (keskeneräinen)

Tällä viikolla käsitellään lähdekoodia, virheenkäsittelyä, geneerisiä tyyppejä ja piirteitä.

Tavoitteet

  • tietää semanttisen versioinnin perusteet
  • tietää mitä on avoin lähdekoodi, sekä mitä avointa lähdekoodia omalla tietokoneella pyörii
  • tietää moduulihierarkia ja itemien näkyvyys
  • osata käyttää eri Rust-kirjastoja omissa ohjelmissa ja kirjastoissa
  • tietää Result-tyypin toiminta
    • miten virheenkäsittely on toteutettu Rustissa (ja funktionaalisissa kielissä)
    • miten virheenkäsittely on toteutettu muissa imperatiivisissa kielissä
  • tietää mitä panikointi tarkoittaa ja miten sitä voi välttää
  • tietää geneeristen tyyppien syntaksi
  • tietää piirteiden toiminta
    • tietää Iterator-piirteen määritelmä sekä tyyppejä jotka implementoivat Iterator-piirteen
  • osata implementoida omia tietorakenteita, kuten binääripuu

Rajapinnat ja virheet

  • Versiointi ja lähdekoodi
  • Virheenkäsittely
  • Piirteet
  • Iteraattorit

Semanttinen versiointi

  • Kertoo kirjastojen versioiden yhteensopivuuden
  • MAJOR.MINOR.PATCH
  • Taaksepäinyhteensopivuus (backward compatibility)

semver


Avoin lähdekoodi

  • Ilmaista ja vapaasti saatavilla
  • Lähdekoodia saa jakaa ja siihen saa tehdä muutoksia
  • Kaikki netistä, kuten GitHubista, löytyvä lähdekoodi ei ole avointa
  • Lisenssit: GPL, MIT, Apache, BSD, UNLICENSE
  • Ohjelmisto: Chromium, VSCodium, GNU/Linux, Firefox, GNU Emacs

chromium

linux

GNU

firefox

blender

emacs

vscodium

debian

freeCAD


Moduulit

  • Rust-lähdekoodin voi jakaa tiedostoihin ja kansioihin
    • Jokainen tiedosto ja kansio määrittelee moduulin
  • Moduuleilla erotetaan kirjastojen osat
  • Moduuli koostuu yksityisistä ja julkisista (pub) itemeistä
otarustlings/
├── README.md
├── Cargo.toml
└── src/
    ├── lib/
    │   ├── exercise.rs
    │   ├── lib.rs
    │   ├── menu.rs
    │   ├── state.rs
    │   ├── tester.rs
    │   └── utils.rs
    └── main.rs

Rust lähdekoodi

  • Paketti koostuu laatikoista
    • Cargo.toml
  • Laatikko koostuu moduuleista
  • Laatikko voi olla ohjelma (bin) tai kirjasto (lib)

crates

[package]
name = "otarustlings"
version = "0.4.4"
edition = "2021"

[lib]
name = "otarustlings"
path = "src/lib/lib.rs"

[[bin]]
name = "otarustlings"
path = "src/main.rs"
doc = false

[dependencies]
notify = "4.0.17"
walkdir = "2.3.2"
toml = "0.5.8"

Pakettihierarkia

  • Paketeilla on riippuvuuksia toisten pakettien kirjastoihin (lib)
  • Kääntöaika ∝ pakettien määrään
  • Yhdellä paketilla voi olla yli 100 transienttia riippuvuutta
λ cargo tree
otarustlings v0.4.4
├── notify v4.0.17
│   ├── bitflags v1.3.2
│   ├── inotify v0.7.1
│   │   ├── bitflags v1.3.2
│   │   ├── inotify-sys v0.1.5
│   │   │   └── libc v0.2.124
│   │   └── libc v0.2.124
│   ├── libc v0.2.124
│   ├── mio v0.6.23
│   │   ├── cfg-if v0.1.10
│   │   ├── iovec v0.1.4
│   │   │   └── libc v0.2.124
│   │   ├── libc v0.2.124

Virheenkäsittely

  • Bugi, Virhe, Result
  • Virheenkäsittely on eksplisiittistä ja pakollista
  • Paniikki panic!, unwrap
  • Propagaatio ?
fn read_hello() -> Result<String, io::Error> {
    let mut file = File::open("hello.txt")?;
    let mut username = String::new();
    file.read_to_string(&mut username)?;
    Ok(username)
}

Virheenkäsittely Pythonissa

def upper(name):
	if name == "":
        raise Exception("no name")
	return name.upper()

def main():
	try:
        name = upper("sammy")
    except Exception as err:
		print("Could not upper:", err)
		return

	print("Uppercase name:", name)

Virheenkäsittely GO:ssa

func upper(name string) (string, error) {
	if name == "" {
		return "", errors.New("no name")
	}
	return strings.ToTitle(name), nil
}

func main() {
	name, err := upper("sammy")
	if err != nil {
		fmt.Println("Could not upper:", err)
        return
	}

	fmt.Println("Uppercase name:", name)
}

Algebrallinen ratkaisu

Imperatiivisissa kielissä

  • Virheet keskeyttävät funktion suorituksen
  • NULL on mitä tahansa tyyppiä
  • NULLin tarkistaminen ei ole pakotettua
  • Ei ole selvää millä rivillä voi tapahtua virhe

Funktionalisissa kielissä

  • Virheet eivät ohita suoritusjärjestystä
  • NULLia vastaava arvo ei ohita tyyppijärjestelmää
  • Option pitää aina tarkistaa (esim match) jotta saa sisäisen arvon
  • NULL blog

LanguageNULLMaybeNULL Score
Objective Cnil, Nil, NULL, NSNullMaybe, from SVMaybe
CNULL⭐⭐
Gonil⭐⭐
C++NULLboost::optional, from Boost.Optional⭐⭐⭐
PythonNoneMaybe, from PyMonad⭐⭐⭐
Javanulljava.lang.Optional⭐⭐⭐⭐
Scalanullscala.Option⭐⭐⭐⭐
OCamloption⭐⭐⭐⭐⭐
RustOption⭐⭐⭐⭐⭐
HaskellMaybe⭐⭐⭐⭐⭐

Ohjelman (bin) virheenkäsittely

  • Väärän tilan välttäminen
  • Turvallinen kaatuminen
  • Käyttäjäystävällisyys

Kirjaston (lib) virheenkäsittely

  • Väärän tilan välttäminen
  • Turvallinen kaatuminen
  • Virheiden propagointi
  • Totaalit funktiot
  • Koodariystävällinen

Result::<T, E>::unwrap

impl<T, E> Result<T, E> {
    pub fn unwrap(self) -> T
    where
        E: fmt::Debug,
    {
        match self {
            Ok(t) => t,
            Err(e) => panic!(
				"called `Result::unwrap()` on an `Err` value: {:?}",
				e,
			),
        }
    }
}

Geneeriset tyypit

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}

type

use std::result;

pub type Result<T> = result::Result<T, ConfigError>;

pub fn config_file(path: impl AsRef<Path>) -> Result<Config> { .. }
  • Tapa nimetä jokin tyyppi, kuten "vakio" tyyppitasolla
  • Geneerinen tyyppi on kuin funktio tyyppitasolla

Piirteet

  • Tyyppien välinen rajapinta
  • Kuvaa toiminnallisuutta abstraktisti
  • Yleisiä piirteitä:
    • Default
    • Clone
    • Iterator
  • Geneeristen tyyppien rajoittaminen
  • dyn Trait

struct String


fn upper

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UpperError {
    NoName,
}

pub fn upper<T>(name: T) -> Result<String, UpperError>
where
    T: AsRef<str>,
{
    let name = name.as_ref(); // convert to &str
    if name.is_empty() {
        return Err(UpperError::NoName);
    }
    Ok(name.to_uppercase())
}

fn main() -> Result<(), UpperError> {
    let name = upper("sammy")?;
    println!("Uppercase name: {}", name);
    Ok(())
}

trait Iterator

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}
  • Abstrakti rajapinta asioille joilta voi pyytää seuraavan arvon
  • Option ilmaisee onko seuraavaa arvoa
  • Loppumaton laiska iterointi
  • Ei tule sekoittaa IntoIterator piirteen kanssa

// src/core/slice/mod.rs
impl<T> [T] {
    pub fn iter(&self) -> Iter<'_, T> {
        Iter::new(self)
    }
}
  • Mitä tyyppiä on &self?
#![allow(unused)]
fn main() {
let x = &[1, 4];
let mut iterator = x.iter();

assert_eq!(iterator.next(), Some(&1));
assert_eq!(iterator.next(), Some(&4));
assert_eq!(iterator.next(), None);
}
  • Iteraattori palauttaa &T arvoja
  • Miksi mut?

struct Iter (slice)

  • Iterin kenttiä ja implementaatiota ei tarvitse tietää
  • Implementoi Iterator piirteen


Muita Iter-nimisiä tietueita


std::iter::repeat

  • Repeat on Iterator
  • Palauttaa next() kutsuessa kloonin: Some(elt.clone())
pub fn repeat<T>(elt: T) -> Repeat<T>
where
    T: Clone,
#![allow(unused)]
fn main() {
// the number four 4ever:
let mut fours = std::iter::repeat(4);

assert_eq!(Some(4), fours.next());
assert_eq!(Some(4), fours.next());
assert_eq!(Some(4), fours.next());
assert_eq!(Some(4), fours.next());

// yup, still four
assert_eq!(Some(4), fours.next());
}

Iteraattoriadapterit

MetodiKuvaus
mapPalauttaa iteraattorin, jonka alkiot kuvataa λ-funktiolla
filterPalauttaa iteraattorin, jonka alkiot ovat ne jotka λ-funktio kuvaa trueksi
collectKuluttaa iteraattorin ja kerää arvot kokoelmaan
fn is_prime(x: &i32) -> bool { .. }

let a = [1, 2, 3, 4, 5, 6];

let mersenne_primes: Vec<i32> = a
    .iter()
    .map(|&x| 2i32.pow(x) - 1)
    .filter(is_prime)
    .collect();

assert_eq!(mersenne_primes, vec![3, 7, 31]);

Range iteraattori

fn is_prime(x: &i32) -> bool { .. }

let mut mersenne_primes = (1..)
    .map(|x| 2i32.pow(x) - 1)
    .filter(is_prime);

assert_eq!(mersenne_primes.next(), Some(3));
assert_eq!(mersenne_primes.next(), Some(7));
assert_eq!(mersenne_primes.next(), Some(31));
assert_eq!(mersenne_primes.next(), Some(127));
// ...
assert_eq!(mersenne_primes.next(), Some(618970019642690137449562111)

Kotiläksy

  • kirjasta 9.1, 9.2
  • https://desmondwillowbrook.github.io/blog/rust-error-handling/
    • Käy läpi piirteitä, Box<dyn Error>, serde, virheen propagointia


Cargo.toml
[dependencies]
serde_json = "1.0.81"

[dependencies.serde]
version = "1.0.137"
features = ["derive"]
lib.rs
fn read_file(path: &Path) ->
    Result<Vec<u8>, std::io::Error>
{
    let mut f = File::open(&path)?;
    let mut buf = Vec::new();
    f.read_to_end(&mut buf)?;
    Ok(buf)
}

serde

#[derive(Deserialize)]
pub struct Config {
  pub name: String
}

#[derive(Debug)]
pub enum ConfigError {
    FileError(io::Error),
    DeserializeError(serde_json::Error)
}

pub fn config_file(path: impl AsRef<Path>) ->
	Result<Config, ConfigError>
{
    let buf = read_file(path.as_ref())?;
    let conf = serde_json::from_slice(&buf)?;
    Ok(conf)
}

pub fn config_file<P: AsRef<Path>>(path: P) ->
	Result<Config, ConfigError> // ...

#[derive(Debug)]
pub enum ConfigError {
    FileError(io::Error),
    DeserializeError(serde_json::Error),
}

impl From<serde_json::Error> for ConfigError {
    fn from(e: serde_json::Error) -> Self {
        Self::DeserializeError(e)
    }
}

impl From<io::Error> for ConfigError {
    fn from(e: io::Error) -> Self {
        Self::FileError(e)
    }
}

main.rs:

use error_handling::config_file;

fn main () {
	let conf = config_file("config.json").unwrap();
	println!("{}", conf.name);
}

λ cargo run
thread 'main' panicked at
called Result::unwrap() on an Err value:
	FileError(Os {
		code: 2,
		kind: NotFound,
		message: "No such file or directory"
	})
	src/main.rs:4:43
run with RUST_BACKTRACE=1 to display a backtrace
  • tiedostoa config.json ei ole
  • virhe ei kerro tiedoston nimestä

use std::path::PathBuf;
#[derive(Debug)]
pub enum ConfigErrorType {
    FileError(std::io::Error),
    DeserializeError(serde_json::Error),
}

#[derive(Debug)]
pub struct ConfigError { // uusi
    pub path: PathBuf,
    pub err: ConfigErrorType,
}

Tehtävät

otarustlings

Viikon tehtävät tehdään otarustligs ohjelmaa käyttäen.

Tehtävät ovat kansiossa week5.

Varoitus: tehtävä 14-bst on haastava ja pitkä. Tekemällä tehtävän olet ansainnut yhden ilmaisen lounaan opettajan kanssa.

otarustlingsin voit päivittää komennolla:

cargo install --force otarustlings

Lue otarustlings ohje

Tätä varten tarvitse Rustin työkalut

Yhteenveto

Tällä viikolla opittiin lähdekoodin rakenteesta, tyyppien välisistä rajapinnoista (piirteet), virheenkäsittelystä geneerisen Result-enumin avulla.

Uusia asioita olivat:

  • moduulit
  • paketin konfiguraatio Cargo.toml
  • virheenkäsittely muissa (imperatiivisissa) kielissä
  • summatyypit virheenkäsittelyssä, Result, funktionaalinen virheenkäsittely, ?
  • virheiden propagointi ja "vahvan" tyyppijärjestelmän edut
  • kirjastot ohjelmien rinnalla
  • geneeriset, piirteet
  • trait Iterator, adapterit
  • trait boundien perusteet

Vanhoja asioita joita kerrattiin olivat:

  • () ja None ero muiden kielten null / nil / undefined arvoihin
  • Option virheenkäsittelyssä
  • esimerkkejä iteraattoreista
  • map

Viikkopalaute

Joka viikolla voi ja kannattaa täyttää palautelomake, jonka avulla opettajat voi päätellä mitä pitäisi opettaa enemmän

Anna palautetta miten sinulla meni tämän viikon aiheet

Esimerkkiratkaisut

01-impl

//! Your task is to add the `color` method to `TrafficLightColor`.

#[derive(Debug)]
enum TrafficLightColor {
    Red,
    Yellow,
    Green,
}

impl TrafficLightColor {
    pub fn color(&self) -> &str {
        match self {
            Self::Red => "red",
            Self::Yellow => "yellow",
            Self::Green => "green",
        }
    }
}

fn main() {
    let c = TrafficLightColor::Yellow;

    assert_eq!(c.color(), "yellow");

    println!("{:?}", c);
}

02-errors

#![allow(unused)]
fn main() {
//! Your task is to fix the `new` method.

#[derive(PartialEq, Debug)]
struct PositiveInteger(u64);

#[derive(PartialEq, Debug)]
enum CreationError {
    Negative,
    Zero,
}

impl PositiveInteger {
    /// Creates a [`PositiveInteger`] from an `i64`.
    ///
    /// # Errors
    ///
    /// - [`CreationError::Negative`] is returned if the `value` is negative.
    /// - [`CreationError::Zero`] is returned if the `value` is zero.
    fn new(value: i64) -> Result<PositiveInteger, CreationError> {
        if value < 0 {
            Err(CreationError::Negative)
        } else if value == 0 {
            Err(CreationError::Zero)
        } else {
            Ok(PositiveInteger(value as u64))
        }
    }
}

#[test]
fn test_creation() {
    assert!(PositiveInteger::new(10).is_ok());
    assert_eq!(Err(CreationError::Negative), PositiveInteger::new(-10));
    assert_eq!(Err(CreationError::Zero), PositiveInteger::new(0));
}
}

03-errors2

#![allow(unused)]
fn main() {
//! Your task is to fix the return type of `main` and change the "user input"
//! if you know what I mean...

use std::error;
use std::fmt;
use std::num::ParseIntError;

#[test]
fn _main() -> Result<(), Box<dyn error::Error>> {
    let pretend_user_input = "42";

    let x: i64 = pretend_user_input.parse()?;
    let s = PositiveNonzeroInteger::new(x)?;

    assert_eq!(PositiveNonzeroInteger(42), s);
    Ok(())
}

// Don't change anything below this line.

#[derive(PartialEq, Debug)]
struct PositiveNonzeroInteger(u64);

#[derive(PartialEq, Debug)]
enum CreationError {
    Negative,
    Zero,
}

impl PositiveNonzeroInteger {
    fn new(value: i64) -> Result<PositiveNonzeroInteger, CreationError> {
        match value {
            0 => Err(CreationError::Zero),
            1.. => Ok(PositiveNonzeroInteger(value as u64)),
            _ => Err(CreationError::Negative),
        }
    }
}

// This is required so that `CreationError` can implement `error::Error`.
impl fmt::Display for CreationError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let description = match *self {
            CreationError::Negative => "number is negative",
            CreationError::Zero => "number is zero",
        };
        f.write_str(description)
    }
}

impl error::Error for CreationError {}
}

04-monad

#![allow(unused)]
fn main() {
//! Your task is to rewrite `bad_multiply` without using `match` or `if left`.
//! Remember that `Option`s are monads, and monads are just monoids in the
//! category of endofunctors. Big deal ¯\_(ツ)¯\_

use std::num::ParseIntError;

fn bad_multiply(left: &str, right: &str) -> Result<i32, ParseIntError> {
    match left.parse::<i32>() {
        Ok(n1) => match right.parse::<i32>() {
            Ok(n2) => Ok(n1 * n2),
            Err(e) => Err(e),
        },
        Err(e) => Err(e),
    }
}

fn multiply(left: &str, right: &str) -> Result<i32, ParseIntError> {
    left.parse::<i32>()
        .and_then(|left| right.parse::<i32>().map(|right| left * right))
}

#[test]
fn _main() {
    // This still presents a reasonable answer.
    let twenty = multiply("10", "2");
    assert_eq!(twenty, Ok(20));

    // The following now provides a much more helpful error message.
    let not_twenty = multiply("ten", "2");
    assert!(matches!(not_twenty, Err(ParseIntError)));
}
}

05-errors3

#![allow(unused)]
fn main() {
//! Your task is to fill in the blanks. Read the documentation comments.
//!
//! Using catch-all error types like `Box<dyn error::Error>` isn't recommended
//! for library code, where callers might want to handle the errors. It is
//! possible to downcast the trait object, but is more cumbersome than matching
//! on an enum directly.

use std::num::ParseIntError;

#[derive(PartialEq, Debug)]
enum Error {
    /// A [`CreationError`] from creating the [`PositiveInteger`].
    Creation(CreationError),
    /// A [`ParseIntError`] from parsing the input string.
    ParseInt(ParseIntError),
}

/// Parses a string into a [`PositiveInteger`].
///
/// # Errors
///
/// - [`Error::ParseInt`] is returned if parsing `s` into an integer fails.
/// - [`Error::Creation`] is returned if creating the [`PositiveInteger`] fails.
fn parse_positive(s: impl AsRef<str>) -> Result<PositiveInteger, Error> {
    // Tip: Error::ParseInt is also a function: ___ -> Error
    let x: i64 = s.as_ref().parse().map_err(Error::ParseInt)?;
    PositiveInteger::new(x).map_err(Error::Creation)
}

// Don't change anything below this line.

#[derive(PartialEq, Debug)]
struct PositiveInteger(u64);

#[derive(PartialEq, Debug)]
enum CreationError {
    Negative,
    Zero,
}

impl PositiveInteger {
    fn new(value: i64) -> Result<PositiveInteger, CreationError> {
        match value {
            0 => Err(CreationError::Zero),
            1.. => Ok(PositiveInteger(value as u64)),
            _ => Err(CreationError::Negative),
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;
    #[test]
    fn test_parse_error() {
        assert!(matches!(
            parse_positive("not a number"),
            Err(Error::ParseInt(_))
        ));
    }
    #[test]
    fn test_negative() {
        assert_eq!(
            parse_positive("-555"),
            Err(Error::Creation(CreationError::Negative))
        );
    }
    #[test]
    fn test_zero() {
        assert_eq!(
            parse_positive("0"),
            Err(Error::Creation(CreationError::Zero))
        );
    }
    #[test]
    fn test_positive() {
        let x = PositiveInteger::new(42);
        assert!(x.is_ok());
        assert_eq!(parse_positive("42"), Ok(x.unwrap()));
    }
}
}

06-from-into

#![allow(unused)]
fn main() {
//! Your task is to implement the `From` trait for `Person` to make creating
//! Persons easier.
//!
//! The From trait is used for value-to-value conversions. When From is
//! implemented for a source type, the Into trait is automatically implemented
//! for the target type. You can read more about it at
//! https://doc.rust-lang.org/std/convert/trait.From.html
//!
//! As a side note, ignore the fact that it makes absolutely no sense that a
//! person has "default values". (This exercise is taken from Rustlings)

#[derive(Debug, PartialEq)]
struct Person {
    name: String,
    age: usize,
}

// We implement the `Default` trait to use it as a fallback when the provided
// string is not convertible into a Person object. Note that this is not good
// practice, one should use a `Result` rather.
impl Default for Person {
    fn default() -> Person {
        Person {
            name: String::from("John"),
            age: 30,
        }
    }
}

// Steps:
// 1. If the length of the provided string is 0, then return the default of
//    Person
// 2. Split the given string on the commas present in it
// 3. Extract the first element from the split operation and use it as the name
// 4. If the name is empty, then return the default of Person
// 5. Extract the other element from the split operation and parse it into a
//    `usize` as the age If while parsing the age, something goes wrong, then
//    return the default of Person Otherwise, then return an instantiated Person
//    object with the results
impl From<&str> for Person {
    fn from(string: &str) -> Self {
        let mut split = string.split(',');
        // Read first three elements; third one is used to check that there is no garbage
        let (name, age, extra) = (split.next(), split.next(), split.next());
        // Filter out empty names
        let name = name.filter(|name| name.len() > 0);
        // Filter out non-numerical values in age
        let age = age.and_then(|age| usize::from_str_radix(age, 10).ok());

        match (name, age, extra) {
            (Some(name), Some(age), None) => Person {
                name: name.to_string(),
                age,
            },
            _ => Person::default(),
        }
    }
}

// Do not edit anything below

#[test]
fn _main() {
    // Use the `from` associated function
    let p1 = Person::from("Mark,20");
    // Use the `into` method
    let p2: Person = "Gerald,70".into();

    println!("{:?}", p1);
    println!("{:?}", p2);
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn default() {
        // Test that the default person is 30 year old John
        let dp = Person::default();
        assert_eq!(dp.name, "John");
        assert_eq!(dp.age, 30);
    }
    #[test]
    fn empty_convert() {
        // Test that John is returned when an empty string is provided
        let p = Person::from("");
        assert_eq!(p, Person::default());
    }
    #[test]
    fn good_convert() {
        // Test that "Mark,20" works
        let p = Person::from("Mark,20");
        assert_eq!(p.name, "Mark");
        assert_eq!(p.age, 20);
    }
    #[test]
    fn bad_age() {
        // Test that "Mark,twenty" will return the default person due to an
        // error in parsing age
        let p = Person::from("Mark,twenty");
        assert_eq!(p, Person::default());
    }

    #[test]
    fn missing_comma_and_age() {
        let p = Person::from("Mark");
        assert_eq!(p, Person::default());
    }

    #[test]
    fn missing_age() {
        let p = Person::from("Mark,");
        assert_eq!(p, Person::default());
    }

    #[test]
    fn missing_name() {
        let p = Person::from(",1");
        assert_eq!(p, Person::default());
    }

    #[test]
    fn missing_name_and_age() {
        let p = Person::from(",");
        assert_eq!(p, Person::default());
    }

    #[test]
    fn missing_name_and_invalid_age() {
        let p = Person::from(",one");
        assert_eq!(p, Person::default());
    }

    #[test]
    fn trailing_comma() {
        let p = Person::from("Mike,32,");
        assert_eq!(p, Person::default());
    }

    #[test]
    fn trailing_comma_and_some_string() {
        let p = Person::from("Mike,32,man");
        assert_eq!(p, Person::default());
    }
}
}

07-errors4

#![allow(unused)]
fn main() {
//! Your task is to implement the missing conversions to `Error`.
//!
//! If the solution seems repetitive, the `thiserror` exercise will scratch that
//! itch.

use std::num::ParseIntError;
use std::str::FromStr;

#[derive(PartialEq, Debug)]
enum Error {
    /// A [`CreationError`] from creating the [`PositiveInteger`].
    Creation(CreationError),
    /// A [`ParseIntError`] from parsing the input string.
    ParseInt(ParseIntError),
}

// TODO impl From...
impl From<CreationError> for Error {
    fn from(e: CreationError) -> Self {
        Error::Creation(e)
    }
}

impl From<ParseIntError> for Error {
    fn from(e: ParseIntError) -> Self {
        Error::ParseInt(e)
    }
}

// Don't change anything below this line.

// Note: using `FromStr` rather than `From<&str>` is better if the parsing can
// fail. `from_str` is rarely used directly, but it's counterpart `str::parse`
// uses it in the background. Read more about `FromStr`:
// https://doc.rust-lang.org/std/str/trait.FromStr.html
impl FromStr for PositiveNonzeroInteger {
    type Err = Error;
    fn from_str(s: &str) -> Result<PositiveNonzeroInteger, Self::Err> {
        let x: i64 = s.parse()?;
        Ok(PositiveNonzeroInteger::new(x)?)
    }
}

#[derive(PartialEq, Debug)]
struct PositiveNonzeroInteger(u64);

#[derive(PartialEq, Debug)]
enum CreationError {
    Negative,
    Zero,
}

impl PositiveNonzeroInteger {
    fn new(value: i64) -> Result<PositiveNonzeroInteger, CreationError> {
        match value {
            0 => Err(CreationError::Zero),
            1.. => Ok(PositiveNonzeroInteger(value as u64)),
            _ => Err(CreationError::Negative),
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;
    #[test]
    fn test_parse_error() {
        assert!(matches!(
            PositiveNonzeroInteger::from_str("not a number"),
            Err(Error::ParseInt(_))
        ));
    }
    #[test]
    fn test_negative() {
        assert_eq!(
            PositiveNonzeroInteger::from_str("-555"),
            Err(Error::Creation(CreationError::Negative))
        );
    }
    #[test]
    fn test_zero() {
        assert_eq!(
            PositiveNonzeroInteger::from_str("0"),
            Err(Error::Creation(CreationError::Zero))
        );
    }
    #[test]
    fn test_positive() {
        let x = PositiveNonzeroInteger::new(42);
        assert!(x.is_ok());
        assert_eq!(PositiveNonzeroInteger::from_str("42").unwrap(), x.unwrap());
    }
}
}

08-thiserror

Cargo.toml

[package]
name = "week5_thiserror"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
thiserror = "1.0.37"

`main.rs

//! Your task is to implement the conversions to `Error` by deriving
//! `thiserror::Error`. You don't need to add any impls.
//!
//! - Step one: add `thiserror` as a dependency
//! - Step two: read how to use `thiserror`
//!   https://docs.rs/thiserror/latest/thiserror/ and fix the code.

use std::num::ParseIntError;
use std::str::FromStr;
use thiserror::Error;

#[derive(PartialEq, Debug, Error)]
enum Error {
    /// A [`CreationError`] from creating the [`PositiveInteger`].
    #[error("creating PositiveInteger failed: {0}")]
    Creation(#[from] CreationError),
    /// A [`ParseIntError`] from parsing the input string.
    #[error("parsing failed: {0}")]
    ParseInt(#[from] ParseIntError),
}

#[derive(PartialEq, Debug, Error)]
enum CreationError {
    #[error("number is negative")]
    Negative,
    #[error("number is zero")]
    Zero,
}

// Don't change anything below this line.

impl FromStr for PositiveNonzeroInteger {
    type Err = Error;
    fn from_str(s: &str) -> Result<PositiveNonzeroInteger, Self::Err> {
        let x: i64 = s.parse()?;
        Ok(PositiveNonzeroInteger::new(x)?)
    }
}

#[derive(PartialEq, Debug)]
struct PositiveNonzeroInteger(u64);

impl PositiveNonzeroInteger {
    fn new(value: i64) -> Result<PositiveNonzeroInteger, CreationError> {
        match value {
            0 => Err(CreationError::Zero),
            1.. => Ok(PositiveNonzeroInteger(value as u64)),
            _ => Err(CreationError::Negative),
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;
    #[test]
    fn test_parse_error() {
        let res = PositiveNonzeroInteger::from_str("not a number");
        assert!(matches!(res, Err(Error::ParseInt(_))));
        assert_eq!(
            format!("{}", res.unwrap_err()),
            "parsing failed: invalid digit found in string"
        );
    }
    #[test]
    fn test_negative() {
        let res = PositiveNonzeroInteger::from_str("-555");
        assert_eq!(res, Err(Error::Creation(CreationError::Negative)));
        assert_eq!(
            format!("{}", res.unwrap_err()),
            "creating PositiveInteger failed: number is negative"
        );
    }
    #[test]
    fn test_zero() {
        let res = PositiveNonzeroInteger::from_str("0");
        assert_eq!(res, Err(Error::Creation(CreationError::Zero)));
        assert_eq!(
            format!("{}", res.unwrap_err()),
            "creating PositiveInteger failed: number is zero"
        );
    }
    #[test]
    fn test_positive() {
        let x = PositiveNonzeroInteger::new(42);
        assert!(x.is_ok());
        assert_eq!(PositiveNonzeroInteger::from_str("42").unwrap(), x.unwrap());
    }
}

09-semver

#![allow(unused)]
fn main() {
//! Your task is to define the struct Semver and implement PartialEq and
//! PartialOrd for it. You also need to implement FromStr for Semver for
//! convenience.

use std::cmp::Ordering;
use std::num::ParseIntError;
use std::str::FromStr;

// You are not allowed to derive more traits
#[derive(Debug)]
struct Semver {
    major: u32,
    minor: u32,
    patch: u32,
}

impl PartialEq for Semver {
    fn eq(&self, other: &Self) -> bool {
        self.major == other.major && self.minor == other.minor && self.patch == other.patch
    }
}

impl PartialOrd for Semver {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        let ord = self
            .major
            .cmp(&other.major)
            .then(self.minor.cmp(&other.minor))
            .then(self.patch.cmp(&other.patch));
        Some(ord)
    }
}

impl Semver {
    pub fn new(major: u32, minor: u32, patch: u32) -> Self {
        Self {
            major,
            minor,
            patch,
        }
    }

    pub fn is_stable(&self) -> bool {
        self.major > 0
    }

    pub fn breaking(&self) -> Self {
        if self.is_stable() {
            Self::new(self.major + 1, 0, 0)
        } else {
            Self::new(0, self.minor + 1, 0)
        }
    }

    pub fn bump(&self) -> Self {
        if self.is_stable() {
            Self::new(self.major, self.minor + 1, 0)
        } else {
            Self::new(0, self.minor, self.patch + 1)
        }
    }
}

impl FromStr for Semver {
    type Err = ParseIntError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        // Collecting Results into a Result catches the first Err
        let mut versions: Result<Vec<_>, _> =
            s.split('.').map(|v| u32::from_str_radix(v, 10)).collect();
        let versions = versions?;
        match &versions[..] {
            &[major, minor, patch] => Ok(Self::new(major, minor, patch)),
            // As there is no custom error for this, we will panic
            _ => panic!("wrong number of version numbers"),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn from_str() {
        let error = "a.0.0".parse::<Semver>().unwrap_err();

        assert_eq!(error.kind(), &std::num::IntErrorKind::InvalidDigit);

        let error = "0.1.c".parse::<Semver>().unwrap_err();

        assert_eq!(error.kind(), &std::num::IntErrorKind::InvalidDigit);

        let error = "0.1.4294967296".parse::<Semver>().unwrap_err();

        assert_eq!(error.kind(), &std::num::IntErrorKind::PosOverflow);
    }

    #[test]
    fn is_stable() {
        let version1: Semver = "1.61.0".parse().unwrap();
        let version2: Semver = "0.10.5".parse().unwrap();

        assert!(version1.is_stable());
        assert!(!version2.is_stable());
    }

    #[test]
    fn major_comparison() {
        let version1: Semver = "1.61.0".parse().unwrap();
        let version2: Semver = "2.0.5".parse().unwrap();

        assert!(version1 < version2);
    }

    #[test]
    fn minor_comparison() {
        let version1: Semver = "1.0.5".parse().unwrap();
        let version2: Semver = "1.61.0".parse().unwrap();

        assert!(version1 < version2);
    }

    #[test]
    fn patch_comparison() {
        let version1: Semver = "1.9.5".parse().unwrap();
        let version2: Semver = "1.9.15".parse().unwrap();

        assert!(version1 < version2);
    }

    #[test]
    fn breaking_change() {
        let version: Semver = "1.61.0".parse().unwrap();
        let next = version.breaking();

        assert_eq!(next, "2.0.0".parse().unwrap());

        let version: Semver = "0.2.0".parse().unwrap();
        let next = version.breaking();

        assert_eq!(next, "0.3.0".parse().unwrap());
    }

    #[test]
    fn minor_bump() {
        let version: Semver = "1.61.0".parse().unwrap();
        let next = version.bump();

        assert_eq!(next, "1.62.0".parse().unwrap());

        let version: Semver = "0.2.0".parse().unwrap();
        let next = version.bump();

        assert_eq!(next, "0.2.1".parse().unwrap());
    }
}
}

10-traits

#![allow(unused)]
fn main() {
//! Your task is to implement vector addition and scaling using built-in
//! operators `+` and `*`.
//!
//! See https://doc.rust-lang.org/std/ops/trait.Add.html and
//! https://doc.rust-lang.org/std/ops/trait.Mul.html for reference.

use std::ops::{Add, Mul};

#[derive(Debug, PartialEq)]
struct Vec3 {
    x: f32,
    y: f32,
    z: f32,
}

impl Vec3 {
    fn new(x: f32, y: f32, z: f32) -> Self {
        Self { x, y, z }
    }
}

impl Add<Vec3> for Vec3 {
    type Output = Vec3;

    fn add(self, rhs: Vec3) -> Self::Output {
        Self::Output {
            x: self.x + rhs.x,
            y: self.y + rhs.y,
            z: self.z + rhs.z,
        }
    }
}

impl Mul<f32> for Vec3 {
    type Output = Vec3;

    fn mul(self, rhs: f32) -> Self::Output {
        Self::Output {
            x: self.x * rhs,
            y: self.y * rhs,
            z: self.z * rhs,
        }
    }
}

impl Mul<Vec3> for f32 {
    type Output = Vec3;

    fn mul(self, rhs: Vec3) -> Self::Output {
        Self::Output {
            x: self * rhs.x,
            y: self * rhs.y,
            z: self * rhs.z,
        }
    }
}

#[test]
fn add() {
    assert_eq!(
        Vec3::new(1.0, 2.0, -1.0) + Vec3::new(3.0, -2.0, 2.0),
        Vec3::new(4.0, 0.0, 1.0)
    );
}

#[test]
fn mul() {
    assert_eq!(2.0 * Vec3::new(3.0, -2.0, 2.0), Vec3::new(6.0, -4.0, 4.0));

    assert_eq!(Vec3::new(3.0, -2.0, 2.0) * 2.0, Vec3::new(6.0, -4.0, 4.0));
}
}

11-recursive_types

#![allow(unused)]
fn main() {
//! Your task is to implement the methods for `Crate`.

use std::{iter::Sum, ops::Add};

#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
struct Kilograms(f32);

impl From<f32> for Kilograms {
    fn from(kilos: f32) -> Self {
        Self(kilos)
    }
}

impl From<i32> for Kilograms {
    fn from(kilos: i32) -> Self {
        Self(kilos as f32)
    }
}

impl Add<Self> for Kilograms {
    type Output = Self;

    fn add(self, rhs: Self) -> Self::Output {
        Kilograms::from(self.0 + rhs.0)
    }
}

impl Sum for Kilograms {
    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
        iter.fold(0.0.into(), |acc, next| acc + next)
    }
}

#[derive(Debug, Clone, PartialEq, PartialOrd)]
enum Crate {
    Empty,
    WithStuff(Kilograms),
    Multiple(Vec<Self>),
}

impl Default for Crate {
    fn default() -> Self {
        Self::Empty
    }
}

#[derive(Debug, Clone, PartialEq)]
enum Error {
    /// Tried to fill a crate with stuff but there were other crates inside
    FillMultiple,
    /// Tried to unbox an empty crate
    EmptyCrate,
}

impl Crate {
    fn new() -> Self {
        Self::default()
    }

    /// Returns the total mass of the crate's contents.
    fn mass(&self) -> Kilograms {
        match self {
            Self::Empty => Kilograms(0.0),
            Self::WithStuff(mass) => *mass,
            Self::Multiple(crates) => crates.iter().map(|c| c.mass()).sum(),
        }
    }

    /// Fills the crate with `mass` kilograms of _stuff_.
    ///
    /// # Errors
    ///
    /// If the crate contains multiple crates there's no way to fill it and an
    /// [`Error::FillMultiple`] is returned.
    fn fill(&mut self, mass: Kilograms) -> Result<(), Error> {
        match self {
            Self::Empty => {
                *self = Self::WithStuff(mass);
                Ok(())
            }
            Self::WithStuff(old) => {
                *self = Self::WithStuff(*old + mass);
                Ok(())
            }
            Self::Multiple(_) => Err(Error::FillMultiple),
        }
    }

    /// Unboxes the crate returing the mass of all the _stuff_. Every unboxed
    /// crate becomes empty and stays in their parent crate. If the crate
    /// contains empty crates and no _stuff_ the unboxing is successful.
    ///
    /// # Errors
    ///
    /// If the top-most crate is empty an [`Error::EmptyCrate`] is returned.
    fn unbox(&mut self) -> Result<Kilograms, Error> {
        match self {
            Self::Empty => Err(Error::EmptyCrate),
            Self::WithStuff(mass) => {
                let mass = *mass; // Create a copy
                *self = Self::Empty;
                Ok(mass)
            }
            Self::Multiple(crates) => {
                let total_mass = crates
                    .into_iter()
                    .filter_map(|c| c.unbox().ok())
                    .sum::<Kilograms>();
                Ok(total_mass)
            }
        }
    }

    /// Inserts an `other` crate inside `self`. If the crate has _stuff_ inside,
    /// its mass is returned. If the crate has other crates inside, the `other`
    /// crate just joins them.
    fn insert(&mut self, other: Self) -> Option<Kilograms> {
        match self {
            Self::Empty => {
                *self = Self::Multiple(vec![other]);
                None
            }
            Self::WithStuff(mass) => {
                let mass = *mass; // Create a copy
                *self = other;
                Some(mass)
            }
            Self::Multiple(crates) => {
                crates.push(other);
                None
            }
        }
    }
}

#[test]
fn mass() {
    let heavy_crate = Crate::WithStuff(12.into());
    let loota = Crate::Multiple(vec![
        heavy_crate.clone(),
        Crate::Multiple(vec![heavy_crate.clone(), Crate::new()]),
    ]);

    assert_eq!(loota.mass(), 24.into());
}

#[test]
fn fill() {
    let mut loota = Crate::Empty;
    loota.fill(5.into());
    assert_eq!(loota.mass(), 5.into());

    let mut loota = Crate::WithStuff(2.into());
    loota.fill(5.into());
    assert_eq!(loota.mass(), 7.into());
}

#[test]
fn unbox() {
    let mut loota = Crate::Empty;
    assert_eq!(loota.unbox(), Err(Error::EmptyCrate));
    assert_eq!(loota, Crate::Empty);

    let mut loota = Crate::Multiple(vec![Crate::Empty, Crate::Multiple(vec![Crate::Empty])]);
    assert_eq!(loota.unbox(), Ok(0.into()));

    let iso_loota = Crate::WithStuff(20.into());
    let mut loota = Crate::Multiple(vec![
        iso_loota.clone(),
        Crate::Multiple(vec![iso_loota.clone(), Crate::new()]),
        iso_loota.clone(),
        Crate::new(),
    ]);
    assert_eq!(loota.unbox(), Ok(60.into()));
    assert_eq!(
        loota,
        Crate::Multiple(vec![
            Crate::Empty,
            Crate::Multiple(vec![Crate::Empty, Crate::Empty]),
            Crate::Empty,
            Crate::Empty
        ])
    );
}

#[test]
fn insert() {
    let pieni_loota = Crate::WithStuff(2.into());
    let iso_loota = Crate::Multiple(vec![
        pieni_loota.clone(),
        Crate::Multiple(vec![pieni_loota.clone(), Crate::new()]),
        pieni_loota.clone(),
    ]);

    let mut loota = Crate::Empty;
    assert_eq!(loota.insert(iso_loota.clone()), None);
    assert_eq!(loota, Crate::Multiple(vec![iso_loota.clone()]));

    let mut loota = Crate::Multiple(vec![
        pieni_loota.clone(),
        Crate::Multiple(vec![Crate::Empty]),
    ]);
    assert_eq!(loota.insert(iso_loota.clone()), None);
    assert_eq!(loota.mass(), 8.into());

    let mut loota = Crate::WithStuff(10.into());
    assert_eq!(loota.insert(iso_loota), Some(10.into()));
    assert_eq!(loota.mass(), 6.into());
}
}

12-lifetimes1

#![allow(unused)]
fn main() {
//! Your task is to fix the lifetime errors. Read the chapter about lifetimes:
//! https://doc.rust-lang.org/stable/book/ch10-03-lifetime-syntax.html

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn always_first<'a>(x: &'a str, y: &'_ str) -> &'a str {
    x
}

// Do not edit anything below

#[test]
fn _main() {
    let s1 = "static string literal";
    {
        let s2 = "not static".to_string();
        let longest = longest(s1, &s2);

        assert_eq!(longest, "static string literal");
    };
    // longest could not live here as it cannot outlive s2 regardless of which
    // string is longer

    let first = {
        let s2 = "not static".to_string();
        always_first(s1, &s2)
    };

    // first should be able to live here as it ignores the lifetime of the
    // second argument
    assert_eq!(first, "static string literal");
}
}

13-as_ref_mut

#![allow(unused)]
fn main() {
//! Your task is to change the functions `byte_counter` and
//! `capitalize_first_word` to support different types of (be generic over)
//! references. More specifically you need to support a `&String` in place of a
//! `&str` and a `&mut String` in place of a `&mut str`.
//!
//! The traits AsRef and AsMut allow for cheap reference-to-reference (compared
//! to From/Into value-to-value) conversions. Read more about them at
//! - https://doc.rust-lang.org/std/convert/trait.AsRef.html
//! - https://doc.rust-lang.org/std/convert/trait.AsMut.html

/// Obtain the number of bytes (not characters) in the given argument
fn byte_counter(s: impl AsRef<str>) -> usize {
    let s = s.as_ref();
    s.as_bytes().len()
}

/// Capitalize first word in a sentence
fn capitalize_first_word(mut s: impl AsMut<str>) {
    let s = s.as_mut();
    let i = s.find(' ').unwrap_or(s.len());
    s[..i].make_ascii_uppercase();
}

// Don't change the tests!
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn bytes() {
        // é takes two bytes
        let s = "Café au lait";
        assert_eq!(byte_counter(s), 13);

        let s = s.to_string();
        assert_eq!(byte_counter(s), 13);
    }

    #[test]
    fn capitalize_word() {
        let mut s = "word".to_string();
        capitalize_first_word(&mut s);
        assert_eq!(s, "WORD");
    }

    #[test]
    fn capitalize_ascii() {
        let mut s = "Cafe au lait".to_string();
        let sref: &mut str = s.as_mut_str();
        capitalize_first_word(sref);
        assert_eq!(s, "CAFE au lait");
    }

    #[test]
    fn capitalize_ascii_only() {
        let mut s = "Caffè latte".to_string();
        capitalize_first_word(&mut s);
        assert_eq!(s, "CAFFè latte");
    }
}
}

14-bst

Saat palautteen tehtävästä palauttamalla sen opettajalle.

Kurssikalenteri PDF

Lisämateriaali

Virallinen Rust-kirja

https://doc.rust-lang.org/book/

The Rust Programming Language, tunnetaan myös nimellä The Book, on Steve Klabnikin ja Carol Nicholsin kirjoittama Rustin virallinen opetusmateriaali, jota käytetään tällä kurssilla ekstensiivisesti.

Kirjassa lukee, että sinun tulisi käyttää editiota 2018, mutta editio 2021 toimii yhtä hyvin

Operaattorien ja symbolien syntaksi

https://doc.rust-lang.org/book/appendix-02-operators.html

Jos huomaat lukiessasi tuntemattomia symboleita, kuten & tai @, tältä sivulta löydät niiden nimet.

Kannattaa myös kokeilla interaktiivista explaine.rs syntaksinselitysjärjestelmää.

Rustia esimerkeillä

https://doc.rust-lang.org/stable/rust-by-example/

Rust By Example käy kirjan aiheita läpi enemmän-koodia-vähemmän-selitystä ajatuksella.

RBE:n kappaleita kannattaakin silmäillä ennen kirjan kappaleita, jotta saa paremman yleiskäsityksen Rustista.

Rustlings-harjoitukset

https://github.com/rust-lang/rustlings/

Rustlings on kokoelma lyhyitä Rust-tehtäviä, joissa keskitytään enemmän toistoon ja tekniikkaan kuin ongelmanratkaisuun. Rustlingsit on suunniteltu tehtäväksi kirjan lukemisen myötä, ja niihin ei aina löydy yksiselitteistä ratkaisua.

Tehtäviä on noin 80 ja niiden tekemiseen menee vähintään monta päivää, jopa kokeneella koodarilla.

Osa Rustlings-harjoituksista tulee vastaan kurssilla

Keittokirja

https://rust-lang-nursery.github.io/rust-cookbook/

Keittokirjasta löytyy hyödyllisiä koodinpätkiä.

Rustin verkkosivut

https://www.rust-lang.org/learn

Huijauslehdet

cheats.rs

Hyvin yksityiskohtainen ja yleiskäyttöinen cheatsheet.

Programming-Idioms huijauslehti

Inferiorimpi huijauslehti.

Container cheat sheet

Visualisaatio Rustin säiliöiden muistirakenteesta.

Memory Container Cheat-sheet

Apu oikean säiliön valitsemiseen.

explaine.rs

Interaktiivinen syntaksin selitys.

Suunnittelumalleja

https://rust-unofficial.github.io/patterns/patterns/index.html

Geneerisiä ja uudelleenkäytettäviä idiomaattisia ratkaisumalleja ongelmiin Rustissa. Suunnittelumallit auttavat kirjoittamaan luettavaa ja ylläpidettävää idiomaattista koodia, joka toimii.

Lue lisää suunnittelumalleista

Vielä lisää idiomaattista chokoa

rust.godbolt.org

Haluaisitko mieluummin lukea assemblyä tai Mid-level Intermediate Representation koodia?

Rustonomicon, Rustin "pimeän taiteen" kirja

https://doc.rust-lang.org/nomicon/index.html

Rustonomicon on tarkoitettu ohjelmoijille, jotka ovat lukeneet kirjan ja etsivät syvempää ymmärrystä omistajuudesta ja muistinhallinnasta Rustissa.

Muita tietolähteitä

Rustilla kehitettyjä ohjelmia

OhjelmaKuvaus
alacrittyGPU-kiihdytetty pääte-emulaattori
batcat(1)-klooni, jossa on syntaksin korostus ja git-integraatio
exaParempi ls(1)
rustRustin kääntäjä rustc ja standardikirjasto ovat kirjoitettu rustilla

Roopen UB esityksen diat 14.11.2022

PDF

Kurssiprojekti

Kurssiprojektin tarkoitus on mahdollistaa luovuuden maksimaalinen käyttö, jotta Rustin (ja ohjelmoinnin) oppiminen olisi mahdollisimman luontevaa.

Projektin laajuus riippuu täysin opiskelijan taitotasosta ja käytössä olevasta ajasta.

Oma idea (suositeltu)

Kaikista paras projekti on sellainen, jolle opiskelijalla on tarve, kuten pieni ohjelma tai skripti/automaatio.

Paras projekti palkitaan!

Jos sellaista ei ole mielessä, voit lukea seuraavia ideoita:

Valmiit projekti-ideat

Peli

Rustissa on muutama suosittu pelikehys:

Lisää tietoa

Komentorivipohjaista peliä varten on console_engine

Lisp-projekti

Tee oma Lisp-tulkki. Tutoriaali: https://vishpat.github.io/lisp-rs/overview.html

Esimerkki Lisp-koodista:

(
	(define m 10)
	(define n 12)
	(define K 100)
	
	(define func1 (lambda (x) (+ x K)))
	(define func2 (lambda (x) (- x K)))
	
	(func1 m)
	(func2 n)
)

Koulun ruokalista työkalu

Ohjelma, joka näyttää viikon ruokalistan REST APIa käyttäen

Mini-redis

Käyttäen tokio-asynkronista runtimeä

https://tokio.rs/tokio/tutorial

IO-projekti kirjasta

Pieni grep implementaatio (komentorivityökalu)

https://doc.rust-lang.org/stable/book/ch12-00-an-io-project.html

Loppuprojekti kirjasta (vaikea)

Monisäikeinen verkkopalvelin

https://doc.rust-lang.org/stable/book/ch20-00-final-project-a-web-server.html

Projektipohjat

Matopeli yewillä

TODO

Muut projekteiksi käyvät vaihtoehdot

Sanasto

Tällä sivulta löytyy kurssin aikana käytettyä Rust- ja ohjelmistosanastoa, ja sanojen lyhyet selitykset. Osa suomenkielisistä termeistä ei ole vakituisia. Selitykset eivät ole täsmällisiä, vaan ne ovat kirjoitettu tämän kurssin näkökulmasta. Termeillä voi olla muutakin merkitystä, kuin mitä on tänne kirjoitettu.

Viikko 1

TermiSelitys
rustMozilla Foundationin kehittämä staattisesti sekä dynaamisesti tyypitetty olio-orientoitu imperatiivinen funktionaalinen ekspressiivinen korkean tason turvallinen järjestelmäohjelmointikieli.
cargoRustin kääntö- ja paketinhallintajärjestelmä. Cargo käyttää rustc kääntämään lähdekoodit.
rustupKomentorivityökalu Rustin ja Cargon asentamiselle.
rustcRustin kääntäjä.
kääntäjä (compiler)Ohjelma, joka "kääntää" lähdekoodin konekieleksi. Kääntäjä voi optimoida (esim. nopeuttaa) koodia tekemällä tiettyjä muutoksia kääntövaiheessa. Rust-kääntäjä kääntää lähdekoodin abstraktiksi syntaksipuuksi, joka jatkaa matkaa erilaisten representaatioiden kautta konekieleksi tai käännösvirheeksi. Rust-kääntäjä on kirjoitettu Rustilla ja käyttää konekielen generoinnissa LLVM-järjestelmää. Lue lisää.
konekieli (machine code)Tietokoneohjelma koostuu listasta konekielisiä käskyjä. Konekieli on binäärikäskyjä, jotka suoraan ohjaavat prosessoria. Rustia voi kääntää esim. x86_64-konekielelle, mutta myös esim. WebAssembly-tavukoodiksi.
kirjasto (library)Kirjasto on kokoelma koodia, jonka yhdistää yhteinen toiminnallisuus ja käyttötarkoitus. Kirjastot on modulaarisia ja laajennettavia, ja usein käyttävät toisia kirjastoja. Kirjastoissa ei yleensä ole main-funktiota, eikä niitä pysty suorittamaan.
ohjelma (binary)Ohjelma on kokoelma koodia, ja sisältää main-funktion, joka on suorituksen lähtöpiste. Ohjelmat yleensä sisältävät vain käyttöliittymäkoodin ja käyttävät samassa paketissa olevia kirjaston ominaisuuksia. Tietojenkäsittelytehtävän esitys joukkona tietokoneen toteutettavaksi tarkoitettuja toimenpiteitä.
ohjelmointikieli (programming language)Ohjelmien laatimiseen ja esittämiseen tarkoitettu toimikieli, joka on muunnettavissa konekielelle. Esimerkejä ohjelmoinikielistä on C, C++, Rust.
laatikko (crate)Laatikko voi olla ohjelma tai kirjasto.
paketti (package)Paketti sisältää laatikoita. Paketin voi julkaista esim crates.io-sivulle, Rustin viralliseen pakettirekesteriin. crates.io on väärin nimetty listaus paketeista (pitäis olla packages.io).
TOMLTom's Obvious Minimal Language on konfiguraatio- ja serialisointiformaatti, jota on helppo kirjoittaa toisin kuin JSON.
editio (edition)Rustin yksi pääominaisuuksista, joka mahdollistaa yhteensopivuuden rikkomisen kolmen vuoden välein, mutta ei estä vanhan koodin kääntymistä. Editio määritellään Cargo.toml-tiedostossa, ja editioita on 3: 2015 (oletus), 2018 ja 2021. Rust-kääntäjän uusin versio aina tukee vanhintakin editiota 2015. Vertaa Python-ekosysteemin hajaantumista kahteen yhteensopimattomaan versioon 2 ja 3.
avoin lähdekoodi (open source)Mahdollistaa ohjelmiston lähdekoodin kopioimisen, muokkaamisen ja levittämisen. Isot avoimen lähdekoodin projektit, kuten GNU/Linux-pohjaiset distribuutiot, usein haarautuu moneksi eri projektiksi. Ilman avoimen lähdekoodin lisenssiä vapaasti internetistä ladattava lähdekoodi ei ole avointa lähdekoodia tekijänoikeuksien nojalla. Rust-ekosysteemin työkalut ovat avointa lähdekoodia.
käyttäjänoikeus (copyleft)Käyttäjänoikeuslisenssi on rajoittavampi avoimen lähdekoodin muoto, joka varmistaa ohjelmiston kehityksen pysyvän vapaana, eli estää kehityksen siirtymisen omisteisen lisenssin alle. Esimerkki käyttäjänoikeuslisenssistä on GPL, joka vaatii saman lisenssin käyttämistä kaikissa johdannaisissa teoksissa. Rust-ekosysteemin käyttämät lisenssit Apache ja MIT eivät ole käyttäjänoikeuslisenssejä.
syntaksi (syntax)Syntaksi määrää miten symbolit, kuten avainsanat, voivat esiintyä ohjelmassa. Syntaksivirhe tapahtuu, kun kääntäjä törmää virheelliseen tai virheellisessä paikassa olevaan symboliin jäsentäessä lähdekoodia. Esimerkiksi muuttujan nimen väärinkirjoittaminen ei ole syntaksivirhe vaan semanttinen virhe.
muuttuja (variable)Yleensä kuitenkin vakio (constant)
xburgerKuuluisa ravintola Otaniemessä

Viikko 2

TermiSelitys
abstraktio (abstraction)Abstraktio jostain konseptista auttaa ihmistä käsittämään ja käsittelemään konseptia korkeammalla ajattelun tasolla. Esimerkiksi luku voi olla abstraktio tietokoneen prosessorissa olevista jännitteistä tai Option<T>-enumeraatio on abstraktio vaihtoehtoisesta arvosta. Erittäin abstrakti makro println! piilottaa koodaajalta täysin I/O operaatiot ja käyttöjärjestelmän yksityiskohdat.
funktio (function)Funktiota voi ajatella abstraktiona tietyn tyyppisen datan algoritmisestä manipuloinnista, jossa data voi olla mitä vain (kunhan tyyppi on oikea).
argumentti (argument)Argumentilla tarkoitetaan dataa joka annetaan funktiolle sitä kutsuttaessa. Esimerkiksi lausekkeessa Some(5) kutsutaan funktiota Option::Some, jolle annetaa argumentiksi 5. Ei tule sekoittaa parametriin.
parametri (parameter)Parametrit ovat funktion muuttujia, jotka saavat arvokseen funktiokutsun argumentit. Esimerkiksi funktiodeklaraatiossa fn frobnify(target: Foo) on yksi parametri target tyyppiä Foo.
metodi (method)Metodi on tiettyyn tyyppiin kiinnitetty funktio. Metodit on kätevä tapa organisoida koodia/funktioita niin että ne ovat helposti löydettävissä. Metodin kutsumisen voi tehdä .-syntaksilla tai ilman. Esimerkiksi metodikutsu cat.meow() on sama kuin Cat::meow(&cat). Metodit määritellään impl-lohkoilla.
lauseke (expression)Lauseke on osa koodia, joka on vakio, muuttuja tai jokin yhdistelmä operaatioita. Kun lauseke suoritetaan, tulokseksi saadaan arvo. Lausekkeet koostuu usein toisista lausekkeista. Esimerkiksi lauseke total.sum() / total.len() koostuu kahdesta lausekkeesta total.sum() ja total.len() jotka suoritetaan ennen jakolaskua. Rustissa myös ohjausrakenteet, kuten if, for ja loop, ovat lausekkeita ja ne tuottaa arvoja.
käsky (statement)Käskyt ovat yleensä kaikki koodinpätkät jotka loppuvat puolipisteeseen ;. Esimerkiksi let-käskyllä määritellään muuttuja ja fn-käskyllä funktio. Käskyt usein sisältävät lausekkeita. Esimerkiksi käsky cat.meow(); kutsuu cat ilmentymälle metodia meow ja unohtaa palautetun arvon.
muuttumattomuus (immutability)
ohjelmointivirhe (software bug)Ohjelmointivirhe johtuu väärillä oletuksilla kirjoitetun koodin väärästä toiminnasta. Rust-kääntäjä estää jotkin semanttiset virheet, kuten muistinhallintavirheet, mutta ei virheitä esimerkiksi kilpailutilanteissa ja bisneslogiikassa.

Viikko 3

TermiSelitys
tyyppi (type)Kaikella tiedolla on tyyppi, joka määrittelee ja rajoittaa missä muodossa tieto esiintyy. Samantyyppistä tietoa voi syöttää samalle funktiolle. Omia tyyppejä voi määritellä tekemällä tietueita tai enumeraatiotita. Vaikka kaksi tyyppiä olisivat identtisiä, niitä ei voi sekoittaa.
tietue (struct)Tietue koostuu kentistä (field), jotka sisältävät jonkin tyyppistä dataa. Tietue koostuu vain tietoa ja määrittelee mitä tyyppiä tämä tieto on. Tietue voi myös olla tuple, tällaista tietuetta kutsutaan tuple structiksi.
enumeraatio (enumeration)Enumeraatio on summatyyppi eri tyypeistä. Enumeraatio koostuu varianteista, jotka voivat sisältää eri tyyppejä. Enumeraation instanssi voi olla vain yksi variantti kerrallaan. Option on yksi tärkeimmistä enumeraatioista, jonka variantteja ovat None ja Some(T).
ilmentymä (instance)Ilmentymä on jonkin tyyppistä tietoa, joka esiintyy suorituksen aikana muistissa. Ilmentymä luodaan aina tyypin, kuten tietueen tai enumeraation mukaisesti. Esim koodissa let kana = Kana::new(), kana on ilmentymä tyypistä Kana. Rustissa kaikki ilmentymät ovat oliota, koska Rust on olioperusteinen (object-oriented) kieli.
olio (object)Olio kapseloi tiedon ja toiminnallisuuden yksityiskohdat yhtenäiseksi korkeamman tason kappaleeksi. Rustissa ei ole luokkia eikä niiden välistä periytymistä. Tyypeille voi määritellä piirteitä (trait).
piirre (trait)Eri piirteet määrittelevät erilaisen käytöksen rajapinnan. Piirteitä käytetään yhtenäistämään eri tyyppien yhteistä toiminnallisuutta. Piirteissä pystytään määrittelemään ainoastaan assosioituja funtkioita, mutta ei esimerkiksi kenttiä. Piirteet voivat periytyä toisista piirteistä, kuten luokat muissa olioperusteisissa kielissä.
kapselointi (encapsulation)Yhteen kuuluvien tietojen ja toimintojen kokoaminen yhdeksi kokonaisuudeksi. Olioperusteisessa ohjelmoinnissa olio on tieto ja toiminnnallisuus enkapsuloitu yhdeksi. Ei tee koodista turvallisempaa, mutta ilmaisee tarkoituksen selkeämmin.
moduuli (module)Moduuli mod on Rustissa kokoelma funktioita, tyyppejä, vakioita, staattisia muuttujia tai toisista moduuleista. Moduuli sisältää vain julkisia jäseniä. Moduulin jäseneihin pääsee käsiksi :: syntaksilla. Esim std::collections on moduuli (ja hyödyllinen sellainen).
tietorakenne (data structure)Tietorakenne on sovelluskohtainen tapa tallentaa ja käsitellä tietoa. Tietorakenteita ovat esimerkiksi taulukot, joukot, vektorit, listat, mapit, ja puut. Tietorakenteilla on usein tarkasti määritellyt käyttäytymisvaatimukset esim aika- ja muistikompleksisuudesta.
nimeämiskäytäntö (naming convention)Nimeämiskäytäntö määrää miten moniosaiset sanat kirjoitetaan. Eri nimeämiskäytäntöjä käytetään monissa eri konteksteissa. Rustissa muuttujat kirjoitetaan snake_casessa, tyypit PascalCasessa, vakiot SCREAMING_SNAKE_CASEssa ja laatikoiden nimet hyvin usein kebab-casessa.
tyyppipäättely (type inference)Rust-kääntäjä voi monessa tilanteessa johtamaan mikä tyyppi pitää olla missäkin. Funktion parametrien tyyppejä tai palautustyyppiä ei voi jättää pääteltäväksi. Joissain tilanteissa voi jättää osan tyypistä pois _-merkillä, esim let v: Vec<_> = hashmap.collect().
indeksi (index)Indeksi on järjestysluku esimerkiksi taulukkoon, tai sliceen. Indeksit ovat aina tyyppiä usize eli 64 bittiä modernilla prosessorilla. Myös taulukon tai slicen pituus on aina tyyppiä usize.
prosessi (process)Käyttöjärjestelmän hallitsema ajossa oleva ohjelma. Jokaisella prosessilla on käytössä oma muistialue ja resursseja joihin muut prosessit eivät pääse käsiksi. Prosessi koostuu aina yhdestä tai useasta säikeestä, jotka voivat suorittaa koodia samanaikaisesti.
omistajuus (ownership)Rustin muistinhallintaparadigma, joka varmistaa muistin käytön turvallisesti sekä vapauttaa käyttämättömät resurssit. Jokaisella arvolla on tasan yksi omistaja, ja se mitä ei omisteta ei pystytä käyttämään.
borrow checker
referenssi (reference)Rustin hallitsema osoitin lainattuun (borrowed) tietoon. Lainauksen aikana muistia ei pysty muuttamaan eikä siirtämään. Lainaus loppuu referenssin pudotessa. Referenssi voi olla muuttumaton &T tai muuttuva &mut T. Muuttuvan lainaukset poissulkevat toiset muuttuvat lainaukset sekä muuttumattomat lainaukset. Rustin borrow checker varmistaa nämä invariantit.
kehys (frame)Jokainen (suljettu) funktiokutsu työntää kutsupinoon kehyksen, joka sisältää funktion parametrit ja funktion paikalliset muuttujat. Kehys sisältää muutakin tietoa, kuten osoitteen, josta prosessori jatkaa funktion palauttaessa. Kehyksiä ei pysty määrittämään lähdekoodissa, koska se on ABIn määrittämä.
kutsupino (call stack)Jokaisella prosessilla on kutsupino, joka sisältää kehykset pinossa ajonaikana. Kutsupino luodaan säikeen käynnistyessä. Kutsupinolla on rajallinen koko, jonka takia prosessi kaatuu jos sisäkkäisiä funktiokutsuja on liian monta. Jos taulukkoon (array), joka on tallennettu kutsupinolle (eikä keolle), kirjoitetaan liian monta alkioita, voidaan saada aikaiseksi kutsupinon ylivuotovirhe, jossa "vahingossa" ylikirjoitetaan kehyksen palautusosoite. Tämä virhe on syynä monelle kriittiselle tietoturvahaavoittuvuudelle.
monadi (monad)Monadi on monoidi endofunktorien kategoriassa.

Viikko 4

TermiSelitys
vapaakokoinen (unzised)Vapaakokoinen tyyppi (piirre ?Sized) on sellainen tyyppi jonka koko (pituus bitteinä) tiedetään vasta ajonaikana. Vapaakokoista dataa ei voi säilyttää kehyksessä vaan se pitää tallentaa kekoon, esim Boxin avulla. Piirreolio (trait object) on esimerkki vapaakokoisesta tyypistä. Vec tai String eivät toisaalta ole vapaakokoisia, mutta [T] ja str ovat.
tyyppiturvallisuus (type safety)Tyyppiturvallinen ohjelmointikieli estää T tyyppisen tiedon t kopioimista/siirtämistä sellaiseen muuttujaan u (osoitteeseen muistissa), jossa t:n bitit ei ole kyseisen osoitteen tyyppijoukossa U. Esimerkiksi Rust on tyyppiturvallinen, koska bool (yleensä tavun kokoinen) tyyppinen muuttuja on aina binääriesityksenä 0 tai 1 eikä voi olla esimerkiksi 3. Rust estää tyyppien "väärinkäytön" kääntöaikana toisin kuin esimerkiksi Python heittää vasta ajonaikana tyyppivirheen TypeError. Useimmat dynaamisesti tyypitetyt kielet, kuten JavaScript ja Python ovat tyyppiturvallisia.
muistiturvallisuus (memory safety)Epätyyppiturvallinen kieli, joka lisäksi sallii arbitraarisen luvun muuttamisen (cast) osoittimeksi on epämuistiturvallinen kieli. Esimerkiksi C ja C++ osoittimia voi tallentaa lukuihin ja lukuja osoittimiin. Rust on tyyppiturvallinen joten se on myös muistiturvallinen. Esimerkkejä epämuistiturvallisten kielten muistivirheistä on puskurin ylivuotovirhe, data race ja use after free.
turvallisuus (safety)Turvallisella ohjelmoinnilla tarkoitetaan ohjelmointikäytäntöjä, joilla vältytään esimerkiksi tietoturvahaavoittuvuuksilta. Epäturvallisia ohjelmointikieliä ovat esimerkiksi C ja C++, koska niissä tehtyjä muistinhallintavirheitä on erittäin vaikeaa havaita ja estää.
muistivirhe (memory error)Muistivirhe on looginen virhe (ei aina tunnistettavissa), jossa muistia esim pinolla tai keolla käytetään väärin. Esimerkkejä väärinkäytöstä on saman muistin lukeminen ja kirjoittaminen samanaikaisesti, ei-initialisoidun muistin lukeminen tai muistin unohtaminen/vuotaminen. Kaikki muistivirheet eivät ole määrittelemätöntä käytöstä, esimerkiksi Rust ei suoraan estä muistivuotoja mutta estää määrittelemättömän käytöksen. Määrittelemätön käytös voi aiheuttaa muistin korruptoitumista, ohjelman kaatumisen, tai mitä tahansa muuta mikä on mahdollista.
yhdistelmätyyppi (composite type)Yhdistelmätyyppi koostuu muista tietotyypeistä tai myös itsestään. Kaikki structit, tuplet ja enumit ovat yhdistelmätyyppejä.
algebrallinen tietotyyppi (algebraic datatype)Algebrallinen tietotyyppi koostuu muista tietotyypeistä tai myös itsestään. Algebrallisia tietotyyppejä on kahdenlaisia: tulotyyppejä T * U ja summatyyppejä T + U. Rustissa yhdistelmätyypit ovat algebrallisia, joten niitä pystyy matchaamaan ergonomisesti. Muissa olioperusteisissa kielissä yhdistelmätyypit eivät ole yhtä algebrallisia.
tulotyyppi (product type)Tulotyyppi sisältää samanaikaisesti monta tyyppiä sisällään. Esimerkiksi kaikki tyypin (i32, bool) jäsenet koostuvat validista i32sta ja boolista. Kaikki structit ovat myös tulotyyppejä, koska ne ovat kuin tupleja joilla on nimetyt alkiot.
summatyyppi (sum type)Summatyyppi koostuu useasta eri variantista, joista vain ja tasan yksi variantti voi olla tallennettu muuttujaan tietyllä hetkellä. Esimerkiksi Option<T> summatyyppi koostuu None variantista ja Some(T) variantista. Option ei voi sisältää molempia samaan aikaan. Kaikki enumit ovat summatyyppejä. Enumin variantit eivät ole tyyppejä vaan konstructoreita, jotka palauttavat kyseisen enumin tyyppistä tietoa. Enumin variantti voi sisältää toisia enumeita tai tulotyyppejä, kuten tupleja. C-kielissä enumit tehdään unioneina.
geneerisyys (generics)Kuten kaikki muukin ohjelmoinnissa, geneerisyys on abstraktio. Geneerisyys on mekanismi, jonka avulla funktioita ja tietorakenteita saadaan toimimaan monella eri tyypillä käännösaikana. Esimerkiksi vektori Vec<T> on geneerinen tietorakenne, koska se voi sisältää listan (lähes) mitä tahansa tyyppiä. Vektorin tyyppiä T kutsutaan nimetyksi tyyppiparametriksi, joten voidaankin ajatella että Vec on kääntämisvaiheessa "tyyppifunktio", joka ottaa sisään esimerkiksi argumentin Vec<i32> ja palauttaa konkreetin tyypin. Kyseessä ei ole kuitenkaan alityypitys: funktio jonka parametri on tyyppiä Animal voi antaa argumentiksi tyyppiä Cat olevaa tietoa.
sulkeuma (closure)Klosuuri on nimetön ilmentymä funktiosta. Sulkeuma \|a\| a + 1 ottaa sisään yhden numeerisen argumentin ja palauttaa numeron. Sulkeuma yleensä päättelee parametrien tyypit kontekstista. Sulkeuma eroaa tavallisista funktioista siinä, että ne voivat sulkea sisäänsä muuttuja ympäröivästä skoopista.
elinaika (lifetime)Kaikella datalla on elinaika, joka tiedetään käännösaikana. Esimerkiksi paikallisten muuttujien elinaika on sama (tai lyhyempi) kuin kyseisen funktion kehyksen. Jos muuttujan data palautetaan funktiosta ja sidotaan uuteen muuttujaan, on kyseessä eri elinaika.
elinaikamerkintä (lifetime annotation)Elinaikamerkintää 'a tarvitaan aina välillä selkeyttämään kääntäjälle tarkoituksemme. Elinaikamerkintöjä harvemmin näkee, koska kääntäjä pystyy päättelemään ne, samalla tavoin kuin tyypit. Esimerkiksi funktio fn(&'a str) -> &'a str ottaa sisään string slicen ja palauttaa string slicen, mutta elinaikamerkintää ei tarvitse merkitä koska kääntäjä pystyy sen päättelemään: fn(&str) -> &str. Merkinnässä heittomerkin jälkeen voi tulla mikä tahansa nimi, kuten muuttujilla. Erikoiselinaikamerkintä 'static tarkoittaa että tietoa ei pudoteta eikä siirretä ennen ohjelman (tarkemmin säikeen) sulkeutumista.
segmentointivirhe (segmentation fault)Segmentointivirhe voi tapahtua epämuistiturvallisissa kielissä, jos prosessi yrittää lukea tai kirjoittaa muistia, joka ei kuulu sille. Esimerkiksi NULL-osoittimen dereferensointi aiheuttaa segmentointivirheen.
turbokala (turbofish)Turbokalaoperaattorilla ::<> voi määritellä funktion geneeriset tyyppiargumentit sen kutsumisen yhteydessä. Esim iteraattorien .collect metodille voi antaa kohdetyypin turbokalan avulla (1..10).into_iter().collect::<Vec<i32>>().

Viikko 5

TermiSelitys
periytyminen (inheritance)
tunnus (identifier)
vakio (constant)
paniikki (panic)
poikkeus (exception)
pakettirekisteri (package registry)
nollakustannusabstraktio (zero-cost abstraction)
rakentaja (constructor)
koersio (coercion)Tyyppikoersiot ovat automaattisia tyyppimuutoksia (cast), esimerkiksi &mut T voi koersoitua eli muuttua &T:ksi tietyissä tilanteissa. Elinajat koersoituu alityypityksen sääntöjen mukaan. Lue lisää Nomiconista.
ariteetti (arity)
alityypitys (subtyping)Alityypitys on olio-orientoidun ohjelmoinnin ominaisuus, jonka mukaan alityyppiä, esimerkiksi Catin ilmentymää voi aina käyttää Animalin sijasta varianssisääntöjen mukaan muuttamatta ohjelman toimintaa. Rustissa tyypit eivät muodosta luokkahierarkiaa toisin kuin traditionaalisissa olio-orientoiduissa kielissä, joten alityypityssäännöt pätevät vain elinajoilla.

Muita unpinnattuja (saattaa siirtyä)

Rustiin liittyvät

TermiSelitys
keko (heap)
debuggaus
testaaminen
kohdearkkitehtuuri
sidonta (dispatch)
avainsana
sijoitus (assignment)
literaali
operaattori
osoitin
kopiointi
kloonaus
kuormittaminen (overloading)
rekisteri (register)
määrittelemätön käyttäytyminen (undefined behavior)
race conditionEi tule sekoittaa termin data race kanssa
data race
roskankeräin
muistivuoto
makro
allokointi (allocation)
haara (branch)
siepata (catch)
rakentaja (builder)https://rust-unofficial.github.io/patterns/patterns/creational/builder.html
semafori (semaphore)
RAII
SoC (separation of concerns)
erikoistaminen (specialization)
polku

Lähteet

Osa selityksistä ja suomennoksista on otettu seuraavista lähteistä

Tehtäväalusta

Kurssin tehtävät tehdään otarustlings tehtäväalustalla (docs.rs).

otarustlings tehtäviä ei tarvitse palauttaa kurssin aikana vaan kaikki palautetaan vasta lopussa. Bonus-tehtävät (B:llä alkavat) eivät ole pakollisia vaan niistä saa lisäpisteitä.

otarustlings on Rust kurssia varten käsin uudelleenkirjoitettu rustlings klooni. rustlings kirjoitettiin uudelleen koska se oli niin huonosti koodattu. Lopulta otarustlings on ihan yhtä huonoa koodia mutta ominaisuudet ovat sentään paremmat.

otarustlingsin jokainen tehtävä sisältää "testin", vaikka tehtävän ratkaisuksi riittää saada koodi kääntymään. Tämä johtuu siitä että tulostetut asiat eivät tulisi muuten näkyviin testatessa.

Asentaminen

Asenna otarustlings Cargolla:

cargo install otarustlings

otarustlingsin pystyy asentamaan manuaalisesti Linux-x86_64 tietokoneelle lataamalla uusimman binäärin repositorion julkaisuista (kohdasta Assets > Other),

Quick start

Suorita seuraavat komennot joko erillisellä komentorivillä tai VS Coden integroidulla komentorivillä

  • Asenna otarustlings (ei toimi windowsilla vielä)

    cargo install otarustlings
    
  • Luo tehtävät

    otarustlings init
    

    Huomaa: komento ei ylikirjoita vanhoja tiedostoja

  • Aloita otarustlings

    otarustlings start
    
  • Avaa tehtävät koodieditorissa, ja aloita koodaaminen. Esimerkiski, jos käytät VS Codea, voit suorittaa seuraavan komennon samasta kansiosta jossa suoritit otarustlings init

    code exercises/week1
    

Ohje

Käyttöohje löytyy otarustlings paketin dokumentaatiosta.

otarustlingsin komentoriviohjeet saat komennolla

otarustlings --help

Rust analyzerin käyttö

Rust analyzerin käyttäminen on ehdottomasti sallittua ja suositeltavaa.

Rust analyzer tarvitsee Cargo.toml pakettimanifestitiedoston, jotta sitä voi käyttää tehtävissä. Kyseinen manifesti sijaitsee sijainnissa exercises/Cargo.toml.hack, joka pitää uudelleennimetä Cargo.tomliksi, jotta Rust analyzer tunnistaa sen.

Tiedoston nimi ei voinut olla Cargo.toml, sillä muuten koko exercises kansio olisi puuttunut julkaisusta. Tulevaisuudessa otarustlings uudelleennimeää tiedoston automaattisesti.

Tehtävien palauttaminen

Otarustlings tehtäväkansio exercises palautetaan kurssin lopussa. Kansion voi arkistoida esimerkiksi zip tai tar.gz muotoon, joka lähetetään arvioitavaksi Niklakselle: telegram tai sähköposti.

Kurssin käsitekaavio

  • Tietokaavio kuvaa mitkä käsitteet vaativat oppiakseen toisen käsitteen osaamista
  • Solmut vastaavat käsitteitä, jotka sisältävät tietoa sekä syntaksista että semantiikasta. Käsitteet eivät ole ali-/ylikäsitejärjestyksessä. Käsitteen vaadittu osaamistaso voi myös vaihdella; sen voi ehkä päätellä siitä riippuvien käsitteiden määrästä
  • Nuolet kulkevat riippuvuuden suuntaan (lähtökäsite riippuu maalikäsitteestä)
  • Nuolen vahvuudet on luokiteltu seuraavasti:
    • Ei nuolta: käsitteet voidaan opettaa eri järjestyksessä
    • Tavallinen: käsite on hyvä tietää ennen seuraavaa
      • Esimerkiksi muuttuvuudesta voi puhua yleisemmin, kuin vain muuttujien yhteydessä. Muuttujan käsitteen voi myös oppia "takaperin" jos ymmärtää muuttuvuuden
    • Vahva: käsite on hyvä ymmärtää ennen seuraavaa
      • Esimerkiksi lainaamista on turha opettaa, koska sen määritelmä on hyvin omistajuuskeskeinen
  • Komposiittinuolia ei tarvitse piirtää, koska osaaminen on useimmiten transitiivista

"on hyvä" jää tässä sen kummemmin määrittelemättä

Viikko 1

Viikko 1 sisältää ainoana oikeana tehtävän guessing_gamen implementoinnin, jonka ohessa oppiii cargon ja println! käytön.

flowchart LR
    linkStyle default stroke-width:1px
    guessing_game
    println[println-makro]
    cargo
    muuttujat
    muuttuvuus
    match
    laatikot
    
    guessing_game --> println & cargo & muuttujat & muuttuvuus & match & laatikot

Viikko 2

Viikko 2 sisältää aloittelijatason käsitteitä Rustista. Kaikki käsitteet liittyvät muuttujiin, joten niistä aloitetaan.

flowchart LR
    linkStyle default stroke-width:1px
    muuttujat
    muuttuvuus
    to[tiedon omistajuus]
    primitiivityypit
    fn[funktioiden määritteleminen]
    args[parametrit, argumentit]
    expr[lausekkeet ja puolipiste]
    if[if-lauseke]
    palauttaminen
    scope
    lainaaminen
    
    to --> muuttujat
    muuttuvuus --> muuttujat
    expr --> muuttujat
    palauttaminen --> expr
    args ==> muuttujat
    scope --> muuttujat
    fn ==> primitiivityypit & args
    fn --> expr & scope & expr
    if --> expr
    lainaaminen ==> to

Viikko 3

Viikko 3 sisältää tutustumisen muistinhallintaan ja tavuihin.

flowchart LR
    linkStyle default stroke-width:1px
    kutsupino
    koko
    representaatio
    slice
    keko
	taulukko
    vec[Vec-tietorakenne]
    referenssi
    osoitin
    proc[prosessit ja säikeet]
    ohjelmatiedosto
    konekieli
    
    vec --> osoitin
    keko --> koko & kutsupino
    osoitin --> keko
    taulukko --> osoitin
    slice --> taulukko & representaatio --> koko
    referenssi --> osoitin
    referenssi ==> lainaaminen ==> to[tiedon omistajuus]
    proc --> ohjelmatiedosto --> konekieli

Viikko 4

Viikko 4 tutustutaan funktionaalisesta ohjelmoinnista tuttuihin algebrallisiin tietotyyppeihin (struct ja enum) ja niiden esitykseen muistissa.

flowchart LR
    linkStyle default stroke-width:1px
	tuplet
	structit
	enumit
	Option
	Result
	at[algebrallinen tietotyyppi]
	staticdyn[staattinen ja dynaaminen tyypitys]
	virheenhallinta
	panikointi
	hashmap[HashMap-tietorakenne]
	match
	derive
    klosuurit
    assocfn[metodit ja assosioidut funktiot]
    impl
    traits[piirteet yleisellä tasolla]
    
    tuplet & structit --> at
    Option & Result --> enumit ==> structit & tuplet
    assocfn --> at
    virheenhallinta --> Option & Result & panikointi
    hashmap ==> Result & Option
    match ==> at
    impl --> enumit
    impl ==> assocfn
    derive --> traits
    klosuurit ==> fn[funktioiden määritteleminen]

Enumit liittyvät structeihin, koska struct-syntaksia käytetään enumien struct-varianteissa

Solmut

  • moduulit
  • laatikot
  • Cargo.toml
  • virhepropagointi ?
  • kutsupinon purku (unwind)
  • geneeriset tyypit, tyyppifunktiot
  • piirteet yleisellä tasolla
  • geneeristen rajoittaminen piirteillä
  • dyn Trait
  • pub
  • Box smart pointer
  • bin/lib

Palautelomake