必修言語Rustの他己紹介

あけましておめでとうございます。オプティムの齋藤です。 R&Dに所属している新卒2年目社員で、トマト && 眼底 && SREな仕事の日々を過ごしています。 好きな言語設計者はアンダース・ヘルスバーグです。

昨年末にRust 2018がリリースされ、今年はいよいよRustの本格的な普及が期待されます。 そこで、リリース当初のRust 1.0から使っている私が、 Rustをよく知らない方向けに良い点、悪い点の双方からRustの紹介をしようと思います。

なお、ここで紹介する内容は執筆時点での最新の言語仕様であるRust 2018を反映しています。

f:id:optim-tech:20181210104234p:plain

なぜRustなのか?

Rustを一言で表すと「CやC++を置き換えようとしている、めっちゃ安全で高速な、割と書きやすいシステムプログラミング言語」でしょう。 以下ではこのRust言語の魅力を紹介します。

皆が愛する言語

Stack Overflowのアンケートでは一番愛されている言語という結果が出ています。 他の名だたる言語を抑えて一位になっているわけですから、そのポテンシャルの高さをうかがい知ることが出来るでしょう。

ネイティブにコンパイルされる

RustはCやC++といった言語と同じくネイティブコンパイル型の言語です。 そのため、システムライブラリに依存していない限りバイナリ単体で起動しますし、 起動時のフットプリントも非常に小さく、高速に動作します。 また、Linuxにおいては muslを使ってlibcのない環境でも動かすことも出来るので、 ディストロを問わないバイナリ作成や、Alpine Linuxの利用によるDockerイメージのサイズ削減にも繋がります。

マルチプラットフォーム

RustはバックエンドにLLVMを使用しています。これは開発コストの軽減や高度な最適化だけでなく、 マルチプラットフォーム対応が容易であるという大きなメリットをもたらします。

例えば、RustはWindowsやLinux、Macといった一般的なプラットフォームだけでなくWebAssemblyを通してブラウザでも動くほか、 Raspberry PiやArduinoのような組み込み環境、あるいは最近話題のRISC-Vでも動作します。

また、Rustは標準ライブラリを使わないという選択も出来るため、OSを作ることまで出来てしまいます。

Cとの相互運用

RustはCとの相互運用ができ、多くの既存のライブラリを使うことが出来ます。 直接利用する場合はメモリレイアウトや呼び出し規約のような低レイヤーの知識が必要にはなりますが、 メジャーなライブラリはRust用ラッパーライブラリが公開されていることも多いので、問題になることは少ないでしょう。

高い安全性

Rustではいわゆる( ´∀`)<ぬるぽが発生しません。 すべての変数が常に値を持つのです。 いわゆるNULLのようなものを表現する際は「値が入っていない」という値を持つOption型を使い、 その中身にアクセスするときは必ず「その値がNULLかどうか」というチェックを伴います。 そのため、予期しないNULLへのアクセスが発生しません。

また、RustはGCがなく、手動でメモリを管理するタイプの言語ですが、 ほとんどのメモリリークやリソースリーク(解放忘れ)を「コンパイルの時点で」弾くことが出来ます。 その秘密は、Rustがコンパイル時に変数の「寿命」を管理していることにあります。 Rustには「借用」や「ライフタイム」と言った機能があり、これによって変数解放の適切なタイミングが コンパイル時に分かるようになっているため、各種リークが起きにくいという安全性を手に入れています。

ゼロコスト抽象化

そもそも「ゼロコスト抽象化」という言葉が抽象的でなんだかよく分かりませんが、 難しい言い方をすると「Rustは動的なメッセージのディスパッチを極力しない」ということです。

多くの言語では仮想関数テーブルによって抽象化を実現していますが、 Rustではこれを極力使わず、静的な関数呼び出しによってコストの無い(=ゼロコストの)関数呼び出しを実現しています。

けっこう速い

Rustはネイティブへのコンパイルや、ゼロコスト抽象化などのおかげで 実行速度は速く、その速度はCやC++に匹敵、場合によってはそれらより速くなることもあります。

https://benchmarksgame-team.pages.debian.net/benchmarksgame/faster/rust.html

そして、これらは基本的に純粋に直列に実行した場合の結果であるので、 SIMDrayonによる並列化でさらなる高速化も望めるでしょう。

柔軟な型推論

Rustの型推論は他の言語とは比べものにならないほど強力です。 なぜなら、型は「どこかで確定」さえすれば良いのです。

fn main() {
    // aはどんな数値型にもなり得る
    let a = 0;
    // bはaと同じ型
    let b = a;
    // cはi32として定義されているので、bに遡及してbがi32になり、aもi32になる
    let c: i32 = b;
}

引数やジェネリクス、戻り値などを通しても間接的に推論されるので、 関数内で型を明示するのはどうしても型が曖昧になってしまう場面のみでしょう。

安全なマクロ

CやC++を使ったことがある人に「マクロ」と言うとちょっと身構えてしまうかもしれません。 ですが、安心してください。 Rustのマクロはある程度Rustの構文に則る必要があるため、プリプロセッサのマクロほどイカした挙動はしません。

例えば、serde-jsonにJSONを定義するマクロがあります。 Rustにはキー部分に文字列を指定するオブジェクトの構文はありませんが、このマクロではそれが使えています。

use serde_json::json;

fn main() {
    let json = json!({
        "foo": "bar",
        "hoge": 0,
        "fuga": true,
        "piyo": [0.1, 0.2, 0.3],
    });
    println!("{}", json);
}

このように、Rustのマクロは「言語構文を拡張する」ようなものと言えます。

ジェネリクス

C++のテンプレートとは対照的に、Rustのジェネリクスは実引数に依存せず、関数宣言だけでその意味論を決定します。 例えばReadトレイトをジェネリクスを用いて引数に取る関数であれば その変数が他にどんな関数を持っていようと、readなどのReadトレイトに定義されている関数しか呼び出すことができません。 これはトレイトという「振る舞い」を定義する仕組みも相まって関数の挙動を分かりやすくします。

また、このジェネリクスによってあらゆる関数の呼び出しがトレイトを通じて静的に解決出来るようになるので、 ゼロコスト抽象化が実現出来ています。

デフォルトで代入がムーブ

Rustでは変数の代入がデフォルトでムーブです。 これはどういうことかというと、不必要な変数のコピーが発生せず、実行時間の短縮やメモリ使用量の削減に繋がると言うことです。

これはC++を使っている方は特にメリットを感じる所だと思います。 C++ではいちいちstd::move(...)と書いたり、時にはstd::forward(...)と書いたりと混乱していたところですが、 Rustでその心配はいりません。ただ代入するだけで良いのです。

ドキュメントが豊富である

英語ではありますが、言語に関するドキュメント標準ライブラリのドキュメントだけでなく。 エディションに関するドキュメントまであります。 Rustについて疑問がある時はこれらのドキュメントを読めば大抵解決します。

また、最新のものに追従している保証はありませんが、日本語ドキュメントも コミュニティによって整備されているので、英語が苦手な方はこちらを読むと良いでしょう。

これに加え、言語としてドキュメント化機能が組み込まれており、 パッケージを公開すると自動でdocs.rsにホスティングされるため、 非標準ライブラリのドキュメントまで豊富に揃う仕組みが出来上がっています。

パッケージマネージャーやビルドツールが標準で存在する

Rustにはcargoと言うパッケージマネージャ兼ビルドツールが存在し、 定義ファイルに依存ライブラリやそのビルド設定を記述するだけで パッケージのダウンロードからビルド、リンクまでをワンストップで行うことが出来ます。

また、cargoには拡張機能を導入することもでき、 例えばcargo tarpaulinでコードカバレッジを測定したり、 cargo-lichkingで依存ライブラリのライセンスを確認したりすることが 容易に可能です。

継続的に進化する

Rustにはstable/beta/nightlyの3つのリリースチャンネルがあり、新しい言語仕様やライブラリはnightlyでテストされ、 betaでバグを潰し、stableでプロダクション利用が可能になります。 stable/betaチャンネルは6週間おきに、nightlyは毎晩アップデートされます。 また、それだけでなく2〜3年おきに「エディション」が新しくなり、言語機能としては互換性の無い変更が入ります。

この「エディション」はRust 1.31から入った機能で、後方互換性の無い構文は「新しいエディション」として入ることになりました。 これは一見、多くの言語で行われる言語改訂と同じように思えます。 例えばC#では仕様改訂によって数々の破壊的変更がありました。

しかし、Rustの進化はこれらとは異なります。 Rustのエディションは古いコードを切り捨てることなく、古いコードを「古いエディション」として別個に処理することで、 後方互換性だけでなく前方互換性も確保するものです *1。 これによってRustは後方互換性を気にすることなく継続的に進化することが出来ます。

Firefoxに組み込まれている

もともとRustはFirefoxの開発者の1人が開発を始めたもので、FirefoxのためにRustが作られたとも言っても過言ではありません。 実際Firefoxにはバージョン48からRust製のコンポーネントが組み込まれており、安全かつ高速なブラウジングに一役買っています。

また、Mozillaのブラウザエンジン研究の一環として開発されているServoは全体がRustで作られており、 Servoから生まれたライブラリも数多くあります。

これは、某Dから始まりDで終わる言語のような、後ろ盾のない、あるいは弱い言語と違って、 将来的にもメンテナンスされることが保証されているとも言えるでしょう。 それもあってか、Mozilla以外にもFacebookやDropbox、Cloudflareなどといった有名企業もRustを使っています。

なぜRustではだめなのか?

Rustは安全性や書きやすさのために犠牲にしていることがいくつかあります。 以下ではそのようなRustの良くない所を紹介します。

Cっぽいけど違う

Rustは、PascalやRubyのようなbegin/end型ではなく、Lispのような丸括弧地獄でもなく、Cのような波括弧型の言語です。 しかしその印象とは裏腹にCと異なる部分が多くあります。これは少し取っ付き難さがあるかもしれません。

下記はほんの一例です。

// 関数はfn、変数の型は後置
fn add(a: i32, b: i32) -> i32 {
    // returnは省略できる
    a + b
}

fn max(a: i32, b: i32) -> i32 {
    // ifの条件に括弧はいらない
    if a > b {
        a
    } else {
        b
    }
}

一般的なOOPではない

Rustのオブジェクト指向は継承しません。 Javaで言う所のclassが無く、extendsが無く、interfaceが無く、implementsが無いのです。

RustのOOPは「データを保持する構造体型(struct)」と「振る舞いを定義したトレイト型(trait)」の2つから成り立ち、 それぞれのstructが0個以上のtraitと、struct専用の処理をそれぞれ実装(impl)する、という構成をします。

// データを保持する構造体
struct Object {
    pub value: i32,
}

// 1を足す、という「振る舞い」
trait Add1 {
    fn add1(&mut self);
}

// 振る舞いを実装する
impl Add1 for Object {
    fn add1(&mut self) {
        self.value += 1;
    }
}

fn main() {
    let mut obj = Object { value: 0 };
    obj.add1();
    println!("{}", obj.value); // 1

    // trait型の変数を宣言することは出来ない
    // let obj_add1: Add1 = obj;

    // 参照などを通せば使える
    {
        let obj_add1: &mut dyn Add1 = &mut obj;
        obj_add1.add1();
    }
    println!("{}", obj.value); // 2
}

オーバーロード出来ない

関数は引数によってオーバーロードすることは出来ません。 引数が違うと言うことは振る舞いが違うと言うことなのだから関数名を変えるべきだ、という考え方です *2

どうしても関数をオーバーロードしたいのだ、と言う方にはRustはおすすめ出来ません。

fn foo() {
}

// エラー
fn foo(a: i32) {
}

標準ライブラリが貧弱

確かにRustは、標準ライブラリではJSONをパース出来ませんし、HTTPのリクエストも出来ません。 Rustは 「バッテリー同梱」のPythonとは反対に、 標準ライブラリをシンプルに保つ「バッテリーを同梱」しない決断をしたためです。

標準ライブラリを用意するということはそのライブラリの互換性を保ちつつずっと保守し続けなければならず、 それはコミュニティにとって無用な負担です。 データフォーマットの標準がXMLからJSONに移り変わったように、使うライブラリも時代によって変えるのがRustの選択なのです。

エラー処理が面倒

Rustには例外機構がありません。ほとんどのエラーをResultOptionという型によって実現しています。 これらの型はいちいちその型の存在を気にする必要があり、 例外のような「放っておけばどこかで処理される」というコードを書けません。

例えば「ファイルから文字列を全部読み込む」という処理を考えただけでも下記のように2回エラー処理が必要になります。

use std::fs::File;
use std::io::prelude::*;

fn main() {
    let mut content = String::new();

    let mut file = File::open("path/to/file")
        // エラー処理
        .expect("failed to open the file");
    file.read_to_string(&mut content)
        // エラー処理
        .expect("failed to read from the file");

    println!("{}", content);
}

ただし、逆に言えば、どこで例外が起きているかが明確になり、エラーをその場で処理するのか、 呼び出し元に返すかがエラーが起こる部分のコードで明確化出来ると言うことでもあります。

また、例外が無いことで例外では高かったアンワインド(unwinding)のコストを削減しています *3

学習コストが高い

Rustはよく学習コストが高いと言われます。 コミュニティもそれを認識しており、 エラーメッセージを分かりやすくしたり、ドキュメントを整備したり、Rust 2018のNLLによって借用の条件を緩和したりなど、 常に改善の施策は打たれていますが、それでも言語の思想上、どうしても学習コストは高くなってしまいます。

しかし、その高い学習コストのおかげで安全なプログラムが書けるようになるわけですから、費用対効果は計り知れません。 また、前述しているようにドキュメントが豊富であるので、学習コストは高いものの学習はしやすいと思います。

まちがい

use std::thread;

fn main() {
    // 別スレッドで、referenceを通してvarに代入したい
    let mut var = 0;

    // varはreferenceほど長く生存出来ないのでエラー
    let reference = &mut var;
    thread::spawn(move || {
        *reference = 10;
    })
    .join().unwrap();

    // referenceはクロージャにムーブされて使えないのでエラー
    println!("{}", *reference);
}

せいかい

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // Mutex及び別スレッドと共有出来る参照カウンタを使う必要がある
    let var = Arc::new(Mutex::new(0));

    {
        let var = var.clone();
        thread::spawn(move || {
            // ロックして代入
            *var.lock().unwrap() = 10;
        })
        .join().unwrap();
    }

    // ロックして参照
    println!("{}", *var.lock().unwrap()); // 10
}

雑に書けない

Rustはエラーを気にしなければならなかったり変数の借用に厳密だったりと、雑に書くとコンパイラに怒られる点が多くあります。 しかし、「雑に書くこと」は「安全性を壊すこと」ことであり、 Rustの目的とは反するものなので雑に書きたいときは他の言語を使いましょう。

オプティムでの使いどころ

オプティムでもいくつか採用例があります。

  • AIの結果を使って物体の動きを追跡するプログラム
    • Python+CuPy(GPU)からRust(CPUのみ)に移植して20〜40倍の速度を達成
  • Python 2/3で実装されたAIを使ってWebカメラなどの動画を解析し、その結果をWebブラウザに送信するHTTPサーバー
    • Pythonプログラムはrust-cpythonによってRustプログラム内で直接実行されるため、 オーバーヘッドが少ない
  • TensorRTを始めとするAIエンジンを使って動画ストリームを解析するミドルウェア
    • crossbeam-channelによって スレッド間の高効率な通信を安全かつ便利に実現している

Rustのおかげで高い安全性と高速性を保ちつつ、相互運用性と書きやすい構文によって素早い開発を実現出来ています。

さいごに

Rustはいいぞ。というわけで皆さんRust使いましょう。

ちなみに、オプティムのR&Dメンバーのうち少なくとも40%以上の人が日常的にRustを使っています。 最近は前述のように社内プロジェクトにRustを採用することも増えてきました。 Rustを使う(使える)企業はまだまだ少ないと思うので、 業務でのRustの利用に興味がある!と言う方は是非一度オプティムを訪れてみてください。

const TEXT: &'static str = "\
    20 5f 5f 20 20 20 20 20 20 20 20 20 20 5f 5f 20 20 5f 20 20 20 20 20 20 20 \
    20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 5f 20 20 20 \
    20 20 20 20 20 20 20 20 5f 5f 5f 5f 20 20 5f 5f 5f 5f 5f 20 5f 5f 5f 5f 5f \
    5f 5f 20 5f 20 5f 5f 20 20 5f 5f 20 5f 20 0a 20 5c 20 5c 20 20 20 20 20 20 \
    20 20 2f 20 2f 20 7c 20 7c 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 \
    20 20 20 20 20 20 20 20 20 20 7c 20 7c 20 20 20 20 20 20 20 20 20 2f 20 5f \
    5f 20 5c 7c 20 20 5f 5f 20 5c 5f 5f 20 20 20 5f 5f 28 5f 29 20 20 5c 2f 20 \
    20 7c 20 7c 0a 20 20 5c 20 5c 20 20 2f 5c 20 20 2f 20 2f 5f 5f 7c 20 7c 20 \
    5f 5f 5f 20 5f 5f 5f 20 20 5f 20 5f 5f 20 5f 5f 5f 20 20 20 5f 5f 5f 20 20 \
    7c 20 7c 5f 20 5f 5f 5f 20 20 20 7c 20 7c 20 20 7c 20 7c 20 7c 5f 5f 29 20 \
    7c 20 7c 20 7c 20 20 20 5f 7c 20 5c 20 20 2f 20 7c 20 7c 0a 20 20 20 5c 20 \
    5c 2f 20 20 5c 2f 20 2f 20 5f 20 5c 20 7c 2f 20 5f 5f 2f 20 5f 20 5c 7c 20 \
    27 5f 20 60 20 5f 20 5c 20 2f 20 5f 20 5c 20 7c 20 5f 5f 2f 20 5f 20 5c 20 \
    20 7c 20 7c 20 20 7c 20 7c 20 20 5f 5f 5f 2f 20 20 7c 20 7c 20 20 7c 20 7c \
    20 7c 5c 2f 7c 20 7c 20 7c 0a 20 20 20 20 5c 20 20 2f 5c 20 20 2f 20 20 5f \
    5f 2f 20 7c 20 28 5f 7c 20 28 5f 29 20 7c 20 7c 20 7c 20 7c 20 7c 20 7c 20 \
    20 5f 5f 2f 20 7c 20 7c 7c 20 28 5f 29 20 7c 20 7c 20 7c 5f 5f 7c 20 7c 20 \
    7c 20 20 20 20 20 20 7c 20 7c 20 20 7c 20 7c 20 7c 20 20 7c 20 7c 5f 7c 0a \
    20 20 20 20 20 5c 2f 20 20 5c 2f 20 5c 5f 5f 5f 7c 5f 7c 5c 5f 5f 5f 5c 5f \
    5f 5f 2f 7c 5f 7c 20 7c 5f 7c 20 7c 5f 7c 5c 5f 5f 5f 7c 20 20 5c 5f 5f 5c \
    5f 5f 5f 2f 20 20 20 5c 5f 5f 5f 5f 2f 7c 5f 7c 20 20 20 20 20 20 7c 5f 7c \
    20 20 7c 5f 7c 5f 7c 20 20 7c 5f 28 5f 29 0a 20 20 20 20 20 20 20 20 20 20 \
    20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 \
    20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 \
    20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 \
    20 20 20 20 0a 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 \
    20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 \
    20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 \
    20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 0a 75 67 67 63 66 \
    3a 2f 2f 6a 6a 6a 2e 62 63 67 76 7a 2e 70 62 2e 77 63 2f 65 72 70 65 68 76 \
    67 2f 76 61 67 72 65 69 76 72 6a 2f 66 67 6e 73 73 30 33 0a\
";

fn main() {
    TEXT.split(" ")
        .map(|x| u8::from_str_radix(x, 16).unwrap())
        .map(|n| match n {
            b'A'..=b'M' | b'a'..=b'm' => n + 13,
            b'N'..=b'Z' | b'n'..=b'z' => n - 13,
            _ => n,
        })
        .map(char::from)
        .for_each(|c| print!("{}", c));
}

ライセンス表記

  • 冒頭の画像中にはRust公式サイトで配布されているロゴを使用しており、 このロゴはMozillaによってCC-BYの下で配布されています

*1:異なるエディションで書かれたライブラリを使うことが出来ると言うことです!

*2:実は出来なくは無いですが、コードの意味としておかしなことになるので全くおすすめ出来ません

*3:ただしアンワインドが無いわけでは無く、パニックが起きた際にはリソース解放を実行するためアンワインドが実行されます