このエントリーをはてなブックマークに追加

Rustは何が新しいのか(基本的な言語機能の紹介)

Rust は、Firefox を開発する Mozilla が開発し、次世代ブラウザの開発に使っているプログラミング言語です。借用検査という概念を導入することによりメモリ安全およびデータ競合安全をコンパイラが保証する言語であり、2015年中頃の安定版のリリースあたりから次第に注目を集めるようになりました。
メモリ安全とは、メモリの範囲外アクセスや二重解放、ヌル参照、未初期化領域へのアクセスがない状態を表します。ただし、Rust の言うメモリ安全とは、メモリリークをしないことを保証するものではありません。
データ競合安全とは、あるひとつのオブジェクトに対しての読み込みおよび書き込みのが同時に起き結果が不定になる状態にならないことを表します。競合状態とは異なります
無名関数という概念を様々な言語が次々と導入したように、プログラミング言語は相互に影響を及ぼし徐々に変化しています。Rust は「寿命」を変数の型に含めることによりメモリ管理を行うという概念を導入し、メモリ安全性など様々な健全性が実現できることが証明されたので、今後他の言語(特に C++)に影響を及ぼすかもしれません。そこで、Rust を使ったことない人も Rust を書かずして Rust の哲学を理解し、今後他の言語にどのような影響を及ぼしうるのかを考える一助になればと思い、この記事を書きました。

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

Rust は、関数型言語で標準的となった機能を多く取り入れ、システムソフトウェアに使われることを考えた、命令型言語です。プログラマがメモリ管理をすべて制御できるため、組み込みを始めとした制約が強い環境での用途にも堪えますし、C 言語に匹敵するレベルの実行速度最適化も可能です。
関数型言語とは、副作用(変数の内容を書き換えること)を避けたプログラミング言語です。関数型言語は言語研究との親和性が高く、様々な概念が関数型言語の上で試され組み込まれていきました。無名関数を始めとした概念はすでに様々な命令型言語にも組み込まれていますが、Rust はより多くの概念を取り込んでいます。
システムソフトウェアとは、アプリケーションよりもシステムあるいはハードウェアに近い領域のソフトウェアのことを指します。ただし、例えばブラウザは、Web アプリケーションから見ればシステムソフトウェアの要素を持ちますが、OS から見るとアプリケーションソフトウェアの要素を持つなど、アプリケーションソフトウェアとシステムソフトウェアの境界は明確ではありません。
命令型言語とは、計算順序を強く意識させたプログラミング言語です。コンピュータのハードウェア実装が命令型であることから、多くの言語は命令型言語に属します。Rust や Scala は関数型言語と命令形言語の要素を併せ持っており境界は曖昧ですが、Scala は繰り返し処理を関数の再帰で書かせる傾向が強く多くの場合において関数型言語に分類される一方で、Rust は末尾再帰最適化を実装していないことや繰り返し文で continue や break が標準的に使われているため、この記事では命令形言語に分類しています。
Rust には、並列化を行うときに必ず気をつけなければならないデータ競合を、コンパイル時に防ぐ仕組みがあります。機械学習や科学計算の補助言語として Python がよく使われますが、1 コアあたりの計算速度が頭打ちになりつつある今、実行速度の最適化だけでなく並列化も難しいスクリプト言語は計算速度は上がりにくく、今後並列化できる言語が普及すると考えられ、並列化の問題が起きにくい Rust はより注目されると考えられます。
スクリプト言語の多くはスレッドセーフを実現するため、スレッドセーフではない命令の実行やデータ競合による変数内容の破壊を防ぐ目的で同時に実行できる命令を 1 つに制限する必要があり(Python ではグローバルインタープリタロックと呼ばれます)、並列化が難しい状況にあります。
また近年、JavaScript へのトランスコンパイルできる言語が乱立するなど、Web 業界においても静的型付け型言語が注目されつつあります。Rust は Web ブラウザ上で高速に計算するための WebAssembly と呼ばれる実行形式への対応を始めており、今後選択肢のひとつとして選ばれることが増えるかもしれません。
トランスコンパイルとは、機械語あるいはそれに準ずる言語にではなく、高級言語に変換することを指します。現代において Web ブラウザで解釈できる言語は JavaScript のみですが、JavaScript には多くの問題があるため、それを補う目的で CoffeeScript や JSX、TypeScript をはじめとした JavaScript に変換される言語が増加傾向にあります。
WebAssembly とは、ブラウザ上でコンパイル言語に匹敵する計算速度を実現するために作られた中間言語です。その目的ゆえ開発には C++ が用いられることが多いですが、Rust は実行速度をほとんど失わず C++ の弱点を補えると期待されています。

メモリ管理の歴史と Rust の解決策

スマートポインタが生まれるまで

C 言語では動的メモリの確保・解放をプログラマの責任で明示的に行わなければなりませんでした。しかし、確保したメモリの解放を人間が忘れず完璧に行うのは難しく、1990 年以降、ランタイムがメモリ管理をするプログラミング言語が主流になりました。ところが、ランタイムによるメモリ管理は「メモリ解放のためにプログラムが一時停止する」「ランタイムが複雑化する」等の問題があり、メモリ効率や実行効率を考えるとどうしても C 言語や C++ で手動でメモリの確保・解放を行うプログラムを書かざるをえない状況を無くせずにいました。
ランタイムによるメモリ管理の実装方法は、プログラムを停止させる方法とさせない方法がありますが、プログラムを停止させる方法は実装が簡単なため使われることが多いです。リアルタイム性を重視した手法も研究されていますが、実装がより複雑になり全体での実行効率が低下する傾向があるという問題があります。
その状況を解決するため、デストラクタの呼ばれるタイミングが明確な C++ は、コンストラクタでメモリを確保しデストラクタでメモリの解放を行うスマートポインタを導入しました。スマートポインタである unique_ptr は、メモリを生ポインターの状態で確保して解放するのと比較してメモリのオーバーヘッドが全くなく、加えて Web サービス開発等の通常の業務プログラミングにおいては多くの状況で unique_ptr で事足りるため、現在では明示的にメモリ解放を行わなければならない状況はほとんどなくなりました。これにより多くの状況で、メモリのオーバーヘッドなしに解放を忘れることなく、メモリ管理ができることがわかりました。
デストラクタとは、オブジェクトが破棄されるときに呼び出されるメソッドです。Java では finalize メソッドに相当するものですが、Java ではオブジェクトがすぐに破棄される保証がないため使用は推奨されていません。一方で C++ は、デストラクタが呼ばれるタイミング(ローカル変数であればスコープを抜けるとき)が決まっているので、スマートポインタへの活用が可能となりました。
unique_ptr は生ポインタのコンテナです。new で確保した生ポインタに対して必ずちょうど 1 回 delete を呼ぶ必要がありますが、分岐が起きたときにすべての経路で忘れず delete を呼ぶのは難しいという問題がありました。
void UserFunction(int value) {
  string* s = new string();
  if (value == 0) {
    // return する前には必ず delete を呼ぶ必要があります
    delete s;
    return;
  }
  delete s;
}
そこで、ローカル変数のデストラクタがスコープを抜けるときに必ず呼ばれることを利用し、delete の呼び忘れを防ぐことができるようになりました。
void UserFunction(int value) {
  std::unique_ptr<string> s(new string());
  if (value == 0) {
    // return すると unique_ptr のデストラクタが呼ばれ、
    // unique_ptr がデストラクタ内で delete を呼び出します。
    return;
  }
}
Google C++ Style Guide に "Do not design your code to use shared ownership without a very good reason." (よほどの理由がない限り、所有権を共有するような設計にしない)と書かれている通り、うまく設計すれば unique_ptr で事足りることがほとんどです。

unique_ptr の問題点

unique_ptr の登場により確保したメモリの解放忘れはなくなりましたが、unique_ptr に確保されているポインタの所有権(解放する権利)を他の unique_ptr に移譲すると元の unique_ptr は初期化されてしまい、その後に元の unique_ptr にアクセスを行うとメモリアクセス違反が起きる(実行例)という問題があります。
// C++
int main() {
  // std::unique_ptr<string> hoge(new string("hoge")); と同義です。
  // 型名を 2 度書くのを避け、コード上から new をなくすことができます。
  auto hoge = std::make_unique<std::string>("hoge");
  auto piyo = std::make_unique<std::string>("piyo");
  println(*hoge);  // => hoge
  println(*piyo);  // => piyo
  hoge = std::move(piyo);  // 変数 piyo にあるポインタの所有権は変数 hoge に移ります。
  println(*hoge);  // => piyo
  println(*piyo);  // 変数 piyo は初期化されてしまっているため、メモリアクセス違反が起きます。
}

移譲(ムーブセマンティクス)

C++11 ではムーブセマンティクスを導入し、文字列型を含めた様々な型で値の移譲(ムーブ)が可能になりました。しかし、移譲は明示的に書く必要があり、書き忘れるとコピーが発生してしまうという問題があります。
// C++
int main() {
  std::string hoge = "hoge";
  std::string piyo = "piyo";
  println(hoge);  // => hoge
  println(piyo);  // => piyo
  // hoge = piyo; と書くと文字列のコピーが発生します!
  hoge = std::move(piyo);  // 変数 piyo にある文字列の所有権は変数 hoge に移ります。
  println(hoge);  // => piyo
  println(piyo);  // => 空文字列
}

Rust による解決

Rust は代入演算子 "=" を、コピー可能と明示的に指定されていない限り、移譲で行われるようにしました。また、移譲された変数へのアクセスをコンパイル時に禁止するようにしました。
fn main() {
  let mut hoge = "hoge".to_string();  // 文字列 "hoge" を変数 hoge に入れる
  let piyo = "piyo".to_string();  // 文字列 "piyo" を変数 piyo に入れる
  println!("{}", hoge);  // => hoge
  println!("{}", piyo);  // => piyo
  hoge = piyo;  // 変数 piyo にある文字列の所有権は変数 hoge に移譲される
  println!("{}", hoge);  // => piyo
  // println!("{}", piyo);  // コメントをはずすとコンパイルエラー
}
上のコード(実行例)の 8 行目のコメントをはずすと、6 行目で移譲された変数 piyo を使うことになりますが、Rust のコンパイラはその問題を見つけ以下のようなコンパイルエラー(実行例)を出力します(この記事では簡単のためエラーメッセージを日本語にしていますが、原因を明確に表示してくれる上に対処法を提案してくれることもあり大変有用です)。これにより誤って値をコピーしてしまったり、移譲済みの値へのアクセスしてしまう問題がコンパイル時に防がれます。
エラー[E0382]: 移譲された値の使用: `piyo`
 --> test.rs:8:18
  |
6 |   hoge = piyo;
  |          ---- ここで値が移譲されています
7 |   println!("{}", hoge);
8 |   println!("{}", piyo);
  |                  ^^^^ 移譲された値がここで使われています
  |
  = 注釈: 移譲は `piyo` が `Copy` トレイトを実装していない型 `std::string::String` であるため起きました

変数の寿命

C++ における参照・ポインタ渡しの問題

C++ では変数の値を参照やポインタで渡すことにより、値のコピー処理を避けることができます。しかし、実体の寿命があることを保証しないため、すでに解放された変数へのアクセスを行うことが可能であり、バグを生みやすいという問題があります。
// C++
#include <stdio.h>
#include <string>

int main() {
  std::string* a;
  {
    std::string b = "hoge";
    a = &b;
    // 変数 b の寿命(スコープ)はここまでであり、ここで文字列 hoge は解放されます。
  }
  printf("%s\n", a->c_str());  // 不正なアクセス!
}

Rust による解決

Rust は借用(参照を渡し、寿命を保証する)という概念を導入し、すでに解放された(スコープを出た)変数へのアクセスが起き得ないようにしました。
fn main() {
  let a;
  {
    let b = "hoge".to_string();
    a = &b;  // コンパイルエラー!
  }
  println!("{}", a);
}
上のコード(実行例)では 5 行目で a に b への参照を代入しています。しかし、a の寿命は 2 行目から 8 行目までであるにもかかわらず、b の寿命は 4 行目から 6 行目であり、代入先よりも短い寿命しかありません。つまり a に参照を代入できてしまうと b が解放されたあとも参照される可能性が出るため Rust はこれを検知し以下のようなコンパイルエラーを出力します。これにより参照でのアクセスが必ず成功することが保証できるようになりました。
エラー: `b` の寿命が不十分です
 --> test.rs:5:10
  |
5 |     a = &b;
  |          ^ ここの寿命が不十分です
6 |   }
  |   - 借用された値はここまでしか生存していません
7 |   println!("{}", a);
8 | }
  | - 借用された値はここまで生存できなければなりません

借用検査(書き換え可能性と参照)

読み込みのために借用している変数の値が書き換えられる可能性を想定するのは、異なるスレッド間であればデータ競合が起きスレッドセーフではなくなるという問題や、シングルスレッド下でもイテレータの正当性を保証するのが難しくなるなどの様々な問題があります。そこで Rust は借用を「1 つ以上の不変(書き換えのできない)参照」あるいは「1 つの可変(書き換えのできる)参照」に制限しています。
fn f1(a: &String, b: &String) {}
fn f2(a: &mut String, b: &String) {}
fn f3(a: &mut String) {}

fn main() {
  let mut a = "hoge".to_string();  // 可変変数の宣言
  f1(&a, &a);  // 不変参照のみ借用しているので OK
  f2(&mut a, &a);  // コンパイルエラー!
  f3(&mut a);  // 可変参照を 1 つだけ借用しているので OK
  println!("{}", a);
}
上のコード(実行例)では 7 行目と 9 行目はコンパイルできますが、8 行目はコンパイルできません。これは不変参照に加えて可変参照を借用しているため以下のようなコンパイルエラーを出力します。これにより借用した変数の読み書きによってデータ競合が起きないことを保証できるようになりました。
エラー[E0502]: `a` はすでに可変借用をされているため、借用できません
 --> test.rs:8:15
  |
8 |   f2(&mut a, &a);
  |           -   ^- 可変借用はここまでです
  |           |   |
  |           |   不変借用がここで起きています
  |           可変借用がここで起きています

返り値の寿命と束縛

Rust では返り値として参照を返すことができます。
// orig に prefix が含まれていればそれを削除した orig の一部を返し、
// orig に prefix が含まれていなければ orig を返す関数です。
// 'a は寿命を表し、orig と返り値は同じ寿命であることを表します。
fn maybe_remove_prefix<'a>(orig: &'a [i32], prefix: &[i32]) -> &'a [i32] {
  if orig.starts_with(prefix) {
    return orig.split_at(prefix.len()).1;
  } else {
    return orig;
  }
}

fn main() {
  let mut a = [1, 2, 3];
  let mut suffix;
  {
    let b = [1, 2];
    suffix = maybe_remove_prefix(&a, &b);
    // suffix = maybe_remove_prefix(&b, &a);
    // a = [4, 5, 6];
  }
  println!("{:?}", suffix);  // => [3]
}
上のコード(実行例)では maybe_remove_prefix の返り値の参照の寿命は、引数に 2 つの参照がありどちらの参照から生成されるのかわからないため、テンプレート引数 'a を用いて、orig と同じと定義されています。suffix には寿命がより短い参照を代入することはできないため、18 行目のコメントをはずすと、以下のようなコンパイルエラーが出力されます。これにより返り値の参照の寿命の有効性が保証されます。
エラー: `b` の寿命が不十分です
  --> test.rs:18:35
   |
18 |     suffix = maybe_remove_prefix(&b, &a);
   |                                   ^ ここの寿命が不十分です
19 |     // a = [4, 5, 6];
20 |   }
   |   - 借用された値はここまでしか生存していません
21 |   println!("{}", suffix);  // => piyo
22 | }
   | - 借用された値はここまで生存できなければなりません
返り値として参照が用いられた場合、返り値が使われている間は、実体は返り値に束縛され変更することはできなくなります。19 行目のコメントをはずすと、suffix に借用されている変数 a を書き換えることになるため、以下のようなコンパイルエラーが出力されます。これにより返り値の参照の実体が書き換えられず有効であることが保証されます。
エラー[E0506]: 借用されているため `a` に代入することはできません
  --> test.rs:19:5
   |
17 |     suffix = maybe_remove_prefix(&a, &b);
   |                                   - `a` の借用がここで起きています
18 |     // suffix = maybe_remove_prefix(&b, &a);
19 |     a = [4, 5, 6];
   |     ^^^^^^^^^^^^^^^^^^^^^ 借用されている `a` への代入がここで起きています

Rust の言語機能

値を持つ列挙型

現代の多くの言語において列挙型(enum)は整数が割り当てられた各識別子のうち 1 つを選択できる型ですが、Rust の列挙型はいくつかの値(構造体やタプルも含む)のうち 1 つを選択できる型であり、タグ付き共用体とも呼ばれるものです。
enum Option<T> {
  None,
  Some(T),
}
例えば Rust で頻繁に用いられる列挙型 Option<T> は上のコードのように定義されており、None あるいは Some(T) をとります。None は値を持ちませんが、Some(T) は T 型の値を持ちます。例えば連想配列から値を取り出すときに用いられ、「値が存在するかしないか」と「存在したときの値」を 1 つの値として表現ができます。

パターンマッチング

Rust には、多くの言語に存在する switch 構文の代わりに、match 構文があります。match 構文は、switch 構文と比べ表現力が非常に高く、列挙型の持つ値による条件分岐も可能であり柔軟です(実行例)。
fn main() {
  let string_value = "1234";
  // parse<i64>() は列挙型 Result<i64, ParseIntError> を返します。
  // Result<T, E> は Ok(T) および Err(E) からなる列挙型です。
  match string_value.trim().parse::<i64>() {
    Ok(x) if x < 0 => println!("負の整数: {}", x),
    Ok(x) => println!("非負整数: {}", x),
    Err(e) => println!("整数への変換に失敗しました: {}", e),
  }
}
コンパイラはパターンが十分であるかの検査します。例えば上のコードの 7 行目を削除すると以下のようなコンパイルエラー(実行例)が出力されます。
エラー[E0004]: 不十分なパターンです: `Ok(_)` が含まれていません
 --> <anon>:5:9
  |
5 |   match string_value.trim().parse::<i64>() {
  |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ パターン `Ok(_)` が含まれていません

総称型(ジェネリクス)

Rust には型をコンパイル時パラメータとして与えられる総称型(ジェネリクス)があります(実行例)。その型に対して何らかの処理を行う場合は、その処理が行えるトレイト(その型が実装しなければならない性質)を指定し、型を予め制限する必要があります。
総称型(ジェネリクス)とは、Java における List<T> の <T> 部分を指し、型をコンパイル時パラメータとして与えられる機能を表します。C++ のテンプレートは、同様に型をコンパイル時パラメータとして与えますが、型に対する制限をコンパイル時にダックタイピングで(呼び出されるメソッドが存在するかしないかをもとに)行います。
use std::ops::AddAssign;
 
// 型 T は Clone トレイト(T.clone())と AddAssign<T> トレイト(S+=T)を
// 実装している必要があります。
fn sum<T: Clone + AddAssign<T>>(v: &Vec<T>, init: T) -> T {
  let mut result = init;
  for x in v {
    result += x.clone();
  }
  result
}

fn main() {
  let v = vec![10, 20, 30, 40];
  // sum<i32, i32> を呼び出します。
  println!("{}", sum(&v, 0));  // => 100

  let v = vec![0.01, 0.02, 0.03, 0.04];
  // sum<f64, f64> を呼び出します。
  println!("{}", sum(&v, 0.0));  // => 0.1
}

ユーザ定義型に対する演算子の定義

Rust は、列挙型および構造体に対して演算子(+ や * など)の定義ができます(実行例)。
use std::ops::Add;

#[derive(Debug)]
struct Point(i32, i32);

impl Add for Point {
  type Output = Point;

  fn add(self, rhs: Point) -> Point {
    Point(self.0 + rhs.0, self.1 + rhs.1)
  }
}

fn main() {
  let result = Point(1, 2) + Point(3, 4);
  println!("{:?}", result);  // => Point(4, 6)
}

参照カウンタと Mutex

Rust には、オブジェクトを複数の所有者から参照可能にする、参照カウンタ(Rc および Arc)が用意されています。Arc はスレッド安全な参照カウンタであり、Mutex と組み合わせることにより複数のスレッドからの書き込みを実現します(実行例)。多くの言語では Mutex は守りたい領域とは別に用意されることが多いですが、Rust ではコンテナとして表現することにより守るべき値を明確にしています。
use std::thread;
use std::sync::{Arc, Mutex};

fn main() {
  let data = Arc::new(Mutex::new(0));
  let mut threads = vec![];
  for thread_id in 0..3 {
    println!("スレッド (ID: {}) を開始します", thread_id);
    let data = data.clone();
    threads.push(thread::spawn(move || {
      for _ in 0..100000 {
        *data.lock().unwrap() += 1;
      }
      println!("スレッド (ID: {}) が終了しました", thread_id);
    }));
  }

  for t in threads {
    t.join().unwrap();
  }

  println!("合計: {}", *data.lock().unwrap());
}

変数の初期化

Rust は変数の初期化を強制しますが、必ずしも宣言時に行う必要はありません(実行例)。
fn user_function(flag: bool) {
  // result は宣言時に初期化されていませんが、どの経路においても 1 度だけ代入され、
  // 書き換えられない変数として解釈できるのでコンパイルエラーとはなりません。
  let result : String;
  if flag {
    result = "hoge".into();
  } else {
    result = "piyo".into();
  }
  println!("{}", result);
}
上のコードの 8 行目を消すと、変数 result が初期化される保証ができなくなるため、以下のようなコンパイルエラーが出力されます。
エラー[E0381]: 未初期化の可能性がある変数が使われています: `result`
  --> <anon>:10:18
   |
10 |   println!("{}", result);
   |                  ^^^^^^ 未初期化の可能性がある変数 `result` が使われています

強力な型推論

Rust には強力な型推論があり、宣言時の代入だけでなく、途中の式で型が決まる状況であれば、型を明示する必要はありません(実行例)。
fn user_function(flag: bool) {
  // result の型はここでは宣言されていませんが、最終的に推論されます。
  // result は初期化されていませんが、どの経路においても 1 度だけ代入され、
  // 書き換えられない変数として解釈できるのでコンパイルエラーとはなりません。
  let result;
  if flag {
    // to_string() の返り値は String 型であり、
    // result の型は String 型に推論されます。
    result = "hoge".to_string();
  } else {
    // into() の返り値は result の型に依存するので、推論には用いられません。
    // result の型が String 型に推論されるので、
    // &str 型から String 型への型変換が into によって呼び出されます。
    result = "piyo".into();
  }
  println!("{}", result);
}

変数名の再利用

Rust では let を使い変数の定義ができますが、let を再度用いれば異なる型であっても同じ変数名で再定義ができ、可変な変数を使う状況を減らすことができます(実行例)。
fn main() {
  // data は不変な &str 型
  let data = "foo,bar";
  // data は不変な Vec<&str> 型
  let data = data.split(",").collect::<Vec<_>>();
  // data は可変な String 型
  let mut data = data.join(" ");
  data += " baz";
  println!("{}", data);  // => foo bar baz
}

エラー処理

Rust ではエラーを返す関数の返り値は Result<T, E> を使うのが一般的です。Go 言語のようにその都度エラーを if 文で処理するとコードが冗長になりますが、Rust では後置演算子 ? を利用してエラーを返すことができ、コードをシンプルにできます(実行例)。
use std::fs::OpenOptions;
use std::io::Write;

// Result 型として返す値がない場合、空のタプル型を与えることができます。
fn write_foo() -> Result<(), std::io::Error> {
  // file オブジェクトは破棄されるときに自動的にファイルを閉じるので、
  // 閉じ忘れることがありません。
  // open は Result<File, std::io::Error> を返しますが、
  // 後置演算子 ? が File を返すように書き換えます(エラー時は return します)。
  OpenOptions::new().write(true).create(true).open("/tmp/foo")?.write(b"foo");
  Ok(())
}

fn user_function() -> Result<(), String> {
  // エラー型を変える場合は map_err メソッドで変換できます。
  write_foo().map_err(|err| format!("Failed to write foo: {}", err))?;
  Ok(())
}

衛生的なマクロ

Rust は他の言語機能で実現できないことを補うためマクロ機能を用意しています。総称型とは異なり引数の型を制限しないため関数だけではできない柔軟な表現が可能です。println! や vec! を始めとした多くのビルトインマクロも存在します。
macro_rules! five_times {
  ($x:expr) => (5 * $x);
}
 
fn main() {
  println!("{}", five_times!(2 + 3));  // => 25
}
C 言語で同様の処理を書くと 5 * 2 + 3 と展開されてしまい 13 と表示されますが(実行例)、Rust では計算式の順序を破壊することなく 5 * (2 + 3) と展開するため上のコードでは 25 と表示されます(実行例)。
また、Rust 1.15(2017年2月2日リリース予定)からは proc_macro が追加され、Rust のコードを入力として受け取り置き換えることができるようになり、構造体のフィールド名を使ったシリアライズ・デシリアライズするような機能がコンパイラを直接書き換えずできるようになるなど、さらに自由度が高まります。

C 言語との連携

Rust はシステムソフトウェアも書けるプログラミング言語を目指しており、メモリ管理を明示的に行っており、ガベージコレクションによるメモリアドレスの変更等の問題もないため、C 言語との連携が明解で容易です。

終わりに

Rust の学習のため、Rust で Lisp インタープリタを書いてみたところ、Rust で変数の参照を扱うときには寿命や可変性の制限があり、雑な設計で書くと可変性が不足し、感覚的には従来の命令形言語と比較してこれらは大きな文脈で影響を及ぼすため、色々なところを書き換える必要が発生したりと若干戸惑う状況もありました。しかし、これらの制限にそって修正を行うと全体の設計がスッキリしたことも確かで、誰に書かせても比較的読みやすいコードになるかもしれません。また、オーバーヘッドのないメモリ安全性は非常に魅力的であり、さらには強力な型推論を始めとした現代的な言語機能も備えています。現在、Mozilla はブラウザ開発に用いていますが、WebAssembly 等でも使えるようになりつつありますし、OS や組み込み等ではメモリの効率性と安全性で大きな優位性があり、今後様々な分野で使われるのは間違いないと信じています。

謝辞

この記事の草稿を @tanakh さんと、@Linda_pp さん、 @ogiekako さんにチェックして頂きました。ありがとうございました。

参考文献