この記事はWanoグループ Advent Calendar 2017の9日目の記事です。
どうも、社内でRustイイヨーと唱えている人です。
今日は具体的にRustのどういった言語機能が便利なのかをご紹介したいと思います。
目次
- 変数がデフォルトでimmutable
- mutableな変数の取り扱いが簡単
- Trait
- 直和型とパターンマッチ
- 例外ではなく戻り値でエラーを返す
- ライフタイム管理
- 使用する処理系のバージョンを切り替えるツールが公式に出ている
- WebAssemblyを吐ける
- JetBrains社公式サポートのIntelliJ IDEAプラグインがある
変数がデフォルトでimmutable
immutableとは不変という意味で変数の値を変更できない事を意味します。
変数がimmutableな事には次のようなメリットがあります。
- 値が変わらないことを前提にした最適化が行われるようになります。
- 関数の引数が不変なことが保証される事によってコードを追わなくても絶対に変更されない事が明確になります。
- コンパイル時に不変性を破っているコードを検出できます。
- 変数の状態遷移を意識しなくて良くなるのでコードを読むのが少し楽になります。
不変性を破っている例:
fn main() {
let a = 1;
iikannji_ni_shori_suru_kansuu(&a);
}
fn iikannji_ni_shori_suru_kansuu(a: &i32) {
*a = 2; // コンパイルエラー
println!("変更しちゃいました☆: {}", *a);
}
実行結果:
error[E0594]: cannot assign to immutable borrowed content `*a`
--> src/main.rs:7:3
|
6 | fn iikannji_ni_shori_suru_kansuu(a: &i32) {
| ---- use `&mut i32` here to make mutable
7 | *a = 2; // コンパイルエラー
| ^^^^^^ cannot borrow as mutable
error: aborting due to previous error
error: Could not compile `playground`.
To learn more, run the command again with --verbose.
Rust Playground(Runボタンでウェブ上でコンパイルと実行がされます)
mutableな変数の取り扱いが簡単
immutableな変数だけで全てを書こうとするとどうしてもツラい部分が出てきたり、逆に可読性が落ちる場面があります。
このような時はmutableな変数を定義することで簡単にコードを書くことが出来ます。
mutableな変数はmutというキーワードを付けるだけで定義できます。
例:
fn main() {
let mut a = 1;
println!("a is {}", a);
a = 2;
println!("a is {}", a);
update(&mut a);
println!("a is {}", a);
}
fn update(x: &mut i32) {
*x = -1;
}
実行結果:
a is 1
a is 2
a is -1
Rust Playground(Runボタンでウェブ上でコンパイルと実行がされます)
mutableな変数の『値の変更』や『mutableな参照を作成する』事は『他に自身への参照が既に存在している時』には出来ないようになっています。
これにより意図しないタイミングでの値の書き換えやデータレースを防ぐことが出来るようになっています。
ただしmutキーワードだけでは上手く書けないケースがあります。
そのような時にはCell型やRc型などを使って記述していくことになります。
特に構造体の中の一部だけを書き換え可能にしたい時にこれらを使うことになります。
Trait
実装を持てるインターフェースの様なものです。
型定義の外部でTraitの実装を定義することが出来る為、自作Traitの実装を既存の型に対して定義する事が出来ます。
またジェネリクス及び制約と組み合わせる事により具体的な型を書かずに実装を用意する事も出来ます。
trait Happy {
fn happy_level(&self) -> i32;
fn happy(&self) -> &'static str {
let hl = self.happy_level();
if hl >= 10 {
return "happy!";
} else if hl <= -10 {
return "unhappy";
} else {
return "soso";
}
}
}
impl Happy for i32 {
fn happy_level(&self) -> i32 {
return *self;
}
}
trait DeadLine {
fn dead_line(&self) -> &'static str;
}
impl DeadLine for i32 {
fn dead_line(&self) -> &'static str {
if *self >= 10 {
return "締め切りまで、まだ時間がある";
} else {
return "締め切り過ぎた…";
}
}
}
trait DisplayStatus {
fn display_status(&self) -> String;
}
impl<T: Happy + DeadLine> DisplayStatus for T {
fn display_status(&self) -> String {
return format!("{} ({})", self.dead_line(), self.happy());
}
}
fn main() {
let a = 10;
println!("{}", a.happy());
println!("{}", a.dead_line());
println!("{}", a.display_status());
let b = -1;
println!("{}", b.happy());
println!("{}", b.dead_line());
println!("{}", b.display_status());
}
実行結果:
happy!
締め切りまで、まだ時間がある
締め切りまで、まだ時間がある (happy!)
soso
締め切り過ぎた…
締め切り過ぎた… (soso)
Rust Playground(Runボタンでウェブ上でコンパイルと実行がされます)
直和型とパターンマッチ
直和型は代数的データ型(algebraic type)、判別共用体とも呼ばれます。
共用体(union)と列挙型(enum)と構造体(struct)が合わさったような機能です。
Rustではenumキーワードを使って定義します。
この直和型はパターンマッチと組み合わせて使う事によりプログラムを直感的に書くことが出来ます。
例えばenumを用いることで簡易的に動画の再生状態を操作するコードを次の様に書けます。
enum Command {
Play,
Stop,
Seek(u32),
}
fn main() {
control_player(Command::Play);
control_player(Command::Stop);
control_player(Command::Seek(63));
}
fn control_player(cmd: Command) {
match cmd {
Command::Play => println!("再生を開始します"),
Command::Stop => println!("再生を停止します"),
Command::Seek(s) => println!("{}秒の位置に移動します", s),
}
}
実行結果:
再生を開始します
再生を停止します
63秒の位置に移動します
Rust Playground(Runボタンでウェブ上でコンパイルと実行がされます)
match
による分岐部分はコンパイラによってケースに不足が無いかチェックされており、不足がある場合はコンパイルエラーとして扱われます。
その為、分岐が1つも実行されない事によるバグが発生しないようになっています。
上記のサンプルコードにコマンドを1つ足してmatch
はそのままの例:
enum Command {
Play,
Stop,
Seek(u32),
Reverse,
}
fn main() {
control_player(Command::Play);
control_player(Command::Stop);
control_player(Command::Seek(63));
}
fn control_player(cmd: Command) {
match cmd {
Command::Play => println!("再生を開始します"),
Command::Stop => println!("再生を停止します"),
Command::Seek(s) => println!("{}秒の位置に移動します", s),
}
}
コンパイル結果:
error[E0004]: non-exhaustive patterns: `Reverse` not covered
--> src/main.rs:15:11
|
15 | match cmd {
| ^^^ pattern `Reverse` not covered
error: aborting due to previous error
error: Could not compile `playground`.
To learn more, run the command again with --verbose.
Rust Playground(Runボタンでウェブ上でコンパイルと実行がされます)
例外ではなく戻り値でエラーを返す
エラーはpanicを除いて全て戻り値で返します。
この時、成功なのか失敗なのかはResult
型によって表します。Result
型はenumです。
Result<成功時に戻す値の型, エラー時に戻す値の型>
と記述するのでどんなエラーが発生しうるのか基本的には戻り値の型を見れば一目瞭然となっています。
Rustではエラー型はenumで表現し、どのようなエラーが起こったのか呼び出し元でmatchを用いたパターンマッチで判別出来るようにするのが主流の方法です。
enumとmatchを用いるので後でコードに修正が入っても過不足が発生したらコンパイラが教えてくれるので安心です。
また、Result
を返す関数の呼び出しの最後に?
キーワードを書くことで『エラーだったらreturnするコード』をコンパイラが自動で生成してくれるようになります。
例:
#[macro_use]
extern crate quick_error;
use std::io;
quick_error! {
#[derive(Debug)]
pub enum ReadNumberError {
Io(err: io::Error) {
from()
}
Parse(err: ::std::num::ParseIntError) {
from()
}
}
}
fn main() {
match read_number() {
Ok(n) => println!("Input Number: {}", n),
Err(ReadNumberError::Io(err)) => println!("IO Error: {:?}", err),
Err(ReadNumberError::Parse(err)) => println!("Parse Error: {:?}", err),
}
}
fn read_number() -> Result<i32, ReadNumberError> {
let mut buf = String::new();
io::stdin().read_line(&mut buf)?;
return Ok(buf.parse()?);
}
実行結果(空行が入力された場合):
Parse Error: ParseIntError { kind: Empty }
Rust Playground(Runボタンでウェブ上でコンパイルと実行がされます)
ライフタイム管理
Rustは変数のライフタイムを管理することで絶対安全な範囲でのみ参照を利用することが出来るようになっています。
またジェネリクスのパラメータの1つとしてライフタイムパラメータというものがあり、これによって引数や戻り値がどのようなライフタイムを持った変数への参照を保持しているのかプログラマが明示することも出来ます。
この機能があることにより、GC無しでも安全に参照を扱うことが出来ます。
ライフタイム違反によってコンパイルエラーになる例:
#[derive(Debug)]
struct Piyo {
label: String,
value: i32,
}
fn main() {
let mut piyo_container = vec![];
for _ in 1..10 {
generate_and_register_piyo(&mut piyo_container);
}
for i in &piyo_container {
println!("{}", i);
}
}
fn generate_and_register_piyo(container: &mut Vec<&Piyo>) {
let p = Piyo {
label: "たこ焼き".to_string(), // どこからか入力されたと仮定
value: 1, // どこからか入力されたと仮定
};
container.push(&p);
}
コンパイル結果:
error[E0597]: `p` does not live long enough
--> src/main.rs:24:21
|
24 | container.push(&p);
| ^ does not live long enough
25 | }
| - borrowed value only lives until here
|
note: borrowed value must be valid for the anonymous lifetime #2 defined on the function body at 18:1...
--> src/main.rs:18:1
|
18 | / fn generate_and_register_piyo(container: &mut Vec<&Piyo>) {
19 | | let p = Piyo {
20 | | label: "たこ焼き".to_string(), // どこからか入力されたと仮定
21 | | value: 1, // どこからか入力されたと仮定
... |
24 | | container.push(&p);
25 | | }
| |_^
error: aborting due to previous error
error: Could not compile `playground`.
To learn more, run the command again with --verbose.
Rust Playground(Runボタンでウェブ上でコンパイルと実行がされます)
関数がファーストクラス
関数・ラムダ式を変数に格納して取り扱うことが出来ます。
現代的なプログラミング言語にはもはや欠かせない機能です。
使用する処理系のバージョンを切り替えるツールが公式に出ている
rustupというgolangでいうgvm、perlでいうplenv、rubyでいうrbenvに相当するツールが公式にリリースされています。
Windows/Mac/Linuxで同じツールを利用可能です。
処理系のバージョンの切り替えだけではなく各プラットフォーム別の処理系のインストールや切り替えもこのrustupから行うことが出来ます。
WebAssemblyを吐ける
つい最近emscriptenを使わずにRust単体でWebAssemblyを吐けるようになりました。
これについては後日取り扱う予定です。
JetBrains社公式サポートのIntelliJ IDEAプラグインがある
現状ではこの先もOSS版としてCommunity向けに提供を続けるそうです。
https://blog.jetbrains.com/blog/2017/08/04/official-support-for-open-source-rust-plugin-for-intellij-idea-clion-and-other-jetbrains-ides/
ただし現状はデバッグがCLionでしか行えず、CLionはCMakeを前提に構築されている為に提供できている機能も非常に限定的とのことです。
デバッグだけはまだしばらくgdbやlldbなどを使っていく必要があります。