LoginSignup
217
199

More than 5 years have passed since last update.

[翻訳] Python プログラマーのための Rust 入門

Last updated at Posted at 2015-06-01

本稿は 2015年5月27日 (水) に Armin Ronacher (@mitsuhiko) 氏によって書かれた記事の翻訳です。

訳者は Rust を全く知らないため、勘違いや誤訳もあると思います (特に用語) 。そういった誤りを見つけたら編集リクエストを送ってもらえると助かります。

Python プログラマーのための Rust 入門

いま Rust 1.0 が登場して非常に安定しているので、Python プログラマー向けに Rust の入門記事を書くとおもしろいのではないかと私は思いました。この手引きでは、Rust 言語の基礎を調べながら異なる構成概念とそれらがどう振る舞うのかを比較します。

Rust という言語は Python と比べると全く異なる獣です。単に一方がコンパイル型言語でもう一方がインタープリター型言語であるというだけでなく、その主要な言語機能においても全く違っていたりします。そのように言語のコア部分は全く違うけれども、これらの言語は API がどう作用するかの考え方に関して多くの共通点をもちます。Python プログラマーとして概念の多くはとても馴染みやすいのではないでしょうか。

構文

Python プログラマーが最初に気付く違いは構文でしょう。Python とは異なり、Rust は中括弧の多い言語です。しかし、これには正当な理由があります。それは Python がうまくサポートできない無名関数、クロージャ、多くのチェーン機能を Rust がもっているからです。非インデントベースの言語において、これらの機能はコードを書いたり理解したりするのをずっと簡単にしてくれます。それでは、両方の言語で同じ例をみてみましょう。

まず "Hello World" を3回表示する Python の例です。

    def main():
        for count in range(3):
            print "{}. Hello World!".format(count)

Rust での同じ例です。

    fn main() {
        for count in 0..3 {
            println!("{}. Hello World!", count);
        }
    }

ご覧の通り、よく似ています。deffn に、コロンが中括弧になります。その他の大きな構文の違いは、Rust は関数のパラメーターに型情報を必要とします。それを Python で行うことはありません。Python 3 なら型アノテーションが利用できるので Rust にみられるような同じ構文を使えます。

Python と比較しての新たな概念の1つは、末尾に感嘆符の付いた関数です。それらはマクロです。マクロはコンパイル時に展開されます。例えば、このマクロは文字列フォーマットとその出力に使われます。このマクロはコンパイル時にコンパイラへ適切なフォーマット文字列を強制できる方法だからです。そうすることで出力関数に渡す引数の数や型を間違えてしまうといったことが起きません。

トレイト対プロトコル

オブジェクトの振る舞いは最も身近でありながら異なる特徴があると言えます。Python では、特殊メソッドを実装することでクラスは特定の振る舞いを選択できます。これは通常 "プロトコルに従う" と呼ばれます。例えば、iterable なオブジェクトを作るためにイテレーターを返す __iter__ メソッドを実装します。これらのメソッドはそのクラス自身で実装されなければなりません。そして後から 実際には 変更できません (monkeypatch は無視します) 。

Rust の概念はとても似ていますが、特殊メソッドの代わりにトレイトを使います。トレイトは同じ目的の実現方法が少し違っていますが、その実装はローカルスコープに置かれ、別のモジュールから型のためのトレイトをさらに実装できます。例えば、整数に特殊な振る舞いをもたせたいのなら、整数型については何も変えずにそうできます。

この概念を比較するために、その型自身へ加算される型の実装方法についてみていきましょう。まず Python からです。

    class MyType(object):

        def __init__(self, value):
            self.value = value

        def __add__(self, other):
            if not isinstance(other, MyType):
                return NotImplemented
            return self.__class__(self.value + other.value)    

Rust での同じ例です。

    use std::ops::Add;

    struct MyType {
        value: i32,
    }

    impl MyType {
        fn new(value: i32) -> MyType {
            MyType { value: value }
        }
    }

    impl Add for MyType {
        type Output = MyType;

        fn add(self, other: MyType) -> MyType {
            MyType { value: self.value + other.value }
        }
    }

ここでの Rust のコードは少し長くなりますが、Python のコードにはない自動的な型の扱いも一緒に行われています。最初に気付くことは、Python ではメソッドがクラスに属しているのに対して、Rust ではデータとその操作が独立しています。struct はデータレイアウトを定義します。impl MyType は型自身がもつメソッドを定義するのに対して、impl Add for My Type はその型向けの Add トレイトを実装します。Add の実装向けに、ここで実装した add 操作の結果の型を定義する必要もありますが、Python で行わなければならないような、実行時の型チェックといった余分な複雑さがなくなります。

もう1つの違いは、コンストラクタが Rust では明示的であるのに対して、Python ではかなり魔術的なものです。Python では、オブジェクトのインスタンスを作成するとき、最後にオブジェクトを初期化するために __init__ を呼び出します。それに対して Rust では、オブジェクトを組み立てて割り当てる (慣例により new という) 静的メソッドを定義するだけです。

エラー処理

Python と Rust のエラー処理は全く異なります。Python のエラーは例外として投げられるのに対して、Rust のエラーは戻り値で返されます。初めは奇妙にみえるかもしれませんが、これは実によくできた概念です。ある関数を見たときにどんなエラーを返すのかが一目瞭然です。

これは Rust における関数が Result を返せるということです。Result は成功と失敗という2つの側面をもつパラメーター化された型です。例えば、Result<i32, MyError> は、この関数が成功した場合は 32bit 整数を、エラーが発生した場合は MyError を返すということを意味します。1つ以上のエラーを返す必要がある場合はどうなるのでしょうか?ここが哲学的な視点の異なるところです。

Python では、関数は任意のエラーを伴って失敗する可能性があり、それに関してはできることが何もありません。もしも Python の "requests" ライブラリを使って全リクエスト例外を捕捉したことがあるなら、そうやっても SSL エラーが捕捉されないことに苛々した後でその問題の本質を理解することになるでしょう。あるライブラリが何を返すかについて文書化されていない場合、そのライブラリのユーザーができることはほとんどありません。

Rust においてそういった状況は大きく異なっています。関数シグネチャは返すエラーを含みます。もし2つのエラーを返す必要がある場合、カスタムエラー型を作成して内部エラーをもっと良いものに変換する方法があります。例えば、HTTP ライブラリがあると仮定して、内部的にそのライブラリは Unicode エラー、IO エラー、SSL エラーといったようなエラーで失敗するかもしれない場合、ライブラリ特有のエラー型1つに変換する必要があり、そのライブラリのユーザーはそのエラーのみを扱う必要があります。必要に応じて、Rust はそのようにエラーが作成されたところに戻ってオリジナルのエラーを指す示すといったエラーチェーンを提供します。

またどこでも Box<Error> 型を使えます。独自のカスタムエラー型を作るのが面倒だったら、その型が任意のエラーに変換します。

Python では暗黙的にエラーが伝搬していくところが、Rust では明示的にエラーが伝搬します。これが意味するところは、その関数でエラー制御を行わないとしてもその関数がエラーを返すのがすぐに分かるということです。これは try! マクロにより実現されます。次の例を紹介します。

    use std::fs::File;

    fn read_file(path: &Path) -> Result<String, io::Error> {
        let mut f = try!(File::open(path));
        let mut rv = String::new();
        try!(f.read_to_string(&mut rv));
        Ok(rv)
    }

File::openread_to_string の両方とも IO エラーで失敗する可能性があります。try! マクロは、エラー時にはそのエラーを上位へ伝搬するために関数からすぐに返り、成功時にはアンパックします。関数の結果を返すとき、成功を示す Ok または失敗を示す Err のどちらかでラップされている必要があります。

try! マクロはエラーを変換可能にする From トレイトを呼び出します。例えば、io::Error から MyError へ戻り値を変更して、From トレイトを実装することにより io::Error から MyError への変換を実装します。そして、その変換がそこで自動的に呼び出されます。

別の方法としては、io::Error から Box<Error> への戻り値を変更して任意のエラーを返せます。しかし、これは実行時エラーについて考えているだけでコンパイル時のエラーではありません。

エラーを制御せずにその実行を中断させたいなら結果を unwrap() するのもできます。そうすることで、成功したときは値を取得し、結果がエラーだったときにプログラムが中断します。

可変性 (Mutability) と所有権 (Ownership)

Rust と Python で全く違う言語の部分は可変性と所有権の概念です。Python はガベージコレクションをもつ言語です。その結果として、ほとんど全てのことが実行時にオブジェクトとともに発生します。自由にそういったオブジェクトを受け渡せますし、それは "普通に動く" でしょう。明示的にメモリリークを起こすこともできますが、多くの問題は実行時に自動で解決されます。

Rust にガベージコレクタはありませんが、それでもメモリ管理は自動的に行われます。これは所有権の追跡として知られている概念により可能になります。作成する全てのものは別のものによって所有されます。Python でこのことを比較するとしたら、Python の全オブジェクトがインタープリターによって所有されるのを想像してみてください。Rust の所有権はもっとずっとローカルなものです。関数呼び出しがあるオブジェクトのリストをもつとします。その場合、そのリストがそのオブジェクトを所有し、その関数のスコープがそのリストを所有します。

もっと複雑な所有権のシナリオは、所有権の生存期間アノテーションと関数シグネチャで表現できます。例えば、前述した Add 実装の例では、そのレシーバーを Python と同じように self という名前にしました。しかし、Python とは違い、その値は関数内に "移動" されたのに対して、Python ではそのメソッドが可変参照とともに呼び出されます。これが意味することは、Python では次のようなことができるということです。

    leaks = []

    class MyType(object):
        def __add__(self, other):
            leaks.append(self)
            return self

    a = MyType() + MyType()

MyType インスタンスと他のオブジェクトを加算するとき、グローバルなリストに self をリークします。上述したコードを実行すると、MyType の最初のインスタンスに対して2つの参照をもちます。1つは leaks に、もう1つは a にです。Rust でこういったことはできません。所有者は1人しかいないからです。もし selfleaks へ追加しようとしたら、コンパイラはその値をそこで "移動" させ、その値がどこかへ移動してしまったためにこの関数からその値を返せなくなります。その値を関数から返すには最初にその値を戻すように移動しなければなりません (例えば、リストからその値を削除するなど) 。

それでは、もしオブジェクトに対する2つの参照をもつ必要がある場合、どうすれば良いのでしょうか?実はその値を借用できます。不変借用 (immutable borrows) の数に制限はありませんが、可変借用 (mutable borrow) は1つしかもてません (そして不変借用がない場合に限ります) 。

不変借用を操作する関数は &self を、可変借用を必要とする関数は &mut self を付けます。参照を貸与できるのは所有者の場合のみです。その値をこの関数 (例えば、その値を関数から返す) の外へ移動したい場合、どのような未処理の貸与をもつことはできませんし、所有権をどこかへ移動した後でその値を貸与するのもできません。

これはプログラムについての考え方を大きく変えるものですが、すぐに慣れるでしょう。

ランタイム借用 (Runtime Borrows) と複数所有者 (Multiple Owners) 1

これまでのところ、ほとんど全ての所有権の追跡はコンパイル時に検証されました。しかし、コンパイル時に所有権を検証できない場合はどうなるのでしょうか?自由に利用するために複数の選択肢があります。1つの例は mutex を使うことです。mutex は実行時に1人だけがオブジェクトに対する可変借用をもつことを保証しますが、その mutex 自身がそのオブジェクトを所有します。そうやって、同じオブジェクトにアクセスするコードを書きますが、ある時点でそのオブジェクトにアクセスできるスレッドは1つだけです。

結果として、これもまた mutex を使い忘れてデータ競合を引き起こすといったことが発生しないことを意味します。そういったコードがコンパイルされないからです。

しかし、Python でそういったプログラミングをしたい場合、どうやってメモリの所有者を見つけることができるのでしょうか?そういった場合、参照カウントラッパー内にオブジェクトを設けて、実行時にこちら側へその値を貸与します。単に循環する可能性があるだけで Python の振る舞いに対してとても近いものになります。Python はそのガベージコレクタで循環を分割し、Rust ではそういったものはありません。

もっと良い方法でこのことを説明するために、複雑な Python の例と Rust の等価なものをみてみましょう。

    from threading import Lock, Thread

    def fib(num):
        if num < 2:
            return 1
        return fib(num - 2) + fib(num - 1)

    def thread_prog(mutex, results, i):
        rv = fib(i)
        with mutex:
            results[i] = rv

    def main():
        mutex = Lock()
        results = {}

        threads = []
        for i in xrange(35):
            thread = Thread(target=thread_prog, args=(mutex, results, i))
            threads.append(thread)
            thread.start()

        for thread in threads:
            thread.join()

        for i, rv in sorted(results.items()):
            print "fib({}) = {}".format(i, rv)

ここでやっていることは 35 個のスレッドを生成し、ひどいやり方でフィボナッチ数を増加させる計算をします。それからスレッドを join してソートされた結果を表示します。ここですぐに気付くことの1つは、mutex (ロック) と結果の配列との間に本質的な関係がないことです。

次に Rust の例です。

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

    fn fib(num: u64) -> u64 {
        if num < 2 { 1 } else { fib(num - 2) + fib(num - 1) }
    }

    fn main() {
        let locked_results = Arc::new(Mutex::new(BTreeMap::new()));
        let threads : Vec<_> = (0..35).map(|i| {
            let locked_results = locked_results.clone();
            thread::spawn(move || {
                let rv = fib(i);
                locked_results.lock().unwrap().insert(i, rv);
            })
        }).collect();
        for thread in threads { thread.join().unwrap(); }
        for (i, rv) in locked_results.lock().unwrap().iter() {
            println!("fib({}) = {}", i, rv);
        }
    }

ここで Python のコードとの大きな違いは、ハッシュテーブルの代わりに B ツリーマップを使っていて Arc'ed mutex にそのマップを追加していることです。それは何でしょうか?まず B ツリーを使っている理由は自動的にソートしてくれるからで、ここで必要だったものだからです。それから、実行時にそのマップをロックできるように mutex へ追加します。ここで関係が確立されました。最後にその mutex を Arc に追加します。Arc の参照はそれが包み込んでいるものを数えます。この場合は mutex です。これは最後のスレッドが実行を終了した後になってのみ、mutex を削除できるのを保証するという意味です。巧妙な仕組みです。

それでは、このコードがどう動くのかを説明します。Python のように 35 まで数えて 2、それぞれの数に対してローカル関数を実行します。Python とは異なり、ここではクロージャが使えます。それからローカルスレッド内に Arc のコピーを作ります。これはそれぞれのメソッドから個々に所有している Arc がみえることを意味します (内部的にこれは自動的に参照カウントを増やし、スレッドが死ぬときに減らします) 。それからローカル関数でそのスレッドを生成します。この move はスレッド内にクロージャを移動させるのを伝えます。それからそれぞれのスレッドでフィボナッチ関数を実行します。結果を返す Arc をロックするとき unwrap してから値を追加します。ちょっとの間、この unwrap は無視してください。それは単に明示的な結果を混乱させるからです。しかし、その要点は mutex をアンロックするときのみ、結果のマップを取得できるという点です。うっかりロックし忘れるということはあり得ません!

それから1次元配列 (vector) に全てのスレッドを collect します。最後に全てのスレッドを繰り返し処理で join して結果を表示します。

ここで注目すべきことが2つあります。目に見える型がとても少ないことです。もちろん Arc の型とフィボナッチ関数は unsigned 64bit 整数の型を扱いますが、それ以外に明示されている型はありません。またハッシュ可能オブジェクトの代わりに B ツリーマップを使った理由は Rust がそういった型を提供してくれるからです。

繰り返し処理は Python と全く同じように動作します。唯一の違いは、この例だと Rust では mutex を獲得する必要があるという点です。その理由はコンパイラからは実行が終了したスレッドやその mutex が不要なことを分からないからです。とはいえ、この mutex を必要としない API があり、そういった API は Rust 1.0 においてもまだ安定していません。

パフォーマンス的には、まさに期待した通りのことが起きるでしょう。(この例はスレッドの動作を説明するために意図的にひどいコードを書いています。)

Unicode

Unicode の話題は私のお気に入りです :)Rust と Python でかなり違っているところです。Python (2 と 3 の両方) は Unicode モデルとよく似ていて、それは文字の配列に対して Unicode データを対応付けるというものです。一方 Rust では必ず UTF-8 として格納される Unicode 文字列です。どうしてこれが Python や C# がやろうとしていることよりずっと良い解法なのかということについて私は前に説明しました (UCS vs UTF-8 as Internal String Encoding を参照) 。Rust についての非常に興味深いのは、エンコーディングにまつわる世界の醜い現実をいかに扱うかというところです。

まず最初に Rust はオペレーティングシステムの API (Windows Unicode も Linux 非 Unicode も両方) が全くひどいものだと完全に認識しています。Python とは異なり、そういった分野に Unicode を強制するようなことはしません。その代わり相互に低コストで (合理的に) 変換できる異なる文字列型をもちます。これは実際にやってみるとうまく機能し、文字列操作をとても高速にします。

プログラムの大部分で UTF-8 を受け入れることによりエンコーディング/デコーディングが不要になります。低コストなバリデーションチェックを実行する必要があるだけで、UTF-8 文字列上での処理は途中でエンコードする必要がなくなります。もし Windows Unicode API を統合する必要があるなら、内部的には UTF-16 のように UCS2 に対してかなり低コストで変換できる WTF-8 encoding を使います。

どこでも Unicode と bytes 間の変換が可能で、必要に応じて bytes を扱います。それから後でバリデーションステップを実行して、全て意図した通りであることを保証できます。これにより本当に速くて本当に便利だというのを両立したプロトコルが書けます。これに対して単に O(1) の文字列インデクシングをサポートするために Python だと絶えずエンコーディングとデコーディングを行わなければならないのと比較してみましょう。

Unicode の本当に優れたストレージモデルの他に Unicode を扱うための多くの API もあります。言語の一部分か、素晴らしい crates.io インデックス のどちらかにあります。これは case folding、カテゴリー化、Unicode 正規表現、Unicode 正規化、よく知られた URI/IRI/URL API 群、分割、単に名前の対応付けをする簡単なものを含みます。

欠点は何でしょうか?"föo"[1] のような文字列で意図したように 'ö' を元に戻すことはできません。しかし、いずれにしてもそれは良い考えではありません。

OS の動作と相互にやり取りする方法の例として、カレントディレクトリのファイルを開いて、その内容とファイル名を表示するアプリケーションを紹介します。

    use std::env;
    use std::fs;

    fn example() -> Result<(), Box<Error>> {
        let here = try!(env::current_dir());
        println!("Contents in: {}", here.display());
        for entry in try!(fs::read_dir(&here)) {
            let path = try!(entry).path();
            let md = try!(fs::metadata(&path));
            println!("  {} ({} bytes)", path.display(), md.len());
        }
        Ok(())
    }

    fn main() {
        example().unwrap();
    }

全ての IO 操作は、この前にも紹介した Path オブジェクトを使います。それはオペレーティングシステムの内部的なパスを適切にカプセル化します。それは bytes または unicode かもしれませんし、そのオペレーティングシステムが使っている別の何かかもしれません。しかし、.display() を呼び出すことにより適切にフォーマット化されます。このメソッドは文字列内にそれ自身をフォーマットできるオブジェクトを返します。これは便利です。その理由は、例えば Python 3 でやるような不正な文字列がうっかりリークしてしまうようなことが決して起こらないからです。きれいな関心の分離です。

ディストリビューションとライブラリ

Rust は virtualenv+pip+setuptools を組み合わせた "cargo" があります。まあ、デフォルトでは Rust の1つのバージョンでしか動作しないので厳密に virtualenv 相当というわけではありませんが、それ以外は期待した通りに使えます。Python のそれよりも優れているところは、ライブラリの異なるバージョンの依存関係を git リポジトリまたは crates.io インデックスでできるという点です。ウェブサイトから rust をダウンロードしたなら cargo コマンドが付属していて、全て期待した通りに使えるでしょう。

Rust は Python を置き換えるか?

私は Python と Rust の間に直接的な関係があるとは思いません。例えば、Python はコンピューターサイエンス分野で成功を収めていて、単にどのぐらい多くの作業量が必要になるのかという理由から、近い将来 Rust がその分野に取り組むとは思いません。同様に、Python でシェルスクリプトを書けるのに Rust でそれを書くというのは全く意味がありません。そうは言っても、多くの Python プログラマーが Go を使い始めたように、これまでは Python を使っていた分野で Rust を検討するといったことが始まると私は思います。

Rust はとても強力な言語であり、力強い基盤をもち、自由なライセンスの下、友好的なコミュニティと言語の進化に対して民主的な姿勢で運営されています。

Rust のランタイムサポートはとても小さいものなので Python から ctypes や CFFI 経由ですごく簡単に使えます。私は未来をとても鮮明に想像できました。それは Rust で書いたバイナリーモジュールの配布物を含めた Python パッケージが作られ、開発者に必要とされた様々な労力なく Python から Rust モジュールを呼び出すようになるでしょう。

© Copyright 2015 by Armin Ronacher.
Content licensed under the Creative Commons attribution-noncommercial-sharealike License.


  1. 原文では "Mutible Owners" ですが、Multiple の誤植ではないかと推測します 

  2. 原文では "we count to 20 like in Python," とありますが、おそらく 35 の誤植だと思います 

217
199
2

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
217
199