(この記事は、「fukuoka.ex(その2) Elixir Advent Calendar 2017」の11日目、Webパフォーマンス Advent Calendar 2017 2017の4日目です)
昨日は@tuchiroさんのElixirでSI開発入門 #7 Multiで使う関数を再利用可能な粒度に分割するでした!
本連載の記事はこちらです。
|> ElixirのGenStageに入門する #1
|> ElixirのGenStageに入門する#2 バックプレッシャーを理解する
|> Elixir並列処理「Flow」の2段ステージ構造を理解する
|> Elixirから簡単にRustを呼び出せるRustler #1 準備編
|> Elixirから簡単にRustを呼び出せるRustler #2 クレートを使ってみる
|> Elixirから簡単にRustを呼び出せるRustler #3 いろいろな型を呼び出す
お礼:各種ランキングに69回のランクインを達成しました
4/27から、30日間に渡り、毎日お届けしている「季節外れのfukuoka.ex Elixir Advent Calendar」と「季節外れのfukuoka.ex(その2) Elixir Advent Calender」ですが、Qiitaトップページトレンドランキングに8回入賞、Elixirウィークリーランキングでは5週連続で1/2/3フィニッシュを飾り、各種ランキング通算で、トータル69回ものランクイン(前週比+60.1%)を達成しています
みなさまの暖かい応援に励まされ、合計452件ものQiita「いいね」(前週差+103件)もいただき、fukuoka.exアドバイザーズとfukuoka.exキャストの一同、ますます季節外れのfukuoka.ex Advent Calendar、頑張っていきます
はじめに
今回はElixirのShift-JIS(SJIS)事情から始めます。
企業の受発注の現場ではWindowsの主要Encodingである、SJISが未だに幅を聞かせています。Elixirの文字列型はUTF8エンコードであり、SJISに対応した変換ライブラリも見当たらず、SI案件では導入の障壁の一つでした。
ここではRustlerの連載の一環としてBinaryを渡して、SJIS->UTF8変換を行う手法を提示致します。
Elixirをサービスのプラットフォームとして選択しない場合でも、Flowというライブラリを使用して、並列でShift-JIS => UTF8変換が可能になるので、バッチ処理内のツールとしてWebパフォーマンスの改善が図れます。
Rustに興味がない方は、途中飛ばして最後の結果だけでも読んで頂ければありがたいです。
さて、前回はコレクション型をElixirから取り出してきました。今回は最大の難関であるBinaryがテーマです。
今回使うRustのクレート
encodingを使います。
Rust用語
以下Rust特有の用語が出てくるので、抜粋しておきます。
- トレイト(trait) オブジェクト指向のクラス的なものですが、メンバ変数がなく関数を集めたものです。
- Option型 値がないかもしれないことを表すラップ。
- スライス 部分配列のこと。必ず本体がある。
##Elixirのバイナリの扱い
実はElixirにはStringとBinaryは区別がありません。どちらもバイナリです。
文字列とバイナリの区別が必用になるのは、Rust側です。
iex(1)> <<"あ",0>> # 最後尾に0を足して、バイトコードを確認
<<227, 129, 130, 0>>
iex(2)> <<227,129,130>>
"あ"
Rustのバイナリの扱い
String型
は一般的な文字列型ですがElixirと異なるのは、UTF-8にないコードを代入すると例外が発生(panic)するところです。
Elixirから渡したSJISとして渡した文字列は、RustのString型やそのスライスのstr型で受けることはできません。
let str : String = args[0].decode()?; // NG
コード解説
どう解決しているかは、コードを見ていきましょう。
fn sjis2utf8<'a>(env: NifEnv<'a>, args: &[NifTerm<'a>]) -> NifResult<NifTerm<'a>> {
let sjis_encoding = encoding_from_whatwg_label("sjis").unwrap();
let in_binary : NifBinary = args[0].decode()?;
let in_str = in_binary.to_owned().unwrap();
let utf8_str = sjis_encoding.decode(in_str.as_slice(), DecoderTrap::Ignore).unwrap();
Ok(utf8_str.encode(env))
}
前回までに紹介してきた他のプリミティブ型(数値型、文字列等)が、直接型指定をして値を取り出してきたのとは違って、一旦NifBinary型
にデコードします。この型はまだバイトコードではありません。次に、Rustlerのトレイト関数to_owned
でOption型に変換します。さらに、Option型から値を取り出すために、unwrap()
関数を実行します。これだけの流れを経て、ようやくバイトコードが取り出せました。
この辺を理解するのはRustの型定義だけでなく、NIFで受け渡しされているデータ構造まで知る必用があります。
encodingクレートでShift-JIS
をUTF-8へデコードする方法は、encodingクレート公式サイト内の"EUC-KR"をDecodeするテストコードを参考にしました。
コード中、sjis_encoding.decode(in_str.as_slice(),・・・
の部分では第一引数に&[u8]
型が求められているので、先ほど取り出したバイトコードのスライスを渡しています。また、戻り値はResult型なのでunwrap()で値を取り出しています。バイナリをStringやstr型ではなく、&[u8]
で渡しているところがポイントです。
郵便番号データ
さて、今回のNIFを使ってShift-JISデータの代表格である、日本郵便の郵便番号データをコンバートしてみましょう。
ここでは全国一括データを使います。
- ken_all.csv
(ソース全体はGithubにアップしました。)
2018/9/29 パッケージ名mbcs_rsでhex公開しました !
(NifExample.sjis2utf8
はMbcsRs.decode!
で実現されております。)
プロジェクトディレクトリにダウンロード、解凍してElixirを起動します。テキストエディタで確認したところ、124184行あります。
以下のコードは、CSVの文字列フィルターのサンプルです。
defmodule PostalCsv do
def filter(word \\ "福岡県") do
"ken_all.csv"
|> File.stream!
|> Stream.map(fn r -> NifExample.sjis2utf8(r) end)
|> Stream.filter(fn r -> String.contains?(r, word) end)
|> Enum.to_list
end
end
iexを起動して、動作を確認します。
$ iex -S mix
iex(1)> PostalCsv.filter("福岡市中央区")
・・・
"40133,\"810 \",\"8100073\",\"フクオカケン\",\"フクオカシチユウオウク\",\"マイヅル\",\"福岡県\",\"福岡市中央区\",\"舞鶴\",0,0,1,0,0,0\n",
・・・
"40133,\"810 \",\"8100037\",\"フクオカケン\",\"フクオカシチユウオウク\",\"ミナミコウエン\",\"福岡県\",\"福岡市中央区\",\"南公園\",0,0,0,0,0,0\n",
"40133,\"810 \",\"8100022\",\"フクオカケン\",\"フクオカシチユウオウク\",\"ヤクイン\",\"福岡県\",\"福 岡市中央区\",\"薬院\",0,0,1,0,0,0\n",
...]
iex(2)> PostalCsv.filter("福岡市中央区") |> Enum.count
53
iex(3)> PostalCsv.filter("0") |> Enum.count
124184
無事SJISのファイルがElixirで読めましたね。
Flowでも動作することは確認できました。このファイルの大きさでは、Flowの並列処理の力が発揮できないので、動作時間はStream版と同程度でした。(Flow版のソースもアップしておりますPostalCsv.filter_flow
)
RustのクレートEncodingはSJISだけでなく、多様なエンコーディングに対応してます。
これでElixirで文字エンコードに悩むことはなくなったので、SI案件には朗報だと思います。
終わりに
バイトコードのスライスを使いだすと、Rust本来の難しさが出てきますね。
今回のRustのコードは書いてみると数行なのですが、動作するようになるまでに4時間を要しました。
encodingクレートの使い方でもencoding_from_whatwg_label
関数を使用するということがわからずに、かなり悩みました。Rustは簡単に書けそうなラスト1マイルがすごく遠い印象です。その過程は楽しいのですが、万人向けではないですね^^。
それでも、一度コードを書いてしまえば鉄壁な安全性を享受できるというのは、NIFのような低レベルでは大きなアドバンテージとなります。
さて、今回でRustlerの連載は最後のつもりでしたが、OTP20の最新機能が盛り込まれていることがわかりましたのでもう少し深堀します。次回は「Elixirから簡単にRustを呼び出せるRustler #5 NIFからメッセージを返す」です。
明日は@zacky1972さんの「ZEAM開発ログv0.1.6 Elixir から Rustler で GPU を駆動しよう〜ElixirでAI/MLを高速化」になります!
PS. 次回予定を「コールバック」から「メッセージ送信」に変更しました。
満員御礼!Elixir MeetUpを6月末に開催します
※スミマセン、増枠分も埋まってしまいました...が、ほんの多少なら、もうちょい入れるかも
「fukuoka.ex#11:DB/データサイエンスにコネクトするElixir」を6/22(金)19時に開催します。
私もETSとFlowを題材に発表いたしますので、ぜひ興味ある方はご参加ください!