Rustは「学習コストが高い?」「関数型プログラミング言語?」
と思っていた私が、実際にRustに触れてみて得られた知見をまとめました。
プログラミング言語の多くの課題を解決してくれる言語
Rustは「安全性」「速度」「並列性」を考慮して設計されています。
言語セマンティクスを強く定義し(Ownershipやlifetime 後述)、ネットワークコネクションなどのリソース管理全般に関するバグを、コンパイル時に発見してくれることで実現しているといえます。
最初はコンパイルが厳しくて「書いたコードはあってるはず...」
と、陥るほどに暗黙的な印象を感じます。コンパイルの厳格さは、人間が間違えないようにするために、静的に解析できる情報をなるべく多く与え、機械的に検証するためだと考えます。
ひと言でまとめると「コンパイルへの押し付け」です。
安全性に重きをおきながら、zero cost abstractions といった ゼロコスト抽象化 についても設計が及んでいます。つまり抽象化レベルが高く、コードを安全に書けるので、高い生産性を出せるかと。余分なオーバーヘッドもなく、C++ 並に速いです。
モダンなプログラミングもできる
Rustは、モダンな開発をするにあたって、あると嬉しい以下の要素を備えています。
Pattern Matching
パターンマッチは値、特に列や木のような構造を持つ値の中に特定のパターンに合致する部分があるかどうかを探し、また合致した部分を取り出すことです。
rustの場合は以下のようにmatch
を使用して記述することができます。
fn foo() -> uint {
// ...
}
let x: uint = foo();
match x {
0 => println!("nothing"),
1 => println!("only one"),
_ => println!("many")
}
C言語のswitchなどではうまく表現することができず、ifの行列を使ったりdefault:の中で分岐したりしている場合に有効かもしれません。
Algebraic data type(代数的データ型)
代数的データ型に馴染みがない方もいるかと思いますが、以下の3つを合わせて代数的データ型と呼びます。
・列挙型 (種類を区別するための型。enumに相当します。)
・直積型 (内部に値を持つ型)
・直和型 (列挙型にフィールドを付加することで、複数の直積型を定義したもの。列挙型と直積型の両方の特徴を持っている。)
またウィキペディアからの引用ですが、「感覚的な説明としては、引数で与えられた他のデータ型の値を、コンストラクタで包んだようなもの」だそう。ちょっとわかりづらいですが、木構造で表現される値からなるデータ型です。
Haskell なんかでは取り扱うあらゆるデータ型は代数的データ型です(配列のような一部の例外は別)。
enum Option<T> {
None,
Some(T)
}
// x : Option<t>
match x {
None => false,
Some(_) => true
}
data Maybe a =
Nothing
| Just a
{- x : Maybe t -}
case x of
Nothing -> False
Just _ -> True
Traits
Traitsは色々な応用例が考えられます。
テンプレートクラスの実装時みたいに静的な文字列や値を返すのも良いのですが、型に応じたものを返すという使い方ができると、幅が広がります。
またC++、Java、C#のようにオブジェクトクラスと混同してしまいそうになりますが、トレイトは型クラスです。よってトレイトを、プリミティブ型を含む任意のデータ型に追加することができます。
traitsに触れたときは、小難しく感じましたが、簡単にまとめると以下のようなものです。
・Traitsとは、テンプレートの特殊化を応用したテクニック
・Traitsを使うと、型に応じた情報を取得できる仕組みが作れる
・Traitsとテンプレートクラスを組み合わせると、1つの型引数から対応する別の型を取得したり、型引数に対する制約をかけたりできる
その他Rustの魅力的なところ
Zero Cost Abstractions
C++との最も顕著な違いは、RustはGCの動作方法の一つである参照カウント(reference counting) ではなく、ライフタイム(実行時のゼロコスト)を使用してメモリを追跡できることです。
C++では、抽象化を取得せず、自分自身を動かしたり自由にしたり、何らかの形でGCのコストを払っています。
FFI
FFIという別のプログラミング言語の関数を呼び出すインターフェースがあります。
(C++は呼び出せない..?)
他言語コードのためのバインディングを書く導入のために snappy という圧縮・展開ライブラリを使います。ランタイムレスなので、Rustで書いておけば対象のプログラミング言語の資産になるかと。
Borrowing
Rustの魅力的な機能の1つ
Borrowingはメモリ管理の仕組みとしての「Ownership」とよくセットで説明されており、ownershipを移さずに値への参照を可能にする機構です。&キーワードで値への参照を作ると、それは値をborrowingする為のpointerとなります。
またRustにおいて、ownerとborrowerにはそれぞれ独自の権限があるので、以下にまとめます。
owner の権限
リソースがいつ解放されるかを制御出来る。
リソースを immutable な形で多くの borrower に貸し与えることが出来る。
リソースを mutable な形で1つの borrower に貸し与えることが出来る。
※これは次の2つの制約の存在も意味します。
owner の制約
既に誰かに貸し与えたリソースを変更したり、mutable な形で別の誰かに貸し与えたりする事は出来ない。
既に誰かに mutable な形で貸し与えたリソースは、アクセスすることが出来ない。
つまり変更によって変な競合が起きる可能性のある事は禁止されてる?
borrower の権限。
borrowがimmutableなら、リソースの読み取りが出来る。
borrowがmutableなら、リソースの読み書きが出来る。
他の誰かにリソースを貸し与える事が出来る。
ただし、borrowerにも重要な制約があり、
「他の誰かにリソースを貸し与えたら、自分のborrow backの前に貸し与えた分を borrow backしてもらわなければならない」
この制約が守られないと、例えばownerのスコープが終了してメモリが解放され、不正な値となった領域にborrowerがアクセスする、といった危険な事が起きてしまう。そういった意味で、大事な制約となっています。
Go言語との比較
Rustを調査するにあたり、Go言語との比較をよく目にしました。
以下は拾ってきた意見たち...
Rustを選択派
1.演算子オーバーロードが使いたかった
→Goには演算子オーバーロードがない
2.ジェネリクス(テンプレート?)が欲しかった
→Goにはジェネリクスがないらしい
3.ガーベッジコレクタ(GC)が気に入らない
→一応GoはGCをオフにもできる?
Goを選択派
GoはCSPチャンネルをコアプリミティブとして持ってる。
元Cプログラマーからするとラーニングカーブがゆるい。
他にも色々ありました。
まとめ
Rustは学習コストが高いプログラミング言語と思っていましたが、そもそもRustのような言語(システムプログラミングができる・OSが書ける)を学ぶことは「流行に左右されるような技術の尻を追いかけるよりも、土台となる技術を身につけることが大切」ということを実感できるものでした。
それは、本来プログラムを正しく書くこと自体が難しく、規模が大きければなおさらであるということ。 動的な言語で、なんとなくプログラムが動いていたということを可視化してくれることではないでしょうか。
またRustの特徴を具体的にあげるとするなら以下の3つになりそうです。
・ガベージコレクションなしのメモリの安全性
・データ競合のない同時実行性
・オーバーヘッドのない抽象化
その他の特徴
・TypeSystemで型を意識して設計されている。
・必要に応じてunsafeなコードが書ける。
・DynamicDispatchよりも、StaticDispatchが推奨されている。
・C++プログラマーでC++の長所と短所を理解している人は、その差分をうまく埋められるプログラミング言語。
以下は他のプログラミング言語からインスパイアされている代表的なものです。
-
C++
-
references
-
RAII
-
smart pointers
-
move semantics
-
monomorphization
-
memory model
-
Cyclone
-
region based memory management
-
SML, OCaml
-
type inference
-
algebraic data types
-
pattern matching
-
Haskell
-
typeclasses
-
Scheme
-
hygienic macros