ろじかるんるんものがたり

病人が特に何も書かない。無駄だからだ。

Rust は何が新しくないのか

disclaimer

追記で注意書き足すのはどうなんでしょうね。ということで追記です。

  • 別にギョムでガンガン書いてるとかではないです。
  • ノミコンの方も含めてドキュメント全部読んだ、API 一通り眺めた、型推論というかリージョン推論部分の概要眺めた、関連する論文読んだ、言語触った、程度の人間が書いてます。
  • 正しくない、不正確な部分もあると思います。
  • 雑です、すいません。

TL;DR 的には、C++ 置き換えるための言語として作られたので当然 C++ にあった概念引き継いでますよみたいな話です。

以下追記前の元の全文。

以下の記事が結構人気と聞きました。

Rustは何が新しいのか(基本的な言語機能の紹介) - いもす研 (imos laboratory)

ここでは、記事中の「新しくない」部分を historical な話を交えて説明する形で何か書きたいと思います。

記事を否定するようなものではないです。寧ろよくまとまっていると思うのですが、綺麗にまとまりすぎていて「分かっている人にしか分からない」部分があるかな、と思い、筆を(キーボードーですけど)取った次第です。対象読者は GC のある言語でしかコードを書いたことが無い人、なのかな。

「Rust のプログラミング言語としての立ち位置」補足

元記事では、言語の今と未来について書かれていますが、ここでは昔話をしましょう。

Mozilla は様々なプロダクトを過去 C++ で実装してきました。巨大なプロダクトが多く、開発には自作の静的解析ツールが利用されていました。

しかし、問題が起きます。静的解析ツールが利用していた C++ をパースするライブラリがバージョンアップされず、C++11 辺りから使えなくなってしまいました。そも、以前から作者はもうメンテナンスしておらず、Mozilla が確かメンテナンスをしていたんだったっけ…ここ嘘かも。

Mozilla(の開発チームの一部)は、言語処理系のプラグインとして静的解析ツールを実装できないか、と考えました。結果、生まれたのが gcc compiler plugin とコンパイラプラグイン Dehydra です。が、gcc の AST(抽象構文木)は、ツールを作るには位置情報等があまりにも不足していました。少し話がそれますが diagnostic を大切なものとして設計された clang の AST は、その辺りかなり配慮のある設計がされています。また、gcc の mangling のタイミングが安定しておらず、コンパイラプラグインにわたってくるタイミングで何故かシンボルが mangling されていたりいなかったり、といったバグもありました。そもそもコンパイラプラグインなんて想定せず開発されていたのでバグともなかなか言いづらいのですが。

自分が見ていた範囲では、上述のようなことを Mozilla は行っていました。clang もコンパイラプラグインの仕組みを持っていたのですが、Mozilla は「代替言語を作る」道を選びます。それは勿論 C++ を捨てることを意味します。

結果として誕生したのが Rust です(Cyclone の話は割愛します)。大規模 C++ プロダクトのコードを置き換えられるような言語でなければいけませんでした。そして、Rust は十分にそれを果たせているように思います。

「スマートポインタが生まれるまで」補足

C++ には GC が存在しません。少なくとも今は。生のアドレス値をポインタ型を使って扱うことができます。逆にいうと、GC がやってくれる「ゴミ捨て」を(GC には compaction 等の仕事もありますが割愛)自分でしなければならない、ということです。GC のない言語を利用したことがない人には想像がなかなかつかないと思うのですが、常にリソースの管理をしながら本来の処理も記述しなければいけない、というのはかなり大変です。

大変ですから、その仕事を楽にしようと仕組みが作られます。元記事で言及されている unique_ptr 以前から、C++ にはスマートポインタが存在しました。様々なライブラリが独自に色々作ったりしていましたが、標準ライブラリからは auto_ptr が提供されていました。これは、Rust に同じく(Copy trait の話は割愛)代入演算子が move となるようなポインタ、のように振舞うクラスでした。しかし、代入演算子が通常はコピーとして使われていた当時、auto_ptr の semantics には多くの開発者が混乱させられました。結果として auto_ptr は「使うな」と言われてしまいました。使うなと言われてはしまいましたけれど、この頃から move semantics は(標準ライブラリレベルで)存在したわけですね。

言語サポートが入ったのは、元記事にある通り C++11 ですが、それ以前から Boost ライブラリ等でもスマートポインタやスレッド(スレッドは所有者が一人でなければならないリソースなので、move が必要です)に対する move のサポートはありました。また、lifetime(寿命)の概念についても Boost scoped_ptr や shared_ptr の頃からライブラリレベルで示されていました。認知度は…兎も角…

「Rust の言語機能」補足

どの機能も、何かしらの既存の言語に存在するものですが、C++ にはなくて辛かった、みたいな機能ですね。一応書いておくと、generics と template は似ているようで全く異なる言語機能です。

結局何が新しくないのか

lifetime や move semantics は C++ に元より存在する概念でした。そも、言語のサポートのあるなしに関わらず、raw pointer を扱う言語であれば、意識しないといけないものです。皆さんも C でこんなコードを書いた経験があるのではないでしょうか。

struct string {
  char *ptr;
  void (*free_fn)(char *ptr);
};
void free_string(char* ptr) { free(ptr); } // malloc などで生成された char* のための開放関数。
void do_nothing(char*) { ; } // リテラルなどで生成された寿命を持たない、もしくは寿命がスタックに管理されている char* のための何もしない解放関数。

デストラクタによる自動解放こそないものの、これはまさしく C++ のスマートポインタにおける custom deleter ですね。結局言語サポートがあるかないかだけで、C だろうが C++ だろうが同じことをやるわけです。ちなみに、このような複数の lifetime を同時に扱う仕組みは、Rust もちゃんとサポートしています。Cow trait だっけ、違ったらすいません。

長くなりましたが、つまり新しくないのは、lifetime や move semantics の概念です。C++11 以降も加えると、move semantics の言語サポートも新しいものではありません。

結局何が新しいのか

消去法で残ったの出すだけです。新しいのは lifetime と linearity の言語サポートです。

Rust では変数の寿命をコンパイラがチェックしてくれます。それは参照も同じです。C++ の参照はごく稀に「この参照触ってる間に死んだりしないよな…」と心配になることがあります。ownership を意識してコードを書いていれば大丈夫なんですが、外部ライブラリとか使ってると信用できませんからね…Rust の借用は、lifetime がコンパイラに管理されている参照です。参照が生きていることはコンパイラが保証してくれるので、安全に利用することができます。

また linearity、雑にいうと変数が一回しか使われてないかどうか、触っちゃいけないもの触ってないか、もチェックされます。これで死んだオブジェクトを触ってアチャーという事故も静的に検出し、防ぐことができるようになります。

Rust の新しい部分は、結局の所メモリなどのリソースを管理する上で、C++ で足りなかった、あって欲しいなと皆が思っていたものと言ってしまっていいでしょう。

C, C++ の話多すぎてわからないんですが

Rust は最初に書いた通り、MozillaC++ で書かれた巨大プロダクトのコードを置き換えられる言語でなければいけませんでした。C++C++ なりに色々頑張っていたので、Rust はそこから良いもの(lifetime, move semantics)を取り出し、足りない部分(lifetime と linearity)を補いました。のでどうしても C++ の話を出さざるを得ません…というかぶっちゃけ Rust って C, C++ 使いがやったーという気持ちになる言語だと思うので、そうでない人に良さ伝えるの、無理ですねこれ(諦念)。

一応諦めずに説明頑張ってみよう。GC のある言語でも、不要になったらこのメソッド読んでね、というようなメソッドを持つクラスはあります。あれを適切なタイミングで呼ぶの、自明なようで大変ですよね。不要になったと思ったら別スレッドでまだ使われていたとか、そういううっかりをやったことがある人はそれなりにいるんじゃないでしょうか。そういったことを常にやらないといけないのが C, C++ で(C++ は頑張ってますが)、それを全力でサポートして、危険なコードにめっしてくれるのが Rust、でどうでしょうか。やっぱり駄目か。

で、全力でサポートしてくれるので、リソース管理を GC に任せっきりで生きてきた人が Rust を書くと、大抵変なコードができあがるわけですが…まあ当然ですよね…

まとめ

話が散漫としすぎたのでまとめます。

  • Rust の lifetime の概念や move semantics の言語サポートは C++ 等で以前よりあった、特別新しいものではない。
  • Rust の lifetime や linearity の言語サポートは(研究用の言語とか除けば)新しいものである。
  • Rust の motivation に C++ が絡むので、C++ 知らないと Rust の嬉しさを真に理解するのは難しい。
    • C++ での lifetime の概念や move semantics の言語サポートですら難しいのに、それにまだ言語サポートが増えるんだから当然のこと。でも、安全なコードを書くためには必要で、(C++ を書いていた)皆が望んでいたもの。

うーんやっぱり Rust の良さ、新しさを C, C++ を書いてこなかった人たちに説明するのって難しいですね…雑におわります。

追記:みんなが望んでいたものとか書いてるけれど世の中にはスマポ使わない派とか free しない派もいるので多様性です。