個人的に、今後10年で最もユーザ数が増える言語はRustだと思っています。
Rustの良いところは色々ありますが、その中でも「広く普及する言語が持つべき要素」である、
- シンタックスのシンプルさ(Rubyのような複雑な構文を持つ言語はパーサーが巨大になり、より単純なPythonと比べて(バッド寄りな)ノウハウが増えがち)
- コンパイラ、パッケージ/ライブラリ管理、プロファイラ、Lint/Formatterといったフルツールチェーンの公式サポート(と、それらの活発なアップデート)
- 標準ライブラリの充実、LLVMベースの処理系の実装品質の良さ、既存のOSSエコシステムとの親和性の良さ
をきちんと備えていることと、Rustならではの機能である
- データライフサイクルとメモリ管理を言語仕様のレベルで結びつける「所有」の仕組みにより、コンパイル時にメモリ保護チェックが可能
- それにより、ガベージコレクション方式より効率が良く、C/C++のnew/deleteようなプログラマ責任のメモリ管理よりも安全性が高いという、従来相反する性質だった2極を高次に両立させている
- オブジェクト指向や関数型言語の中で、現場のソフトウェア開発においてよく使う部分をバランスよく言語仕様に取り込んだことによる生産性と学習コストの良バランス
- 以上により、低レベル(例えば、OSカーネルやファームウェア)から高レベル(例えば、高度に抽象化されたアプリケーション基盤とか)まで効率を犠牲にすることなく対応できるというカバレッジの広さ
が主要な魅力ポイントだと思います。
では、以下ではそのコア機能である「所有」についてさらっと説明しましょう。
(以降、中級者以上向け)
スタックとヒープ
C++と同様、ローカル変数はスタックに置かれますし、Stringなどのサイズ可変な変数は参照がスタックに置かれて内容がヒープに置かれます。
で、基本的なメモリ管理の挙動としてはRAIIだと思ってください。ただし、RAIIでもスコープ内でshallow copyすると2重デストラクションが起きえます。
それを回避するためにRustは「所有権」という概念を導入しており、その名の通り、コード上の各ステップでどの識別子がどの実体を所有しているのかをコンパイラがステップごとに判断します。
「コード上のステップ」はもちろんレキシカルな意味での位置です。さすがにダイナミックな文脈までコンパイル時にカバーできるほどセマンティクスが定式化されているわけではありません。
制御構文と所有権
変数識別子への代入構文を見てコンパイラが判断するとなると、条件分岐やループといった制御構文のなかで動的に所有権が移りうるケースではどうなるのだ?というのが次の疑問になりますよね。
それについてはRustは単純にfail safeな仕様を採用しています。つまり、if文であればいずれかの分岐のどこかで所有権移動が起きうる限り、もともと所有権を持っていた識別子は条件分岐構文のあとのステップで所有権を保証されないとみなす、というものです。ループに関しても同様で、たとえ論理的には1回で終わるループも構文的にみて2回以上回るという保守的な判断をコンパイラが下してチェックが入ります。
これも単純にコンパイラは構文だけをみるようで、論理的に条件分岐のいずれに転んでも所有権が残ることが数学的に証明できるような場合でも、コンパイラは保証なしとするようです。まぁそのほうが言語仕様として単純でいいですよね。コードを書き換えたら数学的な保証がはずれてデグレードしちゃうケースとかよくあることですしね。
並行処理と所有権
では次にスレッドなどの並行処理が入った場合に所有権という機能はメモリ管理をコンパイラチェックで本当に単純化できるのか?という疑問が浮かびますよね?
これについてもRustはすごく単純に、「ヒープに対して所有権(もしくは借用権)を持つ識別子のうち、一つしかmutableに出来ない」という仕様で解決しています。なので、ここまで書いてこなかったですが、変数はデフォルトでimmutableです。
この仕様はめっちゃ単純だけど個人的にかなり膝を打つような感動を覚えました。「単純にfail safeな仕様を採用することでコンパイラを複雑にせずに安全性の底上げを狙う」というRustの設計思想が透けて見える部分であります。
というわけで
Rustのいい点について冒頭で箇条書きし、コア機能である「所有権」について中級者以上のプログラマ向けに説明してみました。
ではまた。