LoginSignup
30
19

More than 3 years have passed since last update.

JavaScriptな人のためのRustのFuture入門

Last updated at Posted at 2019-08-20

RustでI/Oを扱うプログラムを書く機会がありました。非同期I/Oのほうがパフォーマンスがよくなるらしく、tokio というフレームワークがよく使われているとのこと。tokio では Future をベースとして非同期処理を書くようです。明るい Future を生み出していけばよいプログラムがかけそうですね :thumbsup: 。しかし、 tokioやFutureがなんもわからんという問題がありました。そこで、JavaScriptのFuture、Promiseと対比させてRustのFutureについてまとめます。

JavaScriptのPromise

昔のJavaScript(Node.js)では、ファイル読み込みやネットワークアクセス等のI/O待ちが発生するときはコールバックという仕組みを用いていました。人々は辛くなり、ES2015ではPromiseが導入されました。さらにES2017ではasync/awaitが導入され、一層I/Oの処理が書きやすくなりました。

ではまずはJavaScriptで簡単なPromiseを書いてみましょう。

const promise = new Promise((resolve, reject) => {
    console.log("promise called");
    resolve('success');
});

promise の中身のコードはPromiseを定義した時点でランタイムにより実行されはじめます。そのため、上記のコードを実行するとすぐに promise called と表示されます。

Promise が成功したときの処理は then、失敗したときの処理は catch でつなげます。50%の確率で成功、50%の確率で失敗する promise を作ってみましょう。

const promise = new Promise((resolve, reject) => {
    console.log("promise called");

    if (Math.random() * 10 < 5) {
        resolve('success');
    } else {
        reject('failure'); 
    }
});

promise
    .then(res => console.log(res))
    .catch(err => console.error(err));

Wandbox で実行する

RustのFuture

JavaScriptのPromiseに対応するオブジェクトはFutureです。

Promise内では、処理が完了したときに resolveもしくはrejectを呼び結果を通知することになっています。一方で、Futureはポーリングモデルを採用しており、tokio等の非同期ランタイムがFutureの結果を取得しにいきます。tokioはFutureのpollメソッドを定期的に呼び出します。pollはまだ実行中だったら Poll::Pending を、処理が完了したら Poll::Ready を返します。

Futureは futures クレート1のものを使います。現時点(2019/08/14)では stable 版の Rust で利用できる futures クレートの最新版は 0.1.28 です。

futures バージョン 0.1 における Future は次のようなシグネチャをもちます1

pub trait Future {
    type Item;
    type Error;
    fn poll(&mut self) -> Poll<Self::Item, Self::Error>;
}

では Promise で書いた処理を Rust で書いてみましょう。 poll メソッドを実装すれば OK です。 50% の確率で Future が正常終了し "success" が出力され、 50% の確率で Future がエラー "failure" を返します。


struct MyFuture;
impl Future for MyFuture {
    type Item = String;
    type Error = String;

    fn poll(&mut self) -> Poll<String, String> {
        println!("poll called");

        let mut rng = rand::thread_rng();
        let i: i32 = rng.gen_range(0, 10);
        if i < 5 {
            Ok(Async::Ready("success".to_string()))
        } else {
            Err("failure".to_string())
        }

    }
}

Rust の Future を実行するには、 Future を executor にわたす必要があります。 futures クレートにも executor があるので、とりあえずこちらを使ってみましょう。

Promise の then は and_then に、 catch は map_err に対応します。 executor に突っ込む future は Item = (), Error = () にする必要があるため、 Ok(())() を返して型を合わせます。


fn main() {
    let future = (MyFuture{}).and_then(|res| {
        println!("{}", res);
        Ok(())
    }).map_err(|err| {
        println!("{}", err); // () を返す
    });

    // futures クレートの executor
    futures::executor::spawn(future).wait_future().unwrap();
    println!("finished");
}

これでも動きますが、wait_future は Future の実行が完了するまでスレッドをブロックしてしまうので、通常は tokio などのランタイムを使います。

impl Future for MyFuture {
    ...
}

fn main() {
    let future = (MyFuture{}).and_then(|res| {
        println!("{}", res);
        Ok(())
    }).map_err(|err| {
        println!("{}", err); // () を返す
    });

    tokio::run(future);
    println!("finished");
}

Rust Playground で実行する

RustのFutureのFuture

Rust においても async/await が使えるようにするため、 Rust 1.37 で Future が std::future に収録されました。さらに、Rust 1.38 では async/await が stable になる予定です。近い将来には、 std::future の Future が広く使われるようになるでしょう。 tokio も master ブランチでは std::future をデフォルトで使うように開発が進められているようです。

futures 0.1 の Future と futures 0.3 の Future は相互変換できるため、環境を移行するのも難しくはないと思います(希望をこめて)。

futures 0.3 や std::future における Future は次のようなシグネチャを持ちます。

pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output>;
}

std::future では成功/失敗の概念を Future から切り離しました。そのため、成功/失敗を表現するためには、Result を返すようにする必要があります。では、さきほどの処理を std::future の Future で書き換えてみましょう。

struct MyFuture;
impl Future for MyFuture {
    type Output = Result<String, String>;

    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
        println!("poll called");

        let mut rng = rand::thread_rng();
        let i: i32 = rng.gen_range(0, 10);
        if i < 5 {
            Poll::Ready(Ok("success".to_string()))
        } else {
            Poll::Ready(Err("failure".to_string()))
        }
    }
}

この Future を実行するには、 Future を executor にわたす必要があります。 ひとまずは futures::executor::block_on を使ってみましょう。and_then はもとの Future と同じ型を返す必要がありますが、 map_ok ではもとの Future と違う型を返すことができます。

let future = (MyFuture{}).map_ok(|res| {
    println!("{}", res);
}).map_err(|err| {
    println!("{}", err);
});
let _ = block_on(future);

futures 0.3 では ThreadPool という executor もあります。ThreadPool は複数のスレッドを用いて Future を実行してくれるため、通常はこちらの executor を使ったほうがいいと思います。いずれは tokio 等のランタイムでも気軽に std::future も使えるようになるでしょう。

fn get_future() -> impl Future<Output=Result<(), ()>> {
    (MyFuture{}).map_ok(|res| {
        println!("{}", res);
    }).map_err(|err| {
        println!("{}", err);
    })
}

let future = get_future();
ThreadPool::new().expect("Failed to create threadpool").run(future);

Future の途中結果を print したいことは多々あると思います。このために、 inspect_okinspect_err が提供されています2。 print するだけだったら map ではなくて inspect を使ったほうがいいでしょう。

let future = (MyFuture{}).inspect_ok(|res| {
    println!("{}", res); 
}).inspect_err(|err| {
    println!("{}", err);
});

let _ = block_on(future);

ソースコードの全文は Github にあります。

ドキュメントについて

Rust に async/await を取り入れるためには、Future を言語のコア機能に取り入れる必要がありました。一方で、言語のコア機能に Future を取り入れてしまうと、 Future の変更が困難になってしまいます。そのため、 std::future::Future の機能は最小限になっています。
https://doc.rust-lang.org/std/future/index.html

Future の便利機能は今後も futures クレートで提供されるようです。std::future と互換性がある futures 0.3 のドキュメントを参照してください。
https://rust-lang-nursery.github.io/futures-api-docs/0.3.0-alpha.18/futures/index.html

then 等の便利メソッドは FutureExt トレイトで提供されます。これは use futures::FutureExt すると使えるようになります。

type Output = Result<String, String> のように Future の値を Result 型にした場合、 use TryFuture とすると TryFuture トレイト の機能が利用できるようになります。
また、 use futures::TryFutureExt すると、 TryFutureExt トレイト の機能が使えるようになります。map_okinspect_err はこちらで定義されています。

まとめ

この記事では JavaScript の Promise と対比させて、Rust の Future について解説しました。 Rust の Future はポーリングモデルを採用し、ランタイムが poll を呼び出すことで処理が進んでいきます。

Future には 0.1 系と 0.3 系があり、近い将来は 0.3 系が広く使われていくかと思いますが、これらは相互変換できます。現状は tokio 等のランタイムがデフォルトでサポートしており、ドキュメントの多い 0.1 系を使っていくのがいいでしょう。

ツッコミ、感想等コメントお待ちしています :sunglasses:

謝辞

この記事の一部は @__pandaman64__ 氏に助言をいただき執筆しました。この場を借りて謝意を表します。

30
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
19