(この記事は、「fukuoka.ex(その2) Elixir Advent Calendar 2017」の5日目、および「Webパフォーマンス Advent Calendar 2017」の3日目です)
昨日は @tuchiro さんの「ElixirでSI開発入門 #6 Ectoで自由にSQLを書いて実行する(更新編)」でした。
本連載の記事はこちらです。
|> ElixirのGenStageに入門する #1
|> ElixirのGenStageに入門する#2 バックプレッシャーを理解する
|> Elixir並列処理「Flow」の2段ステージ構造を理解する
|> Elixirから簡単にRustを呼び出せるRustler #1 準備編
|> Elixirから簡単にRustを呼び出せるRustler #2 クレートを使ってみる
おしらせ
お礼:各種ランキングに43回のランクインを達成しました
4/27~5/19までの23日間、毎日お届けした「季節外れのfukuoka.ex Elixir Advent Calendar」 は、Qiitaトップページトレンドランキングに4回、「はてなブックマーク」のホットエントリー「テクノロジー」カテゴリに2回もランクインし、他ランキングも含めると、トータル43回ものランクインを果たしました
Qiita「いいね」数は合計349件もいただき、fukuoka.exアドバイザーズとfukuoka.exキャストの一同、みなさまの暖かい応援に励まされていますので、引き続き、「季節外れのfukuoka.ex(その2) Elixir Advent Calendar」でも応援お願いします
Rustler
簡単に並列処理が書けるElixir言語から、C++並の速度を誇るRust言語の機能をカジュアルに使えるRustlerを使って、巨大データ分析の基盤へアプローチをかけてます。Rustを呼び出すことにより、Elixirの速度改善を狙います。
RustlerはElixirからNIF経由で安全にRustのモジュールを呼び出すためのライブラリとmix拡張を含めたボイラープレートを作成するパッケージです。
前回までは数字と文字列の受け渡しを行ないました。今回はコレクション型をご紹介いたします。
こちらのサイトを参考にしました(ありがとうございます!)。Rustlerのバージョンが古かったので、今のバージョン(0.16)に合うように書き換えてご紹介しています。
このシリーズは、できるだけRustの型を使わずに、その速度と安全性のみを享受しようという虫のよい企画です。上手く行ってるかどうか最後までご覧になって下さい。
今回使った環境
- Apline Linux on Docker (公式 elixir:1.6.5-alpine 改)
- Elixir 1.65 OTP20
- Rust 1.22
- Rustler 0.16
関数のマッピングに関しては、アリティ(引数の数)のみが問われるということでElixir側の実装は前回までの連載の内容で事足りるので、今回はRust側の実装のみを解説致します。
Tuple型
これまでのプリミティブ型では値の取り出しにtry!を使用してきましたが、タプルの場合はdecode()?を使用します。あとは、関数タプルの各要素型を指定するだけです。ここは事前に型がわかっている必用があります。
Rustでのタプル内へのアクセスは、下記を見て頂ければ一目瞭然ですねtuple.0, tuple.1 ...
。
def test_tuple() do
# 4要素タプルの呼び出し
tuple = {:im_an_atom, 1.0, 1, "string"}
print_tuple(tuple)
end
fn print_tuple<'a>(env: NifEnv<'a>, args: &[NifTerm<'a>]) -> NifResult<NifTerm<'a>> {
// タプルの4要素の型定義を行う
let tuple = (args[0]).decode::<(NifAtom, f64, i64, String)>()?;
println!("Atom: {:?}, Float: {}, Integer: {}, String: {}", tuple.0, tuple.1, tuple.2, tuple.3);
Ok((atoms::ok(),tuple.3).encode(env)) // {:ok, タプルの4つ目 }を返す
}
iex(1)> NifExample.test_tuple
Atom: im_an_atom, Float: 1, Integer: 1, String: string
{:ok, "string"}
List型
use rustler::types::list::NifListIterator;
use rustler::NifError;
整数型のリストを加算して結果を返す例と、引数なしで[1, 2, 3]
のリストを返す例となります。イテレーターが登場して、俄然複雑になってきました。
fn sum_list<'a>(env: NifEnv<'a>, args: &[NifTerm<'a>]) -> NifResult<NifTerm<'a>> {
let iter: NifListIterator = try!(args[0].decode());
let res: Result<Vec<i64>, NifError> = iter
.map(|x| x.decode::<i64>())
.collect();
match res {
Ok(result) => Ok(result.iter().fold(0, |acc, &x| acc + x).encode(env)),
Err(err) => Err(err),
}
}
リストを返す場合は、Rustのリスト型であるvec!
型をそのままエンコードします。
fn make_list<'a>(env: NifEnv<'a>, _args: &[NifTerm<'a>]) -> NifResult<NifTerm<'a>> {
let list = vec![1, 2, 3];
Ok(list.encode(env))
}
iex(1)> NifExample.sum_list [1,2,3]
6
iex(2)> NifExample.make_list
[1, 2, 3]
Map型
Map用イテレーターであるNifMapIterator
を使用して、要素にアクセスします。
elixirのMapはjavascriptのJSONと同等にKeyもValueも型が自由なので、Rustで値を取り出すのは、それなりの覚悟が必用になってきます。
型推論が効く例
まずはprintln!
のケースを見てみましょう。
ここではprintln!
はRust柔軟な型推論が効いてるで、最小のコードで記述可能です。デバッグ用にはこれで良いですが、実際は値を取り出さないわけにはいかないでしょう。
def test_map_run() do
map = %{"firstEntry" => 1,
"secondEntry" => :second,
:third => 3.0,
4 => "fourthEntry",
"fifthEntry" => "five"}
print_map(map)
end
use rustler::types::map::NifMapIterator;
fn print_map<'a>(env: NifEnv<'a>, args: &[NifTerm<'a>]) -> NifResult<NifTerm<'a>> {
let mut map = NifMapIterator::new(args[0]).expect("Should be a map in the argument");
for x in map {
println!("{:?}", x);
}
Ok((atoms::ok().encode(env)))
}
iex(1)> NifExample.test_map
(4, <<"fourthEntry">>)
(third, 3.000000e+00)
(<<"fifthEntry">>, <<"five">>)
(<<"firstEntry">>, 1)
(<<"secondEntry">>, second)
:ok
iex(2)>
Mapをソートする例
以下は MapのKeyをソートして、{K,V}のタプルのリストを返すサンプルです。Rustlerのテストコードから改変しました。
余談ですが、リンク先のテストコードとは型が微妙に違う部分があります。本稿の型にしないとコンパイルは通りませんのでお気を付けください。
まずはlet mut vec = vec![]
で返却用のリスト用意しています。
前半のforではMapのキーをString型
としてデコードしながら、Valueとタプル化して返却用リストに放り込む処理です。ValueはNifTerm型でラップされたままです。
vec.sort_by_keyでミュータブルなソートを行います。
後半のmap処理では、Rustのタプルから、KeyをNifTerm型にラップしてmake_tuple関数でElixirのタプル型にしています。
ここでは、Rustlerのライブラリに用意されたmake_tuple
というhelper関数を使って返却用のKVペアのタプルを作っています。
use rustler::types::tuple::make_tuple;
fn test_map<'a>(env: NifEnv<'a>, args: &[NifTerm<'a>]) -> NifResult<NifTerm<'a>> {
let iter: NifMapIterator = args[0].decode()?;
let mut vec = vec![];
for (key, value) in iter {
let key_string = key.decode::<String>()?;
vec.push((key_string, value));
}
vec.sort_by_key(|pair| pair.0.clone());
let pairs: Vec<NifTerm> =
vec.into_iter()
.map(|(key, value)| make_tuple(env, &[key.encode(env), value]))
.collect();
Ok(pairs.encode(env))
}
実行結果です。Keyは文字列でないとエラーが出ますが、Valueはどんな型でも入ります。Rust側でNifTerm型のラップを外さない限りは、どんな型でもOKということです。
iex(1)> NifExample.test_map %{"c" =>1, "a"=>%{a: 1, b: 2}, "d" => "a", "g" => [a:
1, b: 2], "b" => :hello}
[{"a", %{a: 1, b: 2}}, {"b", :hello}, {"c", 1}, {"d", "a"}, {"g", [a: 1, b: 2]}]
# 数値のKeyを混ぜた
iex(2)> NifExample.test_map %{"c" =>1, 123 => 2}
** (ArgumentError) argument error
(phx_rust) NifExample.test_map(%{123 => 2, "c" => 1})
ラップされたものから、値を取り出したり関数を適用する・・といえばファンクターやモナドが垣間見えますが、今回はそちらの方向は控えさせて頂きます
1msルール ⇒ OTP20よりDirty CPU Scheduler採用
NIFは呼び出してから1ms以内に返答を返さないといけないというルールがあります、パースコストの高いMapを受け渡すのが得策なのかは設計上十分考慮すべき点となります。
**(追記)**OTP20よりDirty CPU Schedulerが採用され、Rustlerでも、enif_schedule_nif関数がunsafeながら公開されています。この件は情報収集中です。
まとめ
- NifTerm型にラップされている限り、Elixirどんな型も扱える。
- MapとListはRustlerのライブラリにある専用のイテレーターが必用
- コレクションの値を取り出す時は、Rustの知識が必用
終わりに
さて、Rustの型を相手にせずにカジュアルに使うというアイデアはうまく行ったでしょうか?タプルを除いたコレクション型だと、さすがに難しなりましたね。
ここまでRustlerでの様々な型を実装を見てきました。後はBinaryが残っています。次回は「Rustler#4 ElixirでShift-JIS変換を行う」で解説致します。お楽しみに!
明日は @zacky1972 さんの「ZEAM開発ログv0.1.5 Elixir から Rustler でネイティブコードベンチマークを呼び出してみよう〜ElixirでAI/MLを高速化」です。
満員御礼!Elixir MeetUpを6月末に開催します
※応募多数により、増枠しました
「fukuoka.ex#11:DB/データサイエンスにコネクトするElixir」を6/22(金)19時に開催します。私もETSとFlowを交えた発表で参加します! ますます福岡で盛り上がりつつElixir。ご興味がある方はご参加下さい!