【5日目】Rust入門者に贈るRust入門者がRustと闘う物語 -Enumとmatchとif let-


最近、巷で話題のRust。システムプログラミング言語などの低レイヤー言語は触ったことがない筆者が、裸一貫で闘っていく、そんな物語です。

前回の記事

構造体があまりにもあっさりなので5日目もやっちゃいます。寝てました。

Enumとパターンマッチングについてです。URL

はじめに

※物語共通で使用するので不要な方は飛ばしてください。

開発環境や参考サイト、学習フローチャートなどの概要について話します。

本物語について

私は、今業務で使用している言語や趣味でしようしている言語を含め、使用している全ての言語を独学で勉強してきました(せざるを得なったためですが)。

そして、毎回のことのように何度も壁にぶつかり、やっとの思いで扱えるようになるため、プログラミング言語の取得は、修行のようなものだと考えています。

これは私自身の能力不足なのかもしれませんが、大小はあるにしても誰しもが共通して感じるものなのかなと思いまして、後にRustに入門する方々に参考になればと思い、物語として記録しておきます。

本物語の目的

・初心者としてありがちな凡ミスやくだらないエラーを赤裸々に記録し、後続する方々の参考にして頂くため
・自分用のメモ

本物語での記述について

ファイル名:わかりやすく日本語で書いていますが、自らの環境で真似するのはお勧めできません。

表現の置換:私の最も得意として楽に記述できる言語がJavaScript(特にES6の書き方が好きです)のため、多くの部分でJavaScriptで置換して表現します。多くの人がわかるようにPythonで置換しようと思ったのですが、Pythonの書き方は(個人的に)独特なので、これまた知名度の高いJavaScriptで置換しようと考えました。

エラーやつまづきポイント:敢えて全てのエラーの記録をお伝えしますし、遠回りの手法をお伝えすることがあります。これは私を反面教師にして同じ轍を進まなくて済むためと考えています。

※なお、全体を共通して「厳密性」より「なんとなく」を優先することが多々あるかと思います。その点ご了承ください。

対象読者

・今からRustに入門しようとしている方
・プログラミング自体はある程度できるけど、組み込み系やOS設計などの低レイヤーの部分は触れたことがないという方
・開発自体は好きだが、言語がある程度使えるまでの道のりがしんどい方

Why Rust

・業務でWebAssemblyが必要になりそうだった
・何か1つの言語を1から丁寧に学んでみたかった
・流行っていて今後本格的に低レイヤー言語としての地位を獲得しそうな雰囲気がした
・調べると、難しこと以外でデメリットが見当たらなかった
・名前が良い響き

開発環境とスキルセット

再現性は不明ですが、念のため記しておきます。

開発環境

PC: macOS Catalina 10.15.2
開発エディタ: VSCode
Rust: 1.41.0

スキルセット

基本的にWebフロントエンドとソフトウェア開発(金融系など)がメインですが、、

業務で使用
Javascript(デフォルト)(Node.js, Vue.js, React.js, Jquery, etc)
Python(主にAPIなどのバックエンド)(あまりフレームワークは使わない)
Swift(単純な業務アプリ等)
PHP(Wordpressカスタマイズとか頼まれたら)(比較的苦手)

趣味程度
Golang(速度が必要な処理でAPI作成)
Julia(計量経済学を学習した際に)
R(計量経済学を学習した際に)

主にWebのお仕事をもらいつつ、頼まれたら業務に必要なアプリ作ったりしています。
Swiftが一応システムプログラミング言語ですが、そこまで複雑じゃない業務管理アプリ程度しか作ったことがないので、システムプログラミングエンジニアとしては初心者です。
Golangは、Node.jsで速度処理に困った際に比較で学んだのがきっかけで業務での使用はないです。いつかお仕事できればいいな。

ちなみに、よく聞かれるので、JavaとRubyの使用経験についても記載しておきます。
・Java(Kotlin等も)は無茶振りで超単純なアプリを作ったくらいで、趣味にも入らないくらいです。
・Rubyは、変なセミナーに誘われることが多かったので怖くて触れてません。

学習スケジュール(予定)

そもそも私自身Rustを学ぶ目的というか必要がありまして、実は金融系ソフトウェアの開発にあたりWebAssemblyを使うかもしれない可能性があり、そのため最終的にWebAssembly系で何かできたらと思っています。

恥ずかしながら、こうしてスケジュールを組んで学習というのが初でして、いまいち書き方がわかりませんが、以下のようにしております。

基礎学習

n日目 やること
1日目 Rustと周辺ツールのインストールとセットアップ
2日目 3. 普遍的なプログラミング概念
3日目 4. 所有権を理解する
4日目 5. 構造体を使用して関連のあるデータを構造化する
5日目 6. Enumとパターンマッチング
6日目 7. モジュール
7日目 8. 一般的なコレクション
8日目 9. エラー処理
9日目 10. ジェネリック型、トレイト、ライフタイム
10日目 11. テスト
11日目 13. 関数型言語の機能: イテレータとクロージャ
12日目 14. CargoとCrates.ioについてより詳しく
13日目 15. スマートポインタ
14日目 16. 恐れるな!並行性
15日目 17. Rustのオブジェクト指向プログラミング機能
16日目 18. パターンは値の構造に合致する
17日目 19. 高度な機能

こちらを参考にして、スケジュールを組んでいます。

基本的にセクション毎を学んでいきます。量などにムラがあると思いますが、どうにかやり切る覚悟でいきましょう。

こちらはプロジェクト学習で最終的な復習をする前提で、一旦込み入ったことなどはスルーするかもしれないです。個人的に、80%くらいの精度で学習して、最後に実践的にボロボロになって、20%の総和を回収するのがなんだかんだ効率的だと思いますし、何より前に進んでいる実感からモチベが湧くので(ただ筋というかコアの部分は理解する必要があると思いますが)。

プロジェクト学習

n日目 やること
1つ目 2. 数当てゲームをプログラムする
2つ目 12. 入出力プロジェクト: コマンドラインプログラムを構築する
3つ目 20. 最後のプロジェクト: マルチスレッドのWebサーバを構築する

これらは1日でやり切れたらいいですが、文量なども多いので頑張れたらとしましょう。また、日数ではなくて、わからない点や疑問点をこのプロジェクトを通してゼロにしたいと考えているため、1つに1週間くらいかかることも想定されます。

付録

一旦スルーします。必要に応じて見ます。

応用学習

未定ですが、WebAssemblyで何かしたい。

参考サイト等(仮)

Rustチュートリアルがしっかりしてるサイト

何をやったか(5日目)

6. Enumとパターンマッチング

Enum

定義

一見して構造体と似ている。

src/main.rs
enum IpAddrKind {
    V4,
    V6,
}

使用方法

「::」で呼び出し。

src/main.rs
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

より便利な定義

構造体では独立して定義しないと表現できないデータ型。

src/main.rs
enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);

let loopback = IpAddr::V6(String::from("::1"));

自由すぎるEnum

とても自由で好きです。

src/main.rs
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

・Quitには紐付けられたデータは全くなし。
・Moveは、中に匿名構造体を含む。
・Writeは、単独のStringオブジェクトを含む。
・ChangeColorは、3つのi32値を含む。

structだと分解する必要がある上にごちゃごちゃしてしまう。

implもあるよ

以下のようにstruct同様メソッドを作り出せます。
注意点は、&selfが登場するのに「.」ではなくて「::」記法を使用していることでしょう。

src/main.rs
impl Message {
    fn call(&self) {
        // method body would be defined here
        // メソッド本体はここに定義される
    }
}

let m = Message::Write(String::from("hello"));
m.call();

NullがないRustとOptionの誕生

これは驚きでした。Rustにはnullがないようです。この節は結構大切そうな雰囲気がするので読み込みます。

プログラミング言語のデザインは、しばしばどの機能を入れるかという観点で考えられるが、 除いた機能も重要なのです。Rustには、他の多くの言語にはあるnull機能がありません。 nullとはそこに何も値がないことを意味する値です。nullのある言語において、 変数は常に二者択一どちらかの状態になります: nullかそうでないかです。

最強のOption<T>

nullがない代わりに、 値が存在するか不在かという概念を表現するのにこんなものがあるようです。<T>というのは、ジェネリック型引数と言うらしく後に深く学べるようです。

最初に簡潔に説明すると、

Option<T>の列挙子はそのまま使える

<T>を持つ場合はあらゆるデータ型を一つ持てる

<T>を持たない場合はコンパイラにどういう型が入るか明示する必要がある

Option<T>を通常のデータ型のように処理する際にTに変換する必要があったりする

src/main.rs
enum Option<T> {
    Some(T),
    None,
}

Option<T>は有益すぎて、初期化処理(prelude)にさえ含まれています。つまり、明示的にスコープに導入する必要がないのです。 さらに、列挙子もそうなっています: SomeとNoneをOption::の接頭辞なしに直接使えるわけです。 ただ、Option<T>はそうは言っても、普通のenumであり、Some(T)とNoneもOption<T>型のただの列挙子です。

<T>という記法は、まだ語っていないRustの機能です。これは、ジェネリック型引数であり、ジェネリクスについて詳しくは、 第10章で解説します。とりあえず、知っておく必要があることは、<T>は、Option enumのSome列挙子が、 あらゆる型のデータを1つだけ持つことができることを意味していることだけです。こちらは、 Option値を使って、数値型や文字列型を保持する例です。

Optionの呼び出し不要で自由に使える

src/main.rs

let some_number = Some(5);
let some_string = Some("a string");

let absent_number: Option<i32> = None;

ちなみに僕は以下のようにしてエラーが出て1時間くらい悩みました。

src/main.rs
enum Option<T> {
    None,
    Some(T),
}
fn main() {
    let some_number = Some(5);
    let some_string = Some("a string");
    let absent_number: Option<i32> = None;
}

ググった結果以下のようにenumの定義を外すことでエラーが無事実行できました。

src/main.rs
fn main() {
    let some_number = Some(5);
    let some_string = Some("a string");
    let absent_number: Option<i32> = None;
}

以下のように標準ライブラリに定義されています。

というのは、既に内蔵されているという意味なのですね。勘違いしていました。要は、既に定義されているのに再定義しようとして名前競合が起きたようです。反省です。

明示的にOption<T>Tを変換する必要があるコード例

src/main.rs
let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = x + y; //エラーが出る

match

複雑なif/else構文をスマートに書ける。他言語でもあるswitchなどのRust版。Enumが一つの値しか取らないので、matchはbreak書かなくてもいい雰囲気です。

コード例

Coinの列挙子のどれか一つに該当して、それぞれの場合で=>の先の数字を返すコード。

src/main.rs
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u32 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

JavaScriptのアロー関数のようですね。

引数を扱う時

以下のようにかけるようです。

src/main.rs
#[derive(Debug)] // すぐに州を点検できるように
enum UsState {
    Alabama,
    Alaska,
    // ... などなど
}

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

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

完全アロー関数ですね。ここまで自由だと表現できることが多そうな感じがします。

Option<T>とのmatch

コード例

src/main.rs
fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

こちらは動きます。ただRustでは、None => Noneを削除してしまうとエラーになるようです。

エラー例.sh
error[E0004]: non-exhaustive patterns: `None` not covered
(エラー: 包括的でないパターン: `None`がカバーされてません)
 -->
  |
6 |         match x {
  |               ^ pattern `None` not covered

matchは包括的でないといけないようです。

Option<T>以外で、いちいち列挙するのが面倒な時

matchは包括的ではない場合ではエラーが出ます。そのため、u8など取りうる値が0~255まである場合などを考えると、全て列挙するのは限界があります。

Rustには、それを解消するプレースホルダー_があるようです。

src/main.rs
fn main() {
    let some_u8_value = 0u8;
    match some_u8_value {
        1 => println!("one"),
        3 => println!("three"),
        5 => println!("five"),
        7 => println!("seven"),
        _ => (),
    }
}

他言語でも頻出なので見ただけでわかる方も多いかもしれませんが、この場合は_で1,3,5,7以外のu8で何も出力されないというわけですね。

if letで簡潔な制御フロー

matchで特定の値(一つの値)だけ処理したいときに便利な構文

コード例

src/main.rs
fn main() {
    let some_u8_value = Some(0u8);
    // _ => ()を足すのが毎回面倒
    match some_u8_value {
        Some(3) => println!("three"),
        _ => (),
    }

    // if letを使うことで簡潔に書ける
    if let Some(3) = some_u8_value {
        println!("three");
    }
}

少し発展的なコード例

src/main.rs
#[derive(Debug)] // すぐに州を点検できるように
enum UsState {
    Alabama,
    Alaska,
    // ... などなど
}
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}
fn main() {
    fn value_in_cents(coin: Coin) -> u32 {
        let mut count = 0;
        if let Coin::Quarter(state) = coin {
            println!("State quarter from {:?}!", state);
        } else {
            count += 1;
        }
        return count;
    }
}

ざっくり要約

・EnumはStructの上位互換で、あらゆるデータ型を柔軟に扱える

・Rustにはnullがなく、代わりにOption<T>というのがある

・matchでEnumで定義した値を分岐処理できる(関数も実行可能)

・matchの代わりにif letという(時に)簡潔表現もある

よくわからなかったこと、問題点など

・Enumについて、なぜimplで定義したメソッドのシグニチャに&selfがあるときでも「::」記法でいいのか。

Option<T>で定義した値を<T>に変換する方法

気をつけた方が良いこと

・matchは包括的でないといけない

Option<T>は定義されているので、再度定義するとエラーになる

まとめ

matchがとても便利でいいですね。

やはり、Option<T>に慣れられるかが少し不安ですが、慣れれるよう頑張りたいと思います。