Pop メソッド

push と同様,pop はリストを変更するメソッドです. push と異なるところは,実際に何かしらの値を返すことですね. pop はリストが空だったらどうするかという厄介なコーナーケースも扱う必要があります. このケースに対処するために,Option 型という頼もしい道具を使用します:

pub fn pop(&mut self) -> Option<i32> {
    // TODO
}

Option<T> は存在するかどうかわからない値を扱う列挙型です. Some(T)None かのいずれかの値を取ります. Link のときにやったように独自の enum を作ってもいいのですが,ユーザに戻り値が何なのか理解してほしいので, どこにでもあり 誰もが 知っている Option 型を使うのが得策です. 実際,Option は非常に基本的なものなので,すべてのファイルで暗黙のうちにスコープに取り込まれます. そのヴァリアントである SomeNone も同様です. (ですから,Option::None などと書く必要はないのです)

Option<T><T> は,OptionT に対するジェネリックであることを示しています. つまり,どんな型に対しても Option を作ることができるのです!

さて,この Link ですが,EmptyMore のどちらなのか,どうやって判断すればいいでしょう? match でパターンマッチしてみますか!

pub fn pop(&mut self) -> Option<i32> {
    match self.head {
        Link::Empty => {
            // TODO
        }
        Link::More(node) => {
            // TODO
        }
    };
}
> cargo build

error[E0308]: mismatched types
  --> src/first.rs:27:30
   |
27 |     pub fn pop(&mut self) -> Option<i32> {
   |            ---               ^^^^^^^^^^^ expected enum `std::option::Option`, found ()
   |            |
   |            this function's body doesn't return
   |
   = note: expected type `std::option::Option<i32>`
              found type `()`

おっと,pop は値を返さないといけないのに,まだやっていませんでしたね. None を返すこともできますが,この場合, 関数の実装が終わっていないことを示すために unimplemented!() を返す方が良いでしょう. unimplemented!() はマクロで (! はマクロを表します),これに到達するとプログラムがパニックします. (パニックとは,制御された方法でプログラムをクラッシュさせること).

pub fn pop(&mut self) -> Option<i32> {
    match self.head {
        Link::Empty => {
            // TODO
        }
        Link::More(node) => {
            // TODO
        }
    };
    unimplemented!()
}

無条件のパニックは,発散する関数 (diverging function) の例です. 発散する関数は呼び出し元に戻ることがないので,本来期待される値の型がなんであったとしても, 使用することができます. ここでは,Option<T> 型の代わりに unimplemented!() を使用しています.

また,プログラム中に return を書く必要はないことに注意してください. 関数の最後の式 (基本的には行) が,暗黙のうちに戻り値になります. これによって,すごく単純な関数をさらに単純にすることができます. 他の C 系の言語と同様に,return で早めに値を明示的に返すこともできます.

> cargo build

error[E0507]: cannot move out of borrowed content
  --> src/first.rs:28:15
   |
28 |         match self.head {
   |               ^^^^^^^^^
   |               |
   |               cannot move out of borrowed content
   |               help: consider borrowing here: `&self.head`
...
32 |             Link::More(node) => {
   |                        ---- data moved here
   |
note: move occurs because `node` has type `std::boxed::Box<first::Node>`, which does not implement the `Copy` trait
  --> src/first.rs:32:24
   |
32 |             Link::More(node) => {
   |                        ^^^^

おい Rust, 俺たちの邪魔をするな! いつものように,Rust がカンカンに起こっています. でも今回はちゃんと理由を教えてくれました. デフォルトでは,パターンマッチは中身を新しいブランチにムーブしようとしますが, ここでは self を値として所有していないため,これが行えないのです.

help: consider borrowing here: `&self.head`

ヘルプ: 借用することを検討してみてください: `&self.head`

Rust が言うには,match を参照にすれば直るみたいですね.🤷‍♀️ 試してみましょう:

pub fn pop(&mut self) -> Option<i32> {
    match &self.head {
        Link::Empty => {
            // TODO
        }
        Link::More(node) => {
            // TODO
        }
    };
    unimplemented!()
}
> cargo build

warning: unused variable: `node`
  --> src/first.rs:32:24
   |
32 |             Link::More(node) => {
   |                        ^^^^ help: consider prefixing with an underscore: `_node`
   |
   = note: #[warn(unused_variables)] on by default

warning: field is never used: `elem`
  --> src/first.rs:13:5
   |
13 |     elem: i32,
   |     ^^^^^^^^^
   |
   = note: #[warn(dead_code)] on by default

warning: field is never used: `next`
  --> src/first.rs:14:5
   |
14 |     next: Link,
   |     ^^^^^^^^^^

万歳,コンパイルが通ったぞ! それでは,TODO で残しておいたロジック部分を考えていきましょう. Option を作りたいので,そのための変数を作ります. Empty の場合には None を返さないといけませんね. More の場合には Some(i32) を返して,リストの先頭を変更する必要があります. おおまかにはこんな感じでどうでしょうか?

pub fn pop(&mut self) -> Option<i32> {
    let result;
    match &self.head {
        Link::Empty => {
            result = None;
        }
        Link::More(node) => {
            result = Some(node.elem);
            self.head = node.next;
        }
    };
    result
}
> cargo build
   Compiling lists v0.1.0 (/Users/ABeingessner/dev/temp/lists)
error[E0507]: cannot move out of borrowed content
  --> src/first.rs:35:29
   |
35 |                 self.head = node.next;
   |                             ^^^^^^^^^ cannot move out of borrowed content

ゴン!(頭を机に叩きつけた音)

共有参照しているものを,node からムーブしようとしてしまったようです.

立ち止まって,これからやろうとしていることが何なのか考え直すべきでしょう. 私たちがやりたいのは次のようなことです:

  • リストが空かどうかチェックする
  • 空であれば,None を返せばいいです
  • 空でなかったときは以下を実行します:
    • リストの先頭を削除する
    • elem を削除する
    • リストの先頭を next で置き換える
    • Some(elem) を返す

重要なことは,私たちが 削除 をしようとしているという点です. これはつまるところリストの先頭を(所有権のある)値として取得する必要があることを意味します. 共有参照である &self.head では,そんなことはできません. さらに self への参照は既にある可変参照「だけ」なので,ムーブするには置き換えるしかありません. どうやら,また我々は Empty ダンスをしているようです!

試してみましょう:

pub fn pop(&mut self) -> Option<i32> {
    let result;
    match mem::replace(&mut self.head, Link::Empty) {
        Link::Empty => {
            result = None;
        }
        Link::More(node) => {
            result = Some(node.elem);
            self.head = node.next;
        }
    };
    result
}
cargo build

   Finished dev [unoptimized + debuginfo] target(s) in 0.22s

ありえない….

警告を ひとつも 出さずにコンパイルできました!!!!!

個人的なこだわりですが,ここでコード整形をしましょう: result という変数を使いましたが,実はこれは不要です. 関数が最後の式で評価されるのと同じように,match の各ブロックも最後の式で評価されます. 通常はセミコロンを付けて文にすることでこの挙動を抑制し,ブロックの評価を空のタプル () にしますね. この ()push のように戻り値を宣言しない関数の返り値です.

そこで,代わりに pop をこう書くことができます:

pub fn pop(&mut self) -> Option<i32> {
    match mem::replace(&mut self.head, Link::Empty) {
        Link::Empty => None,
        Link::More(node) => {
            self.head = node.next;
            Some(node.elem)
        }
    }
}

より簡潔で慣用的なコードになりました. Link::Empty ブランチでは,中括弧 {} が完全に失われていることに注意してください. 評価する式が1つしかないときには省略できるんです.

cargo build

   Finished dev [unoptimized + debuginfo] target(s) in 0.22s

ちゃんとコンパイルも通りました.いいね!