はじめに
個人的に興味を持っているプログラミング言語Rustのチュートリアルに一通り目を通した。Python/C++プログラマーが興味を持ったRustの機能を順不同で概説する。詳細は先の2つのリンク先を見てほしい。
cargoコマンド
Rust言語はcargoコマンドで管理される。プロジェクトの作成とビルド、テスト、実行が以下のコマンドで行われる。
1 2 3 4 |
$> cargo new example --- exampleプロジェクトの作成 $> cargo build --- exampleプロジェクトのビルド $> cargo test --- exampleプロジェクトのユニットテスト $> cargo run --- exampleプロジェクトの実行 |
他にもプロジェクト管理に便利な機能をたくさんサポートしている。他の言語であれば、統合開発環境(IDE)が担いそうな機能である。ところで、なぜ「cargo」という名前にしたのだろう?
実行時のエントリーポイント
実行時のエントリーポイントはいつもの関数である。
1 2 3 |
fn main() { ... } |
fn
という潔い省略(関数を表す識別子)が魅力的だ。
デフォルトでimmutable
Rustは型を自動推定する静的型付け言語である。変数はデフォルトでimmutable(定数)になる。
1 2 3 4 5 |
let x = 3; //x = 4; /* コンパイル時にエラー! */ /* 明示的にmutableにする必要がある。*/ let mut x = 3; x = 4; |
コード中に記載したように定数変数への代入はコンパイル時に検出されエラーとなる。変数を非定数とする場合は明示的にmut
を書く必要がある(これはmutableのことである)。デフォルトで定数とするのは、関数型言語(例えばHaskell)の参照透過性を真似たものだろう。昨今のモダンな言語は関数型言語からの影響が大変大きい。
パターンマッチがある
パターンマッチの例を以下に示す。
1 2 3 4 5 |
let x = 3; match x { 3 => println!("hello, world!"), /* 各種パターンの記述。アームと呼ばれる。*/ _ => (), } |
パターンマッチも関数型言語では当たり前の機能である。
参照による呼び出し
関数を以下のように定義すると
1 2 3 4 5 |
/* 型は引数の後ろに書く。*/ fn fun_0(v: &i32) { /* 末尾に!が付く関数はマクロである。*/ println!("{}", v); } |
引数は参照呼出しになる。
1 2 |
let x = 32; fun_0(&x); |
Rustの参照はC/C++のポインタに相当すると思えばよい。
1 2 3 |
/* 参照はポインタ */ let y = &x; let z: i32 = *y; /* C/C++のように参照外し */ |
上のように型を明示的に書くこともできる。
文字列型
文字列型に2種類ある。1つは書き換えできない固定長の文字列を表す&str型、もう1つは書き換え可能なString型である。前者は言語に組み込まれた文字列型、後者は標準ライブラリとして提供される型である。これらの文字列型の文字コードはUTF-8である。
1 2 |
let s = "hello"; /* 書き換え不可。*/ let t = String::from(s); |
所有権の移譲
代入を行うと所有権も移譲される。
1 2 3 |
let x = String::from("world"); let y = x; /* 所有権がyに移る。*/ //println!("{}", x); /* コンパイル時エラー */ |
所有権のないインスタンスに対する操作はコンパイル時に検出されエラーとなる。所有権の移譲はRust言語の一大特長である。また、immutableな変数の参照は何度でも取れるが、mutableな変数の参照は一度しか取れないルールがある。
1 2 3 4 5 6 7 8 9 |
let x = String::from("hello"); let y = &x; let z = &x; println!("{} {}", y, z); let mut x = String::from("hello"); let y = &mut x; let z = &mut x; //println!("{} {}", y, z); /* コンパイルエラーになる。*/ |
8行目でmutable変数に対する2回目の参照を取っているので、z
を使う9行目でエラーが検出される。もし、2つの参照に対する操作が許されると、同じメモリ領域に書き込む変数が2つ存在し、データ競合が起きるかもしれない。Rustはこれを未然に防ぐため上のルールを課している。
クラスはない
C++のクラスに相当する機能はない。構造体に相当するものはある。
1 2 3 4 |
struct Person { name: String, age: i32, } |
このとき以下のようにインスタンスを作る。
1 2 3 4 |
let p = Person { name: String::from("Kumada"), age: 23, }; |
メンバ関数(に相当するもの)は構造体とは別に定義する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
impl Person { /* Rustにはコンストラクタはない。*/ /* 必要なら、任意の名前の関数でインスタンスの初期化を行う関数を書くことになる。*/ fn new() -> Person { Person { name: String::from("kumada"), age: 20, } } fn get_name(&self) -> &String { &self.name /* Pythonライクなメンバ変数へのアクセス */ } fn get_age(&self) -> &i32 { &self.age /* セミコロンを書かないと自動的にreturnされる。*/ } fn print() { println!("hello"); } } |
関数の引数に&self
を書くとクラスメッソドになり、self
を通してメンバ変数にアクセスできる。関数の引数にself
を持たないものはstatic関数になる。Rustはコンストラクタを持たないので、もし必要であるなら上のnew
のような任意の名前で、インスタンスを返す静的関数を定義することになる。呼び出し方は以下の通り。
1 2 3 |
println!("{} {}", p.get_name(), p.get_age()); Person::print(); let q = Person::new(); |
1行目はインスタンスp
からのメンバ関数の呼び出し、2行目と3行目は静的関数の呼び出しである。また、関数定義内の一番最後の行の末尾にセミコロンを書かないとその行は自動的に返り値になる。
C++のstd::optionalに相当する型
std::optionalに相当する型がある。
1 2 3 4 5 6 7 8 9 10 11 |
let x = Some(5); /* 型はOption<i32> */ let y: Option<i32> = None; /* パターンマッチで値を取り出す。*/ match x { Some(v) => println!("{}", v), None => (), } match y { Some(v) => println!("{}", v), None => (), } |
自由度の高いenum
C++より自由度の高いenumがある。
1 2 3 4 5 6 7 8 9 10 11 |
enum Message { Quit, Move { x: i32, y: i32 }, /* struct */ Write(String), /* tuple struct */ ChangeColor(i32, i32, i32), /* tuple struct */ } let x = Message::Quit; let y = Message::ChangeColor(255, 255, 255); let z = Message::Move { x: 100, y: 200 }; /* x,y,zの型は全てMessage */ |
enumの要素として
Quit
)Move
)Write
、ChangeColor
)を取ることができる。ひとつ前で紹介したstd::optionalライクな型は実はこのenumを用いて実装されている。
1 2 3 4 |
enum Option<T> { None, Some(T), } |
テンプレートがある
以下はテンプレート関数の定義である。
1 2 3 |
fn print_typename<T>(_: T) { println!("{}", std::any::type_name::<T>()); } |
テンプレート引数T
は、コンパイル時に実際の型に置き換えられる。これは、C++と同じ仕組みのテンプレートである。関数に渡した引数が関数内部で使われないときは上のように_
と書くことができる。呼び出し側は以下の通り。
1 2 3 |
let v = vec![1, 2, 3]; /* コンパイル時に実際の型に置き換えられる真の意味のテンプレート */ print_typename(v); |
ひとつ前で紹介したOption
の定義にもテンプレートは使われている。
traitsがある
C++にtraitsに相当するものはない。Javaのinterfaceに相当する機能である。Rustではtraitsを用いて振る舞いを継承することができる。例えば、まず最初に以下のCry
traitsを定義する。
1 2 3 4 5 6 |
pub trait Cry { fn cry(&self) { /* デフォルトの振る舞い*/ println!("鳴く"); } } |
次に構造体を定義する。
1 2 3 |
pub struct Cat; pub struct Dog; pub struct Bird; |
これらの構造体に対してCry
traitsを定義する。
1 2 3 4 5 6 7 8 9 10 11 |
impl Cry for Cat { fn cry(&self) { println!("ニャーと鳴く"); } } impl Cry for Dog { fn cry(&self) { println!("ワンと鳴く"); } } |
構造体Bird
には意図的にcry
を定義しないことにする。このとき、以下の呼出しができる。
1 2 3 4 |
let dog = Dog {}; let cat = Cat {}; dog.cry(); cat.cry(); |
次に以下の関数を定義する。
1 2 3 4 5 6 |
fn talk<T>(t: &T) where T: Cry, /* Cry traitsを実装した型に制限 */ { t.cry(); } |
これはテンプレート関数であり、かつ、その型T
はCry
traitsを実装していなければならないこを要求する。これはC++のconceptに相当する仕組みである。このとき、以下のように書くことができる。
1 2 3 4 |
talk(&dog); talk(&cat); let bird = Bird {}; /* talk(&bird); エラーになる。*/ |
Bird
はCry
traitsを実装していないのでコンパイル時にエラーになる。
ポリモーフィズムができる
以下のコードでポリモーフィズムができる。
1 2 3 4 5 |
/* DogもCatもCry traitsを実装している。*/ let mut animals: Vec<Box<dyn Cry>> = vec![Box::new(Dog {}), Box::new(Cat {})]; for animal in animals { animal.cry(); } |
Box
は静的関数Box::new
の引数に与えたインスタンスをヒープ領域に確保し、C++のRAIIと同じ仕組みでメモリを管理するクラスである。また、コード中のdyn
は仮想関数テーブルを用いた動的ディスパッチを行う際に必要となる呪文である(と書いたがこのdyn
は省略可能である)。C++のstd::shared_ptrに相当するクラス(Rc
)もあるが今回は割愛する。
ライフタイムがある
以下の関数定義を考える。
1 2 3 4 5 6 7 |
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } |
'a
はライフタイムパラメータと呼ばれる。上の場合、引数の2つの変数と返り値の生存期間が同じであることをコンパイラに教えている。ライフタイムを明示することで、すでに寿命の尽きた変数を参照するバグをコンパイル時に検出できるようになる。以下のように使うことができる。
1 2 3 4 |
let x = String::from("hello"); let y = String::from("cat"); let z = longest(&x, &y); println!("{}", z); |
まとめ
最近チュートリアルを読んだRust言語を簡単に紹介した。巷ではC++を継ぐ言語と言われている。確かに、C++が嫉妬しそうな機能と構文がたくさんある。速度面でもC++と同等らしい。画像処理や機械学習のライブラリが充実してきたら実務で使いたい。私の場合、数値計算系のライブラリが充実しない限り最初の頃の熱意を持続できない。第2のD言語にならないことを祈る(笑)。