🦀

Rustの勉強 2日目

2022/09/28に公開約13,000字

Rustlingsの環境設定をする

公式サイト に教材としてRustlingsへのリンクがあったのでとりあえずそれをやることにした。

$ curl -L https://raw.githubusercontent.com/rust-lang/rustlings/main/install.sh | bash -s rustlings
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  4591  100  4591    0     0  17429      0 --:--:-- --:--:-- --:--:-- 18146
Let's get you set up with Rustlings!
Checking requirements...
SUCCESS: Git is installed
SUCCESS: cc is installed
SUCCESS: rustup is installed
SUCCESS: Rust is installed
SUCCESS: Cargo is installed
SUCCESS: Rust is up to date
Cloning Rustlings at rustlings...
Checking out version tags/5.2.1...
Installing the 'rustlings' executable...
  Installing rustlings v5.2.1 (/Users/yoshifumi/personal/rustlings)
    Updating crates.io index
  Downloaded argh v0.1.9
  Downloaded fsevent v0.4.0
  Downloaded notify v4.0.17
  Downloaded indicatif v0.16.2
  Downloaded toml v0.5.9
  Downloaded number_prefix v0.4.0
  Downloaded argh_derive v0.1.9
  Downloaded console v0.15.1
  Downloaded argh_shared v0.1.9
  Downloaded glob v0.3.0
  Downloaded filetime v0.2.17
  Downloaded walkdir v2.3.2
  Downloaded terminal_size v0.1.17
  Downloaded same-file v1.0.6
  Downloaded fsevent-sys v2.0.1
  Downloaded 15 crates (315.8 KB) in 0.94s
   Compiling proc-macro2 v1.0.44
   Compiling unicode-ident v1.0.4
   Compiling libc v0.2.133
   Compiling quote v1.0.21
   Compiling syn v1.0.101
   Compiling serde_derive v1.0.145
   Compiling memchr v2.5.0
   Compiling serde v1.0.145
   Compiling unicode-width v0.1.10
   Compiling regex-syntax v0.6.27
   Compiling argh_shared v0.1.9
   Compiling heck v0.4.0
   Compiling serde_json v1.0.85
   Compiling cfg-if v1.0.0
   Compiling once_cell v1.15.0
   Compiling bitflags v1.3.2
   Compiling same-file v1.0.6
   Compiling ryu v1.0.11
   Compiling number_prefix v0.4.0
   Compiling itoa v1.0.3
   Compiling lazy_static v1.4.0
   Compiling home v0.5.3
   Compiling glob v0.3.0
   Compiling walkdir v2.3.2
   Compiling aho-corasick v0.7.19
   Compiling regex v1.6.0
   Compiling terminal_size v0.1.17
   Compiling fsevent-sys v2.0.1
   Compiling filetime v0.2.17
   Compiling console v0.15.1
   Compiling fsevent v0.4.0
   Compiling notify v4.0.17
   Compiling indicatif v0.16.2
   Compiling argh_derive v0.1.9
   Compiling argh v0.1.9
   Compiling toml v0.5.9
   Compiling rustlings v5.2.1 (/Users/yoshifumi/personal/rustlings)
    Finished release [optimized] target(s) in 15.28s
  Installing /Users/yoshifumi/.cargo/bin/rustlings
   Installed package `rustlings v5.2.1 (/Users/yoshifumi/personal/rustlings)` (executable `rustlings`)
All done! Run 'rustlings' to get started.

さて始めるかと思ってプロジェクトルートのREADMEを見てみたら、とりあえず exercise ディレクトリにREADMEがあるからそれを読め、と書いてある。で exercise/README を見たらぶっきらぼうに次の表が置いてあった。

# Exercise to Book Chapter mapping

| Exercise               | Book Chapter        |
| ---------------------- | ------------------- |
| variables              | §3.1                |
| functions              | §3.3                |
| if                     | §3.5                |
| primitive_types        | §3.2, §4.3          |
| vecs                   | §8.1                |
| move_semantics         | §4.1, §4.2          |
| structs                | §5.1, §5.3          |
| enums                  | §6, §18.3           |
| strings                | §8.2                |
| modules                | §7                  |
| hashmaps               | §8.3                |
| options                | §10.1               |
| error_handling         | §9                  |
| generics               | §10                 |
| traits                 | §10.2               |
| tests                  | §11.1               |
| lifetimes              | §10.3               |
| standard_library_types | §13.2, §15.1, §16.3 |
| threads                | §16.1, §16.2, §16.3 |
| macros                 | §19.6               |
| clippy                 | n/a                 |
| conversions            | n/a                 |

Book Chapter と書いてあるのはおそらくプロジェクトルートのREADMEに書いてあった「初心者ならこれを読め」という公式の本だと思うので、仕方ないからそれを読み進めていくことにする。

少し版が古いけど日本語版もあった。

The Rust Programming Language 日本語版を読んでいく

まえがき

ざっと眺めておしまい

はじめに

ざっと眺めて、頭から読んでけばいいことがわかったのでそのとおりにする。

事始め

「1.1 インストール」の節はもうインストールは終わってるので飛ばした。

「1.2 Hello, World!」も公式サイトのGetting Startedでやった内容が前半なので飛ばす。後半でいくつか確認事項を理解。

  • main 関数がエントリーポイント
  • 関数は必ず波括弧でブロックを書く必要がある
  • インデントは4スペース
  • ! はマクロの呼び出し

ナイーブに rustc でコンパイルする方法をここで紹介してるけど、依存解決とかが面倒になるからあとでCargoを使ってビルドする方法を紹介すると言って終わり。

「1.3 Hello, Cargo!」で、直前の節の話題を回収する。

まず、前提として「CargoはRustにおけるビルドシステム兼パッケージマネージャ」であるという理解ができた。

cargo new <project name>

で新しいディレクトリを作成して、必要なファイルのスケルトンを作ってくれる(Cargo.tomlsrc サブディレクトリ)。

cargo build

で、依存関係も含めてビルドしてくれる。このとき、細々とした依存関係のバージョンを Cargo.lock というファイルを作って記録してくれる。

cargo run

を行うと、cargo build したあとにプログラム自体の実行まで行ってくれる。ここまでの流れはGoでいうところの go buildgo run に似ている。

数当てゲームのプログラミング

いよいよハンズオン形式のプロジェクトに入ったので進めていく。

cargo new gussing_game
cd guessing_game

で、main.rs を修正して次のようなコードにする。

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);
}

実行してみる

$ cargo run
   Compiling guessing_game v0.1.0 (/Users/yoshifumi/personal/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.22s
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
10
You guessed: 10

その後、解説が書いてある

  • 標準ライブラリのうち、自動で取り込まれるものが prelude と呼ばれる
  • それ以外の場合は use 文で使用を宣言する
  • let キーワードで変数を宣言し、初期値を束縛する。変数はデフォルトではイミュータブル。変数をミュータブルで宣言したかったら mut キーワードも付ける必要がある。
  • :: は型の関連関数(associated function)の呼び出しに使う。
  • & は参照。read_line&mut String を受け取るので、&mut guess を渡す。ここで注意すべきは参照もデフォルトはイミュータブルなので、参照をミュータブルにしたかったら &mut をつける必要がある。(ここでは &guess はダメ)
  • read_lineio::Result という結果型を返し、結果型は OkErr となるEnum型である。io::Result 型には expect というメソッドがあるので、これを呼び出してエラー処理をする。
    • Ok だった場合には expect はただ読み込んだバイト数を返す。
  • {}println! マクロのプレースホルダー。

「秘密の数字を生成する」の節に来た。手で Cargo.toml を修正して rand クレートを入れる説明をしているが、昨日の設定で cargo-edit をインストールしたので次のコマンドで入れてしまおうと思う。そしてそのまま本文と同様に cargo build まで行う。

$ cargo add rand
    Updating crates.io index
      Adding rand v0.8.5 to dependencies.
             Features:
             + alloc
             + getrandom
             + libc
             + rand_chacha
             + std
             + std_rng
             - log
             - min_const_gen
             - nightly
             - packed_simd
             - serde
             - serde1
             - simd_support
             - small_rng

$ cat Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"

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

[dependencies]
rand = "0.8.5"

$ cargo build
  Downloaded ppv-lite86 v0.2.16
  Downloaded rand_chacha v0.3.1
  Downloaded rand_core v0.6.4
  Downloaded getrandom v0.2.7
  Downloaded rand v0.8.5
  Downloaded 5 crates (176.1 KB) in 0.73s
   Compiling libc v0.2.133
   Compiling cfg-if v1.0.0
   Compiling ppv-lite86 v0.2.16
   Compiling getrandom v0.2.7
   Compiling rand_core v0.6.4
   Compiling rand_chacha v0.3.1
   Compiling rand v0.8.5
   Compiling guessing_game v0.1.0 (/Users/yoshifumi/personal/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 3.27s

main.rs のコードは変更していないけれども、Cargo.toml[dependencies]rand が入ったので、 cargo build でレジストリから rand とその依存クレートを持ってきているのが分かる。

あとの説明で Cargo.lock はGoで言うところの go.sum みたいなものだとわかった。依存しているクレートを更新したい場合には cargo update で更新可能。

ここまでわかったところでいよいよ main.rs を変更する。

use rand::Rng;
use std::io;

fn main() {
    println!("Guess the number!");
    let secret_number = rand::thread_rng().gen_range(1..101);

    println!("The secret number is {}", secret_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);
}

このコードについての解説で知ったことは

  • rand::Rng というの型ではなくトレイト(あとで解説されるとのこと。どうも特定のメソッド群をまとめたものっぽい?)
  • 範囲式という 開始..終了 の形式の書き方で数値の範囲を指定

これを実行して、数字が実行ごとにランダムに変わっていることがわかった。

$ cargo run
   Compiling guessing_game v0.1.0 (/Users/yoshifumi/personal/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is 83
Please input your guess.
8
You guessed: 8

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is 14
Please input your guess.
3
You guessed: 3

いよいよこれで乱数と入力値を比較してゲームにする部分を実装する。コードを解説読みながら実装した。

  • Rustは強い型付けなので型が合わないとコンパイルできない。
  • 変数を型推論でなく型付きで宣言するときは let 変数名: 型名 で宣言。
  • Rustでは変数のシャドーイングが可能。違う型でも問題ない。
  • match キーワードでパターンマッチが可能。各アーム(条件節)はカンマ区切りでつなぐ。
use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Guess the number!");
    let secret_number = rand::thread_rng().gen_range(1..101);

    println!("The secret number is {}", secret_number);
    println!("Please input your guess.");

    let mut guess = String::new();
    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");
    let guess: u32 = guess.trim().parse().expect("Please type a number!");

    println!("You guessed: {}", guess);
    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => {
            println!("You win!");
            break
        },
    }
}

これを実行して、ちゃんと動作することを確認。

cargo run
   Compiling guessing_game v0.1.0 (/Users/yoshifumi/personal/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.22s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is 92
Please input your guess.
92
You guessed: 92
You win!

次は繰り返しの実装。ただ繰り返しを実行するだけだと終了できなくなってしまったり、不正入力があると意図せずプログラムが終了してしまうので、以下のように修正する。

  • guess.trim().parse() の返り値で match して、Result型でパターンマッチ。
    • Err 型の値に興味がない場合はアンダースコアでスルーできる。
  • guess.cmp() の返り値が Ordering::Equal だった場合に break する
    • パターンマッチで複数の処理をさせたい場合にはブロックを定義すれば良い。

すると次のようになる。

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Guess the number!");
    let secret_number = rand::thread_rng().gen_range(1..101);

    println!("The secret number is {}", secret_number);

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

        let mut guess = String::new();
        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");
        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

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

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        };
    }
}

これを実行すると

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is 91
Please input your guess.
11
You guessed: 11
Too small!
Please input your guess.
100
You guessed: 100
Too big!
Please input your guess.
91
You guessed: 91
You win!

ちゃんと動いた。最後に、最初の方で秘密の数字を表示している行を消して完成。

 cargo run
   Compiling guessing_game v0.1.0 (/Users/yoshifumi/personal/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.21s
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
1
You guessed: 1
Too small!
Please input your guess.
100
You guessed: 100
Too big!
Please input your guess.
50
You guessed: 50
Too big!
Please input your guess.
25
You guessed: 25
Too big!
Please input your guess.
12
You guessed: 12
Too small!
Please input your guess.
19
You guessed: 19
Too big!
Please input your guess.
16
You guessed: 16
Too big!
Please input your guess.
14
You guessed: 14
Too big!
Please input your guess.
13
You guessed: 13
You win!

無事に2章終了。わかりやすかった。

Discussion

ログインするとコメントできます