Rustを使っていると時々「あれ?この機能、他の言語に似てない?」と思うことがあります。最初に思ったのはトレイトで、これはHaskellの型クラスやScalaのImplicitsを使った型クラスパターンと同等な機能と理解しました。クロージャのシンタックスはRubyのブロック記法に似ているなと感じました。そんな「似ている」を少しだけ深堀りしてみたいと思い、Rustに影響を与えた言語を調べて見ました。
(本記事は自分のブログからの転載記事です。)
TL;DR
-
- Rustに影響を与えた言語に関してはすでにまとまったページがありました
Influences – The Rust Reference
Why Rust? – #Influences | Learning Rust
この記事は上記のページをベースに以下のようにまとめ直して紹介しています
影響を受けた言語の特徴を表形式でまとめる
影響を受けた機能の簡単な紹介
影響の可視化(マインドマップ風)
Rustが影響を受けた言語
「Influences」に記載されている言語1を年代順に並べて、言語の特徴をマトリックスにしてみました。特徴の選択はGCを除いてRustが力を入れているパラダイムを選択しています。また比較のためにRust自身も加えてあります。
各カラムの意味は次のとおりです。言語の特徴は主にWikipediaを参考にしていますが、正確な分類は困難なため多少の独断と偏見が含まれていることをご了承ください。
-
- 登場年代
プログラミング言語が登場した年代です。前後3年の誤差は見逃してください
FP(関数型プログラミング)
言語がFPを強くサポートしているかを示しています
程々にサポートしている場合は△を示しています
OOP(オブジェクト指向プログラミング)
言語がOOPを強くサポートしているかを示しています
並行計算
アクターや CSP/π計算モデルの特徴を言語が強くサポートしているかを示しています
外部ライブラリを使えばできるよ!みたいなものは除外します
静的型付け
言語の最も主要な処理系が静的型付けをサポートしているかを示しています
パラメータ多相
言語がパラメータ多相をサポートしているかを示しています
ジェネリクス(Java)、テンプレート(C++)、let多相(ML系言語)等と呼ばれるものが含まれています
アドホック多相
言語がアドホック多相をサポートしているかを示しています
型クラス(Haskell)、トレイト(Rust)、プロトコル(Swift)等と呼ばれるものが含まれています
単純な多重定義(オーバーロード)は含まれていません
GC(ガベージコレクション)
言語の最も主要な処理系がガベージコレクションを採用しているかを示しています
Rustが影響を受けた機能
ここからは影響を受けた機能をそれぞれ見ていきたいと思います。
代数的データ型(algebraic data types)
代数的データ型は関数型プログラミングをサポートする言語で多く利用されています。直積型の総和です。RustではEnumを用いることで代数的データ型を実現可能です。
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
上記はIPアドレス型を表現していますが、V4型はu8型の直積になっており、V4型とV6型の総和がIpAddr型になっています。
-
- 影響を受けた言語
SML, OCaml
パターンマッチ(pattern matching)
パターンマッチはざっくり言うと、ifやswitch等の一般的な分岐構造の強化版です。Rustではmatchでパターンマッチが利用可能です。
let x = Some(2);
match x {
Some(10) => println!("10"), // `Option`型を解体してマッチングしている
Some(y) => println!("x = {:?}", y),
_ => println!("error!"),
}
パターンマッチは上記のコードのようにデータ構造を分解してマッチングできるので、代数的データ型とも相性がよいです。
パターンマッチに関しては過去に「全プログラマに捧ぐ!図解「パターンマッチ」」という記事も書いたのでよろしければそちらも参考にしてださい。
-
- Rust本
パターンマッチング
リファレンス
Patterns – The Rust Reference
影響を受けた言語
SML, OCaml
型推論(type inference)
静的型付けを持つ言語ではJavaやC#に採用されたこともあり、型推論が徐々に浸透してきています。Rustの型推論は強力なHindley-Milner型推論をベースとしており、後方の式や文から手前の型が推論されることもあります。
let mut x = HashMap::new(); // HashMapのキーと値の型は後方の式や文から推論される。便利!
x.insert(String::from("Key"), 10);
println!("{:?}", x);
上記の例ではHashMapのキーと値の型が後続のinsert関数の引数から推論されています。JavaやC#の型推論ではこれはできません。それはこれらの言語に導入された型推論はローカル変数の型推論であり、Rustの型推論とは異なっているからです。
-
- 影響を受けた言語
SML, OCaml
セミコロンの文区切り(semicolon statement separation)
Rustの関数は主に「文」で構成され、一番最後は「文」か「式」で終わります。Rustの文と文の区切りはセミコロン「;」です。従って最後にセミコロンが付くか否かで文か式かを区別できます。
fn test1() -> i32 {
let mut x = 2; // 文
x = x * 3 ; // 文
x + 5 // 式(戻り値は11)
}
fn test2() -> () {
let mut x = 2; // 文
x = x * 3 ; // 文
x + 5; // 文(戻り値はユニット`()`)
}
Rustの面白いところは、全ての「式」の後にセミコロン「;」を付けるだけで「文」に変換できてしまうところです。そして文になると関数の最後に置かれた場合の戻り値はユニット()になります。上記のコードで説明すると一番最後の行にセミコロン(;)があるかないかで戻り値が変わります6。つまりRustにおいては「文」も「値」を返す「式」の一種と考えることができます。また、ifやmatchやwhileのような制御構造も値を返すのでRustは式指向の言語とも言われています。
影響を受けた言語であるOCamlでも同様にプログラムの構成要素の基本に「式」を置いて、セミコロンで区切るやり方になっています。
-
- 参考文献
OCamlプログラムの構造 – OCaml
影響を受けた言語
SML, OCaml
参照(references)
参照は変数に&をつけることで生成でき、変数に「別名」を作ることができます。C言語のポインタに似た機能ですが、大きな違いはnullポインタが存在しないことです。つまり必ず参照先があることが前提となります。参照先は参照外し演算子「*」を用いることで参照できます。可変(mut付き)の変数の場合は参照先の書き換えも可能です。
let mut x = "hoge".to_string();
let y = &mut x;
println!("y = {}", *y); // hoge
*y = "mohe".to_string(); // `*y`を書き換えることでxも書き換わる
println!("x = {}", x); // mohe
この参照はC++の特徴的な機能と考えられており紛れもなくC++の影響と言えそうですが、大きな違いもあります。それはRustの「所有権とライフタイム」との紐づきであり、C++と異なりRustの参照がダングリング参照になることはありません。
-
- 影響を受けた言語
C++
RAII(Resource Acquisition Is Initialization)
RAIIは直訳では「リソースの確保は(変数の)初期化である」になります。しかしこの概念をより適切に理解するためには「リソースの解放は変数の破棄である」と捉えたほうが真に迫っていると思われます。一般的なリソースの代表例がメモリであり、この場合は変数が初期化されるとメモリが確保され、変数が破棄されるとメモリが解放されます。このRAIIという表現はRustでは表立って使われることは少ないですが、「所有権」や「スコープ」という考え方の中に取り入れられています。
{
let x = 10; // ここで変数xが初期化され、メモリも確保される。
{
let y = "hoge".to_string(); // ここで変数yが初期化され、メモリも確保される。
// いろいろ処理する
} // ここで変数yはスコープを抜けて破棄され、メモリも解放される
// いろいろ処理する
} // ここで変数xはスコープを抜けて破棄され、メモリも解放される
上記のコードの例では変数の有効範囲は変数が初期化されてから、最も内側のスコープ(中括弧{}で囲まれた範囲)の最後までです。JavaやRubyのようなガーベージコレクション(GC)を持つ言語では変数が破棄された後も、ガベージコレクタがメモリを回収するまでメモリが解放されません7。また上記のコードの例ではメモリをリソースとしましたが、メモリ以外でもファイルのオープン、クローズ等の「利用」と「返却」に結びつけても構いません。実際にRustの標準ライブラリの中にはRAIIを利用したものが多くあります。
-
- Rust By Example
RAII
影響を受けた言語
C++
スマートポインタ(smart pointers)
スマートポインタは、ポインタの一種で単にメモリアドレスを指し示すだけではなく付加的な機能を備えたもののことを言います。Rustにおけるスマートポインタとは標準ライブラリの型で言えばStringやVecのように、ヒープのメモリ確保と解放を「スマート」に行うものが第一に挙げられます。Rustではスマートポインタを見分けるポイントとして、「Derefトレイト」や「Dropトレイト」を実装しているかが挙げられます。
{
let a = String::from("hoge"); // 文字列"hoge"はヒープに確保される
let b = vec![1, 2, 3]; // ベクタはヒープに確保される
let c = Box::new(5); // i32型の整数はヒープに確保される
} // 変数a, b, cが破棄され、同時にヒープに確保されたメモリも解放される
-
- Rust本
スマートポインタ
影響を受けた言語
C++
ムーブセマンティクス(move semantics)
ムーブセマンティクスとはざっくり言うと、値を変数にアサインしたり、関数を引数に値渡ししたりするときに所有権の移動が行われることを言います。
let s1 = "Rust Life".to_string();
println!("{}", s1); // OK
let s2 = s1; // ムーブセマンティクス:所有権が`s1`から`s2`に移動している
println!("{}", s2); // OK
println!("{}", s1); // コンパイルエラー: 所有権は`s2`に移動しているので`s1`にアクセス不可
上記のコードではlet s2 = s1;がムーブセマンティクスになっています。Rustでは値の所有権は常にひとつに制限されているのでこのような動作になります。
-
- Rust本
所有権とは?
Rust By Example
所有権とムーブ
影響を受けた言語
C++
単相化(monomorphization)
Rustのジェネリクスはコンパイル時にプログラム内で利用される具体的な型に展開されますが、これは「単相化」と呼ばれています。「単相化」によってコンパイル時に呼び出される関数が決定されるので静的ディスパッチになり、抽象化に伴う実行時の呼び出しオーバーヘッドがありません。
影響を受けたC++では、単相化はテンプレートのインスタンス化、特殊化として知られています。
-
- 影響を受けた言語
C++
メモリモデル(memory model)
メモリモデルが意味するものはいくつか挙げられますが、この文脈おけるメモリモデルはマルチスレッド環境における共有メモリアクセスの一貫性に関するものになります。一般的にマルチスレッドから安全に操作できるものとして「アトミックな操作」がありますが、これらを実現するためにメモリモデルが必要になってきます。詳細が知りたい方は以下のRustのドキュメントを参照してください。
-
- 標準ライブラリドキュメント
std::sync::atomic
std::sync::atomic::Ordering
影響を受けた言語
C++
リージョンベースのメモリ管理(region based memory management)
リージョンベースのメモリ管理では、メモリを「リージョン」と呼ばれる領域に分割して、さらに型システムに関連付けてメモリ管理を行います。Rustにおいては参照のライフタイム管理に大きく関わっているものと思われます。
-
- 参考文献
Region-Based Memory Management in Cyclone
Cyclone: Memory Management Via Regions
影響を受けた言語
ML Kit, Cyclone
型クラス(typeclasses)、型族(type families)
「型クラス」はHaskell由来の言葉でRustで対応する機能はトレイトになり、型に共通する振る舞いを定義するときに用いられます。Javaのインタフェースに近いものがありますが、型の定義時ではなく型の定義後に後付でトレイトを実装できることが特徴です。
trait Greeting { // トレイトの定義
fn greet(&self) -> String;
}
fn print_greet<T: Greeting>(person: T) { // トレイト境界を用いた関数
println!("{}!", person.greet());
}
struct Japanese { name: String, } // `struct`を用いた型の定義
struct American { name: String, age: u32,}
impl Greeting for Japanese { // トレイトの実装
fn greet(&self) -> String { "こんにちわ".to_string() }
}
impl Greeting for American {
fn greet(&self) -> String { "Hello".to_string() }
}
impl Greeting for i32 { // 組み込み型にもトレイトを実装できる!
fn greet(&self) -> String { self.to_string() }
}
fn main() {
let person_a = Japanese {name: "Taro".to_string(),};
let person_b = American {name: "Alex".to_string(), age: 20,};
// print_greet関数はGreetingを実装した異なる型に対して呼び出し可能(アドホック多相)
print_greet(person_a);
print_greet(person_b);
print_greet(123);
}
上記のコードで説明すると、print_greet()関数はトレイトGreetingを実装していれば呼べる関数になっています。そしてすでにJapaneseという型が定義されていた場合、Greetingトレイトを実装(impl Greeting for Japanese)すれば、print_greet()関数で呼び出すことができます。面白いのはi32のような組み込み型にも後付でトレイトが実装できることです。このprint_greet()関数のように、後付けで渡せる型を増やせる関数の性質をアドホック多相性と言ったりします。
「型族」は、ざっくり説明すると型を受け取って型を返す型関数を実現する機能です。Rustでは「関連型」と繋がりがあります。標準ライブラリのAddから定義と利用例を引用します。
pub trait Add<Rhs = Self> {
type Output; // 関連型
fn add(self, rhs: Rhs) -> Self::Output;
}
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Self; // 関連型
fn add(self, other: Self) -> Self {
Self {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
assert_eq!(Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
Point { x: 3, y: 3 });
関連型は上記のコードのように、トレイトの中でtypeを使って宣言されます。ジェネリクスと似たようなこともできますがきちんと使いどころもあります。以下の参考文献に細かなシチュエーションが載っていたので興味がある方はご確認ください。
-
- 関連型の参考文献
関連型が必要になる状況 – Rust By Example 日本語版
Rustの関連型の使いどころ | κeenのHappy Hacκing Blog
影響を受けた言語
Haskell
チャネル(channels), 並行性(concurrency)
チャネルは非同期コミュニケーションのためのプリミティブです。送信側と受信側がチャネルを通して非同期にデータの受け渡しをすることができます。以下のコードはチャネル – Rust By Example 日本語版からコード部分を引用したものです(コメントは独自のものに変更)。
use std::sync::mpsc::{Sender, Receiver};
use std::sync::mpsc;
use std::thread;
static NTHREADS: i32 = 3;
fn main() {
let (tx, rx): (Sender<i32>, Receiver<i32>) = mpsc::channel(); // チャネルの作成
let mut children = Vec::new();
for id in 0..NTHREADS {
let thread_tx = tx.clone();
let child = thread::spawn(move || { // スレッドの作成
thread_tx.send(id).unwrap(); // チャネルを通してデータの送信
println!("thread {} finished", id);
});
children.push(child);
}
let mut ids = Vec::with_capacity(NTHREADS as usize);
for _ in 0..NTHREADS {
ids.push(rx.recv()); // 子スレッドから送信されたデータを受信
}
for child in children {
child.join().expect("oops! the child thread panicked");
}
println!("{:?}", ids);
}
上記のコードはチャネルを生成して子スレッドに渡して、子スレッドからチャネルを通してデータを送信して親スレッド受け取るコードになっています。
-
- 影響を受けた言語
Newsqueak, Alef, Limbo
メッセージパッシング(message passing), スレッド失敗(thread failure)
調べきれなかったので割愛します。
-
- 影響を受けた言語
Erlang
オプショナルバインディング
オプショナルバインディングはSwiftの機能で、その名の通りOptionalの値が存在する場合に変数を束縛してコードブロックを実行します。Rustの対応する機能はif letですが、Optionに限らず様々なパターンマッチか利用可能です。
let num = Some(10);
if let Some(i) = num {
println!("num = {}", i);
}
-
- Rust本
if letで簡潔な制御フロー
影響を受けた言語
Swift
衛生的マクロ(hygienic macros)
衛生的マクロとはマクロ内で導入される変数名と、マクロ呼び出し側の変数名が衝突しないことが保証されているマクロです。以下は簡単なRustのマクロのサンプルコードです。
macro_rules! my_macro { // マクロ
($x:expr) => {
{
let a = 2;
$x + a
}
};
}
fn main() {
let a = 5;
println!("{}", my_macro!(a)); // 7
}
Rustのマクロは衛生的なため、my_macro!マクロに変数aが渡されても内部のletで導入された変数aとは別物とし扱われます。これがC言語のマクロやLispマクロでは衝突する可能性があるため、意図的に衝突しない変数を選ぶ必要がありました。
-
- 影響を受けた言語
Scheme
属性(attributes)
属性は主に宣言に対して付加される追加情報(メタデータ)です。Rustでよく見かけるのは単体テストのマークとなる#[test]属性です。
#[test] // 属性(テスト関数のマーキング)
fn test_hoge() {
// test code
}
#[allow(dead_code)] // 属性(未使用関数の警告抑制)
fn foo() {}
#[derive(Debug, Clone, Copy, Default, Eq, Hash, Ord, PartialOrd, PartialEq)] // 属性(トレイトの自動実装)
struct Num(i32);
-
- リファレンス
Attributes – The Rust Reference
Rust By Example
アトリビュート
影響を受けた言語
C#
クロージャー記法(closure syntax)
これはRubyのブロック記法とRustのクロージャ記法を見比べて貰えば似ていることがおわかり頂けると思います。
ia = [1,2,3]
ia.each {|e| puts e } # Rubyのブロック(`each`の引数)
let ia = [1, 2, 3];
ia.iter().for_each(|e| println!("{}", e)); // Rustのクロージャ(`for_each`の引数)
-
- 影響を受けた言語
Ruby
影響の可視化
Rustに影響を与えた言語を可視化してみました。言語は年代順に時計回りでざっくり並べています。色はFP,OOP,並行計算,その他でざっくり分類しています。
この図を見ると、様々なパラダイムの言語からバランス良く影響を受けている様子が見て取れます。
まとめ
Rustに影響を与えた言語についてざっくり表に分類して、さらに可視化してみました。また、影響を与えた個々の機能に関しても大まかに紹介しました。元ネタになったRustリファレンスのInfluencesの記載は以下のとおりです。
-
- SML, OCaml: algebraic data types, pattern matching, type inference, semicolon statement separation
-
- C++: references, RAII, smart pointers, move semantics, monomorphization, memory model
-
- ML Kit, Cyclone: region based memory management
-
- Haskell (GHC): typeclasses, type families
-
- Newsqueak, Alef, Limbo: channels, concurrency
-
- Erlang: message passing, thread failure, linked thread failure, lightweight concurrency
Swift: optional bindings
Scheme: hygienic macros
C#: attributes
Ruby: closure syntax, block syntax
NIL, Hermes: typestate
削除された機能のみに紐づく言語なので本記事では扱わなかった
Unicode Annex #31: identifier and pattern syntax
言語ではないので本記事では扱わなかった
Rustは一見すると多くの先進的な機能が詰まっているように見えますが、その多くは研究成果や実績のある言語を下敷きにしていることが分かります。Rustの特徴としてよく話題になる所有権システムや借用チェッカーでさえもC++,ML Kit,Cyclone等の言語から多くの影響を受けています。そして個々の影響を調べると、ML KitやCycloneで成し遂げられなかった完全なGCとの決別をRustで実現できたという流れも見えたのは面白かったです。
自分も調べてみるまで、ここまで多くの機能が他の言語由来だとは思っていませんでした。影響を受けた言語の年代やパラダイムもバラエティに富んでおり、Rustを調べている内にさながら言語の進化の歴史を学んでいるような感覚に陥りました。そしてRustはそれぞれの言語の欠点をうまく乗り越えて、良い点をうまく統合していく過程を見せてくれたような気がします。
確かにこれだけの言語から影響を受ければ初学者にとって学習曲線が急峻だと言われるのも分かりますが、逆を考えると影響を受けた様々な言語の集大成を一つの言語で学べる非常に学びがいがあるお得な言語だと言えるのではないかと、これを書きながら思った次第です。
本記事がRustに興味がある方々の一助になれば幸いです。
参考文献
-
- Influences – The Rust Reference
-
- Why Rust? – #Influences | Learning Rust
-
- C言語 – Wikipedia
-
- Scheme – Wikipedia
-
- C++ – Wikipedia
-
- Newsqueak – Wikipedia
-
- Erlang – Wikipedia
-
- Standard ML – Wikipedia
-
- Haskell – Wikipedia
-
- Alef (programming language) – Wikipedia
-
- Limbo – Wikipedia
-
- Ruby – Wikipedia
-
- OCaml – Wikipedia
-
- C Sharp – Wikipedia
-
- Cyclone (programming language) – Wikipedia
-
- Swift (プログラミング言語) – Wikipedia
-
- Rust (プログラミング言語) – Wikipedia
-
- Programming with Regions in the ML Kit
-
- Rustの型推論の概略 – 簡潔なQ
-
- メモリモデル (プログラミング) – Wikipedia
- Rustの Arc を読む(4): アトミック変数とメモリ順序 – Qiita
Unicode Annexは言語ではないので除外しました。NIL, Hermesはすでに廃止された機能への影響だったので、これも除外しました。C言語はWhy Rust? – #Influences | Learning Rustに記載があったので追加しました。F#も入れるかどうか悩みましたが「Functional Programing」だけだと具体的に影響を与えた機能が分かりづらいので入れませんでした。 ↩
Programming with Regions in the ML Kitの発表年を基準にしています。 ↩
ML Kitの実装にはリージョンベースのメモリ管理にGCを付けたものもあるので△にしています。(参考文献) ↩
GCの利用はオプションなので△にしています。具体的にはヒープリージョンにガベージコレクションを利用できます。 ↩
SwiftではARC(Automatic Reference Counting / 自動参照カウント)が使われています。ARCをGCに分類するかは議論の余地がありますが、この記事ではARCの実行時オーバーヘッドの存在を考慮して「GC」として分類しています。 ↩
戻り値がユニット()との場合、関数のシグネチャから戻り値を省略できます。つまりfn test2 -> () {…}はfn test2() {…}と同義です。 ↩
Javaでもプリミティブ型などはローカル変数として宣言された場合にはスタックに確保されておりRAIIの要件を満たしていますが、その他のオブジェクトはほとんどヒープに確保されてガーベージコレクションの対象になります。 ↩