Open69

Rust の the book の 6〜10 章までを読む

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Defining an Enum

https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html

  • 列挙体は取り得る値(バリアント)のうちの 1 つであるものを表現するのに適している、例えば YES や NO など。
  • YES や NO なら bool を使えば良いが、種類が 3 種類以上になると列挙体の出番だ。
enum IpAddrKind {
    V4,
    V6,
}

Rust では構造体や列挙体はキャメル記法のようだ、またバリアントもキャメル記法のようだ。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Enum Values

https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html#enum-values

let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
fn route(ip_kind: IpAddrKind) {}
route(IpAddrKind::V4);
route(IpAddrKind::V6);
enum IpAddrKind {
    V4,
    V6,
}

struct IpAddr {
    kind: IpAddrKind,
    address: String,
}

let home = IpAddr {
    kind: IpAddrKind::V4,
    address: String::from("127.0.0.1"),
};

let home = IpAddr {
    kind: IpAddrKind::V6,
    address: String::from("::1"),
};
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

C 言語などであれば上記で十分だが、Rust ではさらに下記のようにまとめられるようだ。

enum IpAddr {
    V4(String),
    V6(String),
}

let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IPAddr::V6(String::from("::1"));

バリアントごとに別のデータ型であっても問題はない。

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String)
}

let home = IpAddr::V4(127. 0, 0, 1);
let loopback = IPAddr::V6(String::from("::1"));

上記のように IP アドレスを格納するデータ型を自分で定義することもできるが、実用上は下記の標準ライブラリに含まれる std::net::IpAddr 列挙体を使えば良い。

https://doc.rust-lang.org/std/net/enum.IpAddr.html

標準ライブラリでは下記のように定義されている。

struct Ipv4Addr {
    // private fields
}

struct Ipv6Addr {
    // private fields
}

enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}

列挙体には下記のように様々な型のデータを組み込める。

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

バリアントに組み込むデータ型には下記のように構造体を使うこともできる。

struct QuitMessage;
struct MoveMessage {
    x: i32,
    y: i32,
}
struct WriteMessage(String);
struct ChangeColorMessage(i32, i32, i32);

ただし列挙体を使った方が関数の引数を与える時などに便利なのでなるべくこちらを使うのが良いのかも知れない。

なんと列挙体にメソッドを定義することもできる。

impl Message {
    fn call(&self) {
        // ...
    }
}

let m = Message::Write(String::from("hello"));
m.call();
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

The Option Enum and Its Advantages Over Null Values

  • Option は結果が何かの値か、あるいは値が何もないケースに使用される。
  • 例えばリストの最初の要素を取得する関数の場合、空のリストであれば何も得られず、そうでなければ何かの与えが得られる。
  • Rust には null がない、なんということだ。
  • Null の発明者であるトニーさんは「null は 10 億ドル単位のミスだった」と言っているらしい。
  • できれば参照が安全に使用されているかどうかをコンパイラが自動的にチェックできるようにしたかったが、null は実装するのがあまりにも簡単なのでその誘惑に抗うことができなかった。
  • 結果的に数え切れないほどのエラーや脆弱性やクラッシュがもたらされ、過去 40 年間にわたって 10 億ドル単位の苦しみと損失を招いただろう。

トニーさんの経験談はとてもためにになる。

  • Null が表現しようとしていること自体は便利なものだ。
  • Null が表現しようとしていることは値が現在無効であるか何らかの理由で不在であることだ。
  • それを表現しようとすることが問題なのではなく、それを null を使って実装しようとすることが問題である。
  • Rust では null が無い代わりに Option<T> を使って 存在/不在の値を扱っている。
  • Option<T> の定義は下記の通り。
enum Option<T> {
    None,
    Some(T),
}
  • Option<T> 列挙体はプレリュードに含まれているので明示的にインポートする必要がない。
  • バリアントもプレリュードに含まれているので Option:: も前置する必要がない。
  • <T> はジェネリック型パラメーターと呼ばれるものであり後のチャプターで学ぶ。
  • 今のところは Option 列挙体はどんな型のデータにも使用できることを理解していれば良い。
let some_number = Some(5);
let some_char = Some('e');

let absent_number: Option<i32> = None;

次は下記から始めよう。

The type of some_number is Option<i32>.

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida
  • Some バリアントを使った場合には最初のパラメーターの型から Option<T> を推論できる。
  • None バリアントを使った場合は型アノテーションを追加する必要がある。
  • Option<T> が null よりも優れている点は、それが T とは異なる型なのでコンパイラーが T 型の値と同じように扱うことを禁止する点だ。
let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = x + y; // この行でエラーになる。

上記のコードをコンパイルすると下記のエラーメッセージが表示される。

cannot add Option<i8> to i8

  • i8 型は常に値が存在することが保証されるが、Option<i8> はそうではない。
  • したがって使用前に値が存在することを確認する必要がある、言い換えると Option<i8>i8 に変換する必要がある。
  • コンパイル時にエラーになることによって本当は null なのに null ではないと仮定してしまう問題を検出できる。
  • Option<T> を使うことで使用前に値が不在のケースを処理することが強制される。
  • バリアントによって実行するコードを変えたい場合には match 式が便利である。
  • match 式では列挙体に値が含まれる場合にその値にアクセスできる。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

The match Control Flow Construct

  • Rust では match を使って特定のパターンにマッチする場合に式を評価したり文を実行したりする機能がある。
  • パターンマッチにはリテラル、変数名、ワイルドカードなど様々なものが利用できるらしい。
  • マッチ式の素晴らしい点はすべてのありうる可能性を処理している点をコンパイラが確認できる点である。
  • マッチ式はコイン振り分け機に例えられる:コインがコロコロと転がっていて一番最初にフィットした穴に入っていくイメージである。
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}
  • マッチ式は if 式と似ているが条件が boolean ではなく値である点が異なる。
  • マッチ式のそれぞれのパターンとコードの組み合わせは「アーム」と呼ばれる。
  • アームでは => オペレーターを使用する、TypeScript のアロウ関数とごっちゃになりそうだ。。。
  • アームはカンマで区切られる。
  • アームのコードは式であり、式の評価結果はマッチ式の評価結果になる。
  • アームのコードには {} を使えるが、1行の場合は省略できる。
  • {} を使った場合にはカンマはあってもなくても良い。
fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
                println!("Lucky penny!");
                1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Patterns That Binds to Values

マッチ式の便利な機能は列挙体にバインドされた値を取り出せる点である。

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // ...
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {:?}!", state);
            25
        },
    }
}

列挙体にバインドされた値を取り出すにはアームのパターン部に (state) のように書く。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Matching with Option<T>

Coin 列挙体と同様に Option<T> 列挙体についても同様に値を取り出すことができる。

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(x) => Some(x + 1),
    }
}

let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Matches Are Exhaustive

https://doc.rust-lang.org/book/ch06-02-match.html#matches-are-exhaustive

  • match 式はすべての可能性が考慮されていなければならない。
  • 例えば下記のコードをコンパイルすると non-exhaustive patterns: None not covered とエラーメッセージが表示される。
fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        Some(x) => Some(x + 1),
    }
}
  • Rust は全ての可能性を考慮していないことを検出するのに加え、どのようなパターンが考慮されていないかまでを教えてくれる。
  • Exhaustive とは「網羅的」などに和訳され、漏れがないことを意味する。
  • マッチ式が網羅的になっているおかげで null のように考慮していないケースがあることを防げる。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Catch-all Patterns and the _ Placeholder

https://doc.rust-lang.org/book/ch06-02-match.html#catch-all-patterns-and-the-_-placeholder

other を使うと該当しなかった全てのパターンにマッチさせることができる、switch 文の default: のようなイメージだ。

let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    other => move_player(dice_roll),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}

列挙体の値を使用しない場合は other の代わりに _ を使用する。

let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    _ => reroll(),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}

特定のパターンに該当しない時に何もしない場合は明示的に _ => () と書く。

let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    _ => (),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}

other_ は最後のアームである必要がある。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Concise Control Flow with if let

マッチ式で 1 つのパターンにマッチした時のみコードを実行し、それ以外の場合は何もしない場合には if let 構文を使える。

let config_max = Some(3u8);
match config_max {
    Some(max) => println!("The maximum is configured to be {}", max),
    _ => (),
}

上記の代わりに下記のように書ける。

let config_max = Some(3u8);
if let config_max = Some(max) {
    println!("The maximum is configured to be {}", max);
}
  • => の代わりに = を使う、== と書いてしまいそうなので注意が必要。
  • if let 構文のデメリットとして網羅性のチェックが損なわれること。
  • if let 構文には else をつなげることができ、catch-all にマッチした場合のコードを指定できる。
let mut count = 0;
if let Coin::Quarter(state) = coin {
    println!("State quarter from {:?}!", state);
} else {
    count += 1;
}

if let では左辺値と右辺値が入れ替わっても良いのだろうか?

fn main() {
    let some = Some(1);

    if let Some(value) = some {
        println!("value is {value}");
    }
}

上はコンパイルが通るが下はコンパイルが通らない。

fn main() {
    let some = Some(1);

    if let some = Some(value) {
        println!("value is {value}");
    }
}
コンソール出力
error[E0425]: cannot find value `value` in this scope
 --> src/main.rs:4:24
  |
4 |     if let some = Some(value) {
  |                        ^^^^^ not found in this scope

error[E0425]: cannot find value `value` in this scope
 --> src/main.rs:5:29
  |
5 |         println!("value is {value}");
  |                             ^^^^^ not found in this scope

For more information about this error, try `rustc --explain E0425`.
error: could not compile `if_let` (bin "if_let") due to 2 previous errors

順序を間違えそうだ。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Managing Growing Projects with Packages, Crates, and Modules

  • パッケージ・クレート・モジュールは関連する機能をまとめたり異なる機能を別のファイルに分けたりすることで、ある機能が実装されている位置を探したり新しい機能を追加すべき場所を明確にすることを助ける。
  • パッケージは複数のバイナリクレートと多くても 1 つのライブラリクレートを含む。
  • パッケージが大きくなってきたら外部依存になる別のクレートに分けることができる。
  • とても大きなプロジェクトの場合は Cargo はワークスペース機能が便利である。
  • 実装の詳細を隠す方法についても学んでいく。
  • 関連する概念としてスコープがある。
  • コードが書かれているネストされた文脈にはスコープ内として定義された名前があり、プログラマーやコンパイラーはある名前が何を指して何を意味するのかを知る必要がある(?)

A related concept is scope: the nested context in which code is written has a set of names that are defined as “in scope.” When reading, writing, and compiling code, programmers and compilers need to know whether a particular name at a particular spot refers to a variable, function, struct, enum, module, constant, or other item and what that item means.

  • 同じスコープ内で同じ名前は 1 回しか使うことができない。
  • Rust ではコードを構成するための手段が提供されており、これらはモジュールシステムと呼ばれることがある。
  • パッケージ:Cargo の機能であり、複数のクレートをビルド・テスト・配布できる。
  • クレート:モジュールのツリーであり、ライブラリや実行可能ファイルを生成できる。
  • モジュール:コードの構成・スコープ・パスの公開/非公開を制御できる。
  • パス:構造体・関数・モジュールの名前を付ける方法。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Packages and Crates

  • クレートはコンパイラが一度に考慮するコードの最小単位でである。
  • 1 つのソースコードに対して rustc コマンドを実行した時であってもコンパイラはそのファイルをクレートとして扱う。
  • クレートは複数のモジュールを含むことができる。
  • クレートにはライブラリとバイナリの 2 種類がある。
  • ライブラリクレートは main 関数を持たず、複数のプロジェクトで共有される機能を定義する。
  • 単純にクレートと言った場合はライブラリクレートを指すことが多く、クレートは他のプログラミング言語ではライブラリやパッケージと同じような意味で用いられる。
  • クレートルートとはコンパイラが最初に処理するソースコードであり、ルートモジュールが作成される。
  • パッケージとは 1 つ以上のクレートの集合であり、これらのクレートをコンパイルするための情報が Cargo.toml に含まれている。
  • Cargo 自体もクレートであり、cargo コマンドのバイナリクレートと API を提供するライブラリクレートが含まれている。
  • パッケージは複数のバイナリクレートを含むことができるが、ライブラリクレートは最大で 1 つまで。
  • Cargo.toml には src/main.rs がルートモジュールであることが記載されていないが、バイナリクレートのクレートルートのデフォルトが src/main.rs であるという規約があるので cargo はそれに従っている。
  • この場合、バイナリクレートの名前はパッケージと同じになる。
  • ライブラリクレートのデフォルトは src/lib.rs であるという規約もある。
  • 複数のバイナリクレートを含めたい場合は src/bin ディレクトリにソースコードを配置する。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Defining Modules to Control Scope and Privacy

  • use キーワードを使うとパスをスコープに含めることができる。
  • pug キーワードを使うと関数や構造体などを公開できる。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Modules Cheat Sheet

https://doc.rust-lang.org/book/ch07-02-defining-modules-to-control-scope-and-privacy.html#modules-cheat-sheet

  • コンパイラーは最初にクレートルートファイルを探す、これはライブラリクレートの場合は src/lib.rs、バイナリクレートの場合は src/main.rs がデフォルトになっている。
  • クレートルートファイルではモジュールを宣言できる。
  • 例えば garden モジュールを定義する場合は mod garden; のように書く。
  • モジュールの実装コードはインライン、src/garden.rs、src/garden/mod.rs のいずれかで定義される。
  • クレートルートファイル以外でもサブモジュールを宣言できる。
  • 例えば vegetables サブモジュールを定義する場合は src/garden.rs に mod vegetables; と書く。
  • サブモジュールの実装コードはインライン、src/garden/vegetables.rs、src/garden/vegetables/mod.rs のいずれかで定義される。
  • モジュールが公開する関数などには同じクレートのどこからでもアクセスできる。
  • 例えば vegetables モジュールの Asparagus 構造体にアクセスするには crete::graden::vegetables::Asparagus と書く。
  • デフォルトではモジュール内のコードは非公開であり、アクセスできるようにするにはモジュール自体と関数などの両方を pub キーワードを使って公開する必要がある。
  • use キーワードを使うとパスを省略できる、例えば use crete::graden::vegetables::Asparagus; と書くと単に Asparagus と書くだけで構造体を利用できるようになる。
コマンド
cargo new backyard
cd backyard
mkdir src/garden
touch src/garden.rs
touch src/garden/vegetables.rs

疲れたので続きは後から。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida
src/main.rs
use crate::garden::vegetables::Asparagus;

pub mod garden;

fn main() {
    let plant = Asparagus {};
    println!("I'm growing {:?}", plant);
}
src/garden.rs
pub mod vegetables;
src/garden/vegetables.rs
#[derive(Debug)]
pub struct Asparagus {}
コンソール出力
I'm growing Asparagus
  • クレートルートファイルは src/main.rs である。
  • 実際にコーディングする際にはトップダウンで書いていった方が良い、ボトムアップで書こうとすると VSCode の Rust エクステンションから何やらメッセージが表示される。
  • pub mod garden;pub mod vegetables; を書くと garden.rs や vegetables.rs がクレートに含まれるようになる。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

https://doc.rust-lang.org/book/ch07-02-defining-modules-to-control-scope-and-privacy.html#grouping-related-code-in-modules

  • モジュールは可読性と再利用性を高めるためにクレート内でソースコードを整理するのに役立つ。
  • モジュールではアイテムの公開/非公開を制御でき、デフォルトでは非公開になる。
  • モジュールとアイテムの両方を公開することでモジュール外からアクセスできるようになる。
コマンド
cd ~/workspace/rust
cargo new restaurant --lib
cd restaurant
src/lib.rs
mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}

        fn seat_at_table() {}
    }

    mod serving {
        fn take_order() {}

        fn serve_order() {}

        fn take_payment() {}
    }
}
  • src/main.rs や src/lib.rs はクレートルートと呼ばれる。
  • この名前の理由はこれらのファイルのいずれかの内容が crete と名付けられたモジュールを形成するからである。
  • crete は「モジュールツリー」と呼ばれるクレートのモジュール構造の根本に位置する。
モジュールツリー
crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Paths for Referring to an Item in the Module Tree

  • パスはモジュールツリー内にあるアイテムを特定するために使用される。
  • パスには絶対パスと相対パスの 2 種類がある、絶対パスは crete から始まり、相対パスは selfsuper などから始まる。
  • パスの区切りにはダブルコロン :: を使う。
src/lib.rs
mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // 絶対パス
    crete::front_of_house::hosting::add_to_waitlist();
    // 相対パス
    front_of_house::hosting::add_to_waitlist();
}
  • 絶対パスと相対パスのどちらを使うかは選択する必要がある。
  • ファイル移動する時に一緒に動くのであれば相対パスを使う方が良い。
  • 絶対パスを使っていもて VSCode がよろしくやってくれそうな気がする。
  • 上記のコードをコンパイルすると下記のエラーメッセージが表示される。
コンソール出力
error[E0603]: module `hosting` is private
 --> src/lib.rs:9:28
  |
9 |     crate::front_of_house::hosting::add_to_waitlist();
  |                            ^^^^^^^ private module
  |
note: the module `hosting` is defined here
 --> src/lib.rs:2:5
  |
2 |     mod hosting {
  |     ^^^^^^^^^^^

error[E0603]: module `hosting` is private
  --> src/lib.rs:12:21
   |
12 |     front_of_house::hosting::add_to_waitlist();
   |                     ^^^^^^^ private module
   |
note: the module `hosting` is defined here
  --> src/lib.rs:2:5
   |
2  |     mod hosting {
   |     ^^^^^^^^^^^

For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` due to 2 previous errors

洗濯が終わったので続きは午後にやろう。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida
  • エラーメッセージの内容は hosting モジュールが非公開であること。
  • Rust では原則としてアイテムはデフォルトで非公開となる。
  • 親モジュールから子モジュールのアイテムにはアクセスできないが、その逆は可能である。
  • アイテムを公開するには pub キーワードを使用する。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Exposing Paths with the pub keyword

https://doc.rust-lang.org/book/ch07-03-paths-for-referring-to-an-item-in-the-module-tree.html#exposing-paths-with-the-pub-keyword

  • アイテムを公開するにはモジュールとアイテムの両方を公開する必要がある。
  • 仮にモジュールだけを公開すると下記のエラーメッセージが表示される。
エラーメッセージ
error[E0603]: function `add_to_waitlist` is private
  --> src/lib.rs:18:37
   |
18 |     crate::front_of_house::hosting::add_to_waitlist();
   |                                     ^^^^^^^^^^^^^^^ private function
   |
note: the function `add_to_waitlist` is defined here
  --> src/lib.rs:3:9
   |
3  |         fn add_to_waitlist() {}
   |         ^^^^^^^^^^^^^^^^^^^^

error[E0603]: function `add_to_waitlist` is private
  --> src/lib.rs:19:30
   |
19 |     front_of_house::hosting::add_to_waitlist();
   |                              ^^^^^^^^^^^^^^^ private function
   |
note: the function `add_to_waitlist` is defined here
  --> src/lib.rs:3:9
   |
3  |         fn add_to_waitlist() {}
   |         ^^^^^^^^^^^^^^^^^^^^

For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` (lib) due to 2 previous errors

下記のように両方に pub をつけることによってコンパイルが成功するようになる。

src/lib.rs
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    crate::front_of_house::hosting::add_to_waitlist();
    front_of_house::hosting::add_to_waitlist();
}
  • 絶対パスは crate から始まり、crate はクレートのモジュールツリーの根を表す。
  • front_of_house モジュールはクレートルートに定義されており、公開はされていないが eat_at_restaurant 関数と同じモジュールに含まれているのでモジュールにはアクセスすることができる。
  • hosting モジュールも pub キーワードによって公開されているのでアクセスできる。
  • add_to_waitlist 関数も pub キーワードによって公開されているのでアクセスできる。
  • 相対パスも原理は同じだが最初が crate ではなく front_of_house から始まっている点が異なっている。
  • 相対パスの場合は関数などが定義されている位置からの始点となる。
  • クレートを開発して公開する場合、pub をつけたアイテムは依存するクレートに影響を与えるので慎重に考える必要がある。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Best Practices for Packages with a Binary and a Library

  • パッケージにはデフォルトのバイナリクレートとライブラリクレートの両方を含むことができる。
  • この性質を利用してライブラリクレートにロジックを書いておき、バイナリクレートからライブラリクレートのコードを呼び出すという設計パターンがある。
  • こうすることで他のプロジェクトはライブラリクレートの API にアクセスできる利点がある。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Starting Relative Paths with super

https://doc.rust-lang.org/book/ch07-03-paths-for-referring-to-an-item-in-the-module-tree.html#starting-relative-paths-with-super

  • super を使うことで親モジュールを起点とする相対パスでアイテムを指定できる。
  • ファイルシステムの .. によく似ている。
src/lib.rs
fn deliver_order() {}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::deliver_order();
    }

    fn cook_order() {}
}
  • 上記のコードでは fix_incorrect_order 関数内のコードは back_of_house モジュール内にある。
  • back_of_house モジュールの親はクレートルートなので、クレートルートに定義された deliver_order を呼び出すことができる。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Making Structs and Enums Public

https://doc.rust-lang.org/book/ch07-03-paths-for-referring-to-an-item-in-the-module-tree.html#making-structs-and-enums-public

  • 構造体や列挙体についても pub キーワードを使って公開できる。
  • ただし構造体についてはフィールドやメソッドにも個々に pub を指定しないと公開されない。
src/lib.rs
mod back_of_house {
    pub struct Breakfast {
        pub toast: String,
        seasonal_fruit: String,
    }

    impl Breakfast {
        pub fn summer(toast: &str) -> Breakfast {
            Breakfast {
                toast: String::from(toast),
                seasonal_fruit: String::from("peaches"),
            }
        }
    }
}

pub fn eat_at_restaurant() {
    let mut meal = back_of_house::Breakfast::summer("Rye");
    meal.toast = String::from("What");
    println!("I'd like {} toast please", meal.toast);

    // 次の行は seasonal_fruit が非公開フィールドなのでコンパイルできない。
    // meal.seasonal_fruit = String::from("blueberries");
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

上記のコードでは Breakfast 構造体は非公開フィールドを持っているので、インスタンスを作成する関連関数を設ける必要がある。

関連関数でなかったらどうなるのだろう?と気になって試したところ同じモジュール内の関数であれば問題はなかった。

ただ、関連関数として作成しておくほうがわかりやすくて良いだろう。

列挙体を公開するとバリアントも一緒に公開される。

src/lib.rs
mod back_of_house {
    pub enum Appetizer {
        Soup,
        Salad,
    }
}

pub fn eat_at_restaurant() {
    let order1 = back_of_house::Appetizer::Soup;
    let order2 = back_of_house::Appetizer::Salad;
}

構造体で一部のフィールドを非公開にすることはあっても、列挙体でバリアントの一部を非公開にすることはほぼないのでこのような仕様になっている。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Bringing Paths Into Scope with the use Keyword

use キーワードを使うことでスコープにパスを持ち込むことができ、毎回相対パスや絶対パスで指定する必要がなくなる。

src/lib.rs
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}

use は特定のスコープに対してのみパスを持ち込むので、スコープが異なる場合は注意する必要がある。

src/lib.rs
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting;

mod customer {
    pub fn eat_at_restaurant() {
        hosting::add_to_waitlist();
    }
}
コンソール出力
error[E0433]: failed to resolve: use of undeclared crate or module `hosting`
  --> src/lib.rs:79:9
   |
79 |         hosting::add_to_waitlist();
   |         ^^^^^^^ use of undeclared crate or module `hosting`
   |
help: consider importing this module through its public re-export
   |
78 +     use crate::hosting;
   |

warning: unused import: `crate::front_of_house::hosting`
  --> src/lib.rs:75:5
   |
75 | use crate::front_of_house::hosting;
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: `#[warn(unused_imports)]` on by default

For more information about this error, try `rustc --explain E0433`.
warning: `restaurant` (lib) generated 1 warning
error: could not compile `restaurant` (lib) due to 1 previous error; 1 warning emitted
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Creating Idiomatic use Paths

https://doc.rust-lang.org/book/ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#creating-idiomatic-use-paths

モジュールと関数のどちらのパスに対して use を使えば良いか悩ましいケースがあるが、慣習的にはモジュールに対してパスを使った方が良いようだ。

こうすることで別モジュールの関数呼び出しであることが明確化される。

一方、構造体や列挙体の場合はアイテムそのものに対して use を使う方がより慣習的になっている。

src/main.rs
use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert(1, 2);
}

もし名前が衝突してしまう場合にはモジュールをスコープに持ち込む方が良い。

src/lib.rs
use std::fmt;
use std::io;

fn function1() -> fmt::Result {}
fn function2() -> io::Result<()> {}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Re-exporting Names with pub use

use を使ってスコープに持ち込まれる名前は公開されず、公開したい場合は pub use を使う。

このようなテクニックは再エクスポートと呼ばれる。

src/lib.rs
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}

上記のコードでは restaurant::hosting::add_to_waitlist() で関数を呼び出せるようになっている。

再エクスポートは内部の構造と外側から見えるインタフェースが異なる場合に便利である。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Using External Packages

https://doc.rust-lang.org/book/ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#using-external-packages

外部パッケージを使用したい場合は Cargo.toml に依存関係を追加する。

そうするだけでビルド時に自動的に Cargo が依存関係をダウンロードして解決してくれる。

パッケージ内の名前を使うときには use でスコープにインポートする。

std も外部パッケージであるが Rust に同梱されているので依存関係として追加する必要はないが use でインポートする必要はある。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Using Nested Paths to Clean Up Large use Lists

https://doc.rust-lang.org/book/ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#using-nested-paths-to-clean-up-large-use-lists

src/main.rs
use std::cmp::Ordering;
use std::io;

上記のコードは下記のようにまとめられる。

src/main.rs
use std::{cmp::Ordering, io};

src/main.rs
use std::io;
use std::io::Write;

上記のコードは下記のようにまとめられる。

src/main.rs
use std::io::{self, Write};
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

The Glob Operator

https://doc.rust-lang.org/book/ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#the-glob-operator

パス内に定義されているすべてのアイテムをスコープに持ち込みたい場合は下記のように書ける。

use std::collections::*;

このようなグロブオペレーターを使う場合は名前が何を指しているかやどこで定義されているかがわかりにくくなることがあるので注意する必要がある。

グロブオペレーターは主に単体テスト目的で使用される、また「プレリュード」と呼ばれるパターンでも使用される。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Separating Modules into Different Files

コマンド
rm -rf restaurant
cargo new --lib restaurant
cd restaurant
src/lib.rs
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub use front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}

上記のコードをファイルに分けていく。

コマンド
touch src/front_of_house.rs
mkdir src/front_of_house
touch src/front_of_house/hosting.rs

まずは front_of_house モジュールのファイルを分ける。

src/front_of_house.rs
pub mod hosting {
    pub fn add_to_waitlist() {}
}

次に hosting サブモジュールのファイルを分ける。

src/front_of_house.rs
pub mod hosting;
src/front_of_house/hosting.rs
pub fn add_to_waitlist() {}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Alternative File Paths

https://doc.rust-lang.org/book/ch07-05-separating-modules-into-different-files.html#alternate-file-paths

上記の例で src/front_of_house.rs の代わりに src/front_of_house/mod.rs も利用できるがエディタで mod.rs だらけになるのであまり推奨されない。

mod.rs を使うのは古いスタイルとされている。

同じプロジェクトで両方を使うことはできるがわかりにくくなるので新しいプロジェクトでは mod.rs を使わない方が良い。

ただし同じモジュールに対して両方のファイルを同時に使おうとした場合はエラーになる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Summary

https://doc.rust-lang.org/book/ch07-05-separating-modules-into-different-files.html#summary

  • Rust ではパッケージを複数のクレートに分けることができ、クレートも複数のモジュールに分けることができる。
  • モジュール内のアイテムには相対パスか絶対パスを使って参照できる。
  • use 文を使うことでパスをスコープに持ち込むことができる。
  • モジュールのコードはデフォルトで非公開だが pub キーワードを使って公開することができる。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Common Collections

  • コレクションは複数の値を含むことができるデータ構造である。
  • 配列やタプルのデータはスタックに格納されるが、コレクションのデータはヒープに格納される点が異なる。
  • このチャプターではベクタ、文字列、ハッシュマップの 3 つのコレクションを学んでいく。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Creating a New Vector

https://doc.rust-lang.org/book/ch08-01-vectors.html#creating-a-new-vector

let v: Vec<i32> = Vec::new();
  • 空のベクトルを初期化する場合には型アノテーションが必要になる。
  • ただし、後のコードで推論可能な場合は省略できるケースがある。
let v = vec![1, 2, 3];

初期値がある場合は vec! マクロを使用できる、この場合は型アノテーションは不要である。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Reading Elements of Vectors

コマンド
cargo new vectors
cd vectors
src/main.rs
fn main() {
    let v = vec![1, 2, 3, 4, 5];

    let third = &v[2];
    println!("The third element is {third}");

    let third = v.get(2);
    match third {
        Some(third) => println!("The third element is {third}"),
        None => println!("There is no third element."),
    }
}
コンソール出力
The third element is 3
The third element is 3
  • ベクタの要素を取得するには添え字アクセスか get メソッドを使用する。
  • 添え字アクセスの場合は範囲を超えているとパニックになるが get メソッドの場合は戻り値が Option<T> 型なので match 式でコントロールできる。
src/main.rs
fn main() {
    let mut v = vec![1, 2, 3, 4, 5];

    let third = &v[2];
    v.push(6);
    println!("The third element is {third}");
}

上記のコード例では借用チェッカーにより下記のエラーメッセーが表示される。

コンソール出力
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> src/main.rs:5:5
  |
4 |     let third = &v[2];
  |                  - immutable borrow occurs here
5 |     v.push(6);
  |     ^^^^^^^^^ mutable borrow occurs here
6 |     println!("The third element is {third}");
  |                                    ------- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
  • 前に学んだ通り Rust では同時に存在できるのは 1 つの可変参照か 1 つ以上の不可変参照のいずれかだけなのでエラーになる。
  • v.push(6); が最後の行になるとコンパイルできるようになる。
  • ベクタでは要素の追加などによってメモリを再配置する可能性があり、再配置前に作成された参照は無効になる可能性がある。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Iterating over the Values in a Vector

for 文を使うことでベクタを走査できる、下記の例では各要素への不可変参照を取得している。

src/main.rs
fn main() {
    let v = vec![100, 32, 57];
    for i in &v {
        println!("{i}");
    }
}
コンソール出力
100
32
57

可変参照を取得するには & の代わりに &mut を使う。

src/main.rs
fn main() {
    let mut v = vec![100, 32, 57];

    for i in &mut v {
        *i += 50;
    }

    for i in v {
        println!("{i}");
    }
}
コンソール出力
150
82
107

ちなみに &&mut もつけないことが可能なようだ。

要素の中身を書き換えるために * 演算子を使っているがこれについては後のチャプターで学ぶ。

for ループ内でベクタの要素を増減させようとするとコンパイルエラーになる。

    for i in &mut v {
        *i += 50;
        v.pop();
    }
エラーメッセージ
error[E0499]: cannot borrow `v` as mutable more than once at a time
  --> src/main.rs:21:9
   |
19 |     for i in &mut v {
   |              ------
   |              |
   |              first mutable borrow occurs here
   |              first borrow later used here
20 |         *i += 50;
21 |         v.pop();
   |         ^ second mutable borrow occurs here

For more information about this error, try `rustc --explain E0499`.
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Using an Enum to Store Multiple Types

https://doc.rust-lang.org/book/ch08-01-vectors.html#using-an-enum-to-store-multiple-types

ベクタ単体では 1 つのデータ型しか扱えないが列挙型と組み合わせることで複数のデータ型を扱えるようになる。

src/main.rs
enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}

fn main() {
    let row = vec![
        SpreadsheetCell::Int(3),
        SpreadsheetCell::Text(String::from("blue")),
        SpreadsheetCell::Float(10.12),
    ];

    for cell in &row {
        match cell {
            SpreadsheetCell::Int(v) => println!("Int: {v}"),
            SpreadsheetCell::Text(v) => println!("String: {v}"),
            SpreadsheetCell::Float(v) => println!("Float: {v}"),
        }
    }
}
コンソール出力
Int: 3
String: blue
Float: 10.12

もしコンパイル時点でどのような型を格納するかを確定できない場合はトレイトオブジェクトを使えるらしい、これについてはチャプター 17 で学ぶ。

ベクタの便利なメソッドについては公式ドキュメントで詳しく説明されている。

https://doc.rust-lang.org/std/vec/struct.Vec.html

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Dropping a Vector Drops Its Elements

ベクタもスコープから外れるとメモリが解放され有効ではなくなる。

{
    let v = vec![1, 2, 3, 4];
} // <- v がスコープから外れてメモリが解放される。

ベクタが解放される時、ベクタ内の全ての要素も同時に解放される。

借用チェッカーはベクタ内の要素への参照がベクタ自体が有効である間だけ使用されていることを確認してくれる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Storing UTF-8 Encoded Text With Strings

https://doc.rust-lang.org/book/ch08-02-strings.html

初心者にとって Rust の文字列は次の 3 つの理由でハマりやすい。

  • 起こり得るエラーを表示する Rust の性質
  • プログラマーが考えているより文字列が複雑なデータ構造であること
  • UTF-8

文字列はバイト列なのでコレクションの 1 つではあるが、計算機が扱うバイトと人間が扱う文字に違いがあるので添え字アクセスなどで注意すべき点がある。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

What Is a String?

https://doc.rust-lang.org/book/ch08-02-strings.html#what-is-a-string

  • Rust では言語の機能として文字列スライスが提供されている、これは通常 &str のように参照と組み合わせて使用する。
  • 文字列スライスとは UTF-8 エンコードされた文字列データへの参照である。
  • 例えば文字列リテラルはバイナリに含まれており、リテラルへのスライスはバイナリのアドレスへの参照となる。
  • 一方、String 型は Rust の標準ライブラリから提供されている。
  • String 型は追加・変更が可能でプログラムで所有された UTF-8 エンコードされた文字列データの型である。
  • Rust で「文字列」と言う場合は String 型と文字列スライス参照のいずれかを指す。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Creating a New String

文字列はバイトのベクタとして実装されており、保証・制約・能力が若干追加されている。

src/main.rs
fn main() {
    let data = "initial contents";
    let s = data.to_string();
    let s = "initial contents".to_string();
    let s = String::from("initial contents")
}

Display トレイトが実装されている型であれば to_string メソッドを利用できる。

また、文字列リテラルの場合は String::from 関連関数が利用できる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Appending to a String with push_str and push

https://doc.rust-lang.org/book/ch08-02-strings.html#appending-to-a-string-with-push_str-and-push

src/main.rs
fn main() {
    let mut s1 = String::from("foo");
    s1.push_str("bar");

    let s2 = String::from("baz");
    s1.push_str(&s2);

    s1.push('.');

    println!("{} {}", s1, s2);
}

実行すると forbarbaz. baz と表示される。

push_str メソッドに String インスタンスを渡す場合は参照を使う必要がある。

push メソッドには文字列ではなく文字を渡す。