rust
OriginalWACULDay 9

知っていないと中々思いつかないrustのイディオム

この記事はwacul アドベントカレンダーの9日目の記事です。

この頃、rustやっているのですが、知っていないと中々思いつかないイディオムに遭遇したのでメモします。2ついきます。

freezing

文法的な話からはいると、https://rustbyexample.com/scope/borrow/freeze.html#freezing の技法のことです。雑に言うと、スコープ内では、変数をmutable->immutableにfreezingでき、一旦freezeした後は、mutableに戻せない、と言っています。

一見、大したことない事項のようですが、これをうまく利用するとコードがスマートにかけたりすることもあるので、侮れない。

例えば、こういう場合を考えます1

ダメな例
extern crate reqwest;
use std::io;
use std::path::{PathBuf};

fn main() {
    let mut response = reqwest::get(url).unwrap();
    let save_dir = PathBuf::from("./data");
    // https://docs.rs/reqwest/0.8.1/reqwest/struct.Response.html#method.url
    // fn url(&self) -> &Url なので、responseがimmutable borrowになる
    let fname = response.url()
            .path_segments()
            .unwrap()
            .last()
            .unwrap(); // &str
    let fname = save_dir.as_ref().join(fname);
    // responseをmutableとしてborrowする
    io::copy(&mut response, &mut File::create(fname).unwrap()).unwrap();
}

pub fn copy<R: ?Sized, W: ?Sized>(reader: &mut R, writer: &mut W)なので、両方&mutが要求されます。

これを実行します:

error[E0502]: cannot borrow `response` as mutable because it is also borrowed as immutable
  --> src/ch03/ch03.rs:18:15
   |
10 |     let fname = response.url()
   |                 -------- immutable borrow occurs here
...
18 |     copy(&mut response, &mut File::create(fname).unwrap()).unwrap();
   |               ^^^^^^^^ mutable borrow occurs here
19 | }
   | - immutable borrow ends here

原因は、一度immutableとして貸し出してしまったので、再度mutableにすることができないからです。

どうするかなんですが、同じスコープ内にあるからダメなのであって、スコープを制限してしまいます:

良い例
extern crate request;
use std::io;
use std::path::{PathBuf};

fn main() {
    // define mut to use as mutable later
    let mut response = reqwest::get(url).unwrap();
    let save_dir = PathBuf::from("./data");
    // scoped
    let fname = {
        // fn url(&self) -> &Url
        let fname = response.url()
            .path_segments()
            .unwrap()
            .last()
            .unwrap();
        save_dir.as_ref().join(fname) // セミコロンがついていないので、ここの値がfnameにsetされる
    };
    io::copy(&mut response, &mut File::create(fname).unwrap()).unwrap();
}

これで大丈夫なのだ!
このように、スコープ狭めるとE0502が回避できる場合があり、一つの武器として知っておくと、いざという時にちょくちょく役立つ。
素敵ですね:cocktail:

Struct fields that can be loaned or "taken"2

これは、fn func(&mut self)のように、引数が借用型であって欲しいorそうせざるを得ないんだけど、funcの内部で、実体を借りたい場合に、たまーに見かけるテクニックです。

Bは適当な型、consume(self: B)とします。結論から書くと、こんな風に解決します。

イディオム
struct A {
    // Box型ではなく、Option型とする。
    area: Option<B>,
    name: String,
}

impl A {    
    fn func(&mut self) {
        if let Some(b) = self.area.take() {
            b.consume(); // bはB型で所有権持つので、大丈夫。
        }
    }
}

fn take(&mut self) -> Option<T>の中で、&mut selfのように借用型で構わないけれども、Tという実体を取り出せるところが一番のポイントです(unwrapやexpectも値を取り出しますが、selfなので、今回の要件では使用できない) 後は、Boxではなく、Optionにする必要があることも今回のポイントです。

特に名前なさそうなので、本記事では、takeイディオムと呼びます。


これだけでは、メリットがよくわからんと思うので、実践例を見ていきましょう。
https://doc.rust-lang.org/book/second-edition/ch20-06-graceful-shutdown-and-cleanup.html#graceful-shutdown-and-cleanup での公式の例。文脈はかなり端折っているので、詳細は上記のリンクを参照のこと:

ダメな例
use std::thread;

type Worker = InnerHandle;
type InnerHandle = thread::JoinHandle<()>;

pub struct ThreadPool {
    workers: Vec<Worker>,
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        // &mutまたは、&を明示的につけないと cannot move out of hereとなる
        (&mut self.workers)
            .into_iter()
            // s: &Workerだが、joinメソッドはself, つまり、Worker型を要求するので、
            // cannot move out of borrowed content となる
            .for_each(|s| s.join().unwrap());

    }
}

スレッドプールの実装で本記事と関係のある部分を抜き出しました。
まず、自作のThreadPoolを作っており、安全にThreadPoolを終わらせたいというモチベーションで、Dropトレイトを実装しようとしている場面です。なので、dropメソッドを実装する必要があります。
dropメソッドは、fn drop(&mut self)を要求するので、&mut self(借用型)ですが、内部で、スレッドがすべて終了するまで待つ必要があるので、std::thread::JoinHandle::joinメソッドを使用する必要があります。しかしながら、fn join(self) -> Result<T>(第一引数に所有権が要求される)なので、上のコードを動かすと、

error[E0507]: cannot move out of borrowed content
  --> src/main.rs:17:27
   |
17 |             .for_each(|s| s.join().unwrap());
   |                           ^ cannot move out of borrowed content

というエラーになります。これを回避するためのテクニックとして、takeイディオムを用いて次のようにします:

オッケーな例
use std::thread;

// Optionでラップする
type Worker = Option<InnerHandle>;
type InnerHandle = thread::JoinHandle<()>;

pub struct ThreadPool {
    workers: Vec<Worker>,
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        (&mut self.workers)
            .into_iter()
            .for_each(|worker| {
                if let Some(thread) = worker.take() {
                    // joinはFnOnceで、threadは所有権を持っているので大丈夫
                    thread.join().unwrap();
                }
            })
    }
}

素敵ですね:wine_glass:

他の公式の例としては、https://doc.rust-lang.org/book/second-edition/ch17-03-oo-design-patterns.html#defining-post-and-creating-a-new-instance-in-the-draft-staterequest_reviewがそうで、同じような動機でtakeイディオムを用いています。