はじめに
この記事について
最近暇なので先月あたりから Rust を学んでいます。
Rust の特徴をざっくりいうと、静的型付け(コンパイル型)で、手続き型、オブジェクト指向、関数型をサポートするマルチパラダイムプログラミング言語です。
事前に見聞きしていた情報の中では、所有権とか借用とかライフタイムが難しいらしいというのだけ意識していて、それ以外はあまりこの言語特有の概念はないのかな、という印象でした。
本記事では Rust 書いたプログラムを実際に動かしてみた印象をつらつらと書いておきます。主に自分が興味のある型システム周りについて。
Rust の基本的な事柄はこの辺を読めばわかります。
The Rust Programming Language 日本語版 - The Rust Programming Language 日本語版
Introduction - Rust By Example 日本語版
1. 所有権と借用
/// str1 の前に str2 を連結した新しい文字列を返す fn str_prepend(str1: String, str2: String) -> String { let mut prestr = str2; prestr.push_str(&str1); prestr // 関数を抜けると str1, str2 のメモリが開放される(所有権を持っているので安全に開放できる) } fn main() { let name = String::from("John"); let greeting = str_prepend(name, "Hi, ".to_string()); // ここで name の所有権が移る println!("{}", greeting); // "Hi, John" println!("{}", name); // 所有権がないので参照できない(コンパイルエラー) }
上のコードをコンパイルすると以下のようなエラーになります。
error[E0382]: borrow of moved value: `name` --> src/main.rs:18:20 | 15 | let name = String::from("John"); | ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait 16 | let greeting = str_prepend(name, "Hi, ".to_string()); | ---- value moved here 17 | println!("{}", greeting); 18 | println!("{}", name); | ^^^^ value borrowed here after move |
所有権の仕組みはメモリ安全性のためにあるようで、「The Rust Programming Language」には以下の説明があります。
先ほど、変数がスコープを抜けたら、Rustは自動的にdrop関数を呼び出し、 その変数が使っていたヒープメモリを片付けると述べました。しかし、図4-2は、 両方のデータポインタが同じ場所を指していることを示しています。これは問題です: s2とs1がスコープを抜けたら、 両方とも同じメモリを解放しようとします。これは二重解放エラーとして知られ、以前触れたメモリ安全性上のバグの一つになります。 メモリを2回解放することは、memory corruption (訳注: メモリの崩壊。意図せぬメモリの書き換え) につながり、 セキュリティ上の脆弱性を生む可能性があります。
The Rust Programming Language 日本語版 https://doc.rust-jp.rs/book-ja/ch04-01-what-is-ownership.html
所有権を渡さずに関数呼び出し後にも name を参照できるようにするためには、引数に渡すときに &
をつけて渡すようにする(参照渡し)とコンパイルが通ります。
/// str1 の前に str2 を連結した新しい文字列を返す fn str_prepend(str1: &String, str2: String) -> String { let mut prestr = str2; prestr.push_str(&str1); prestr // 関数を抜けると str2 のメモリだけが開放される(所有権を持っているので安全に開放できる) } fn main() { let name = String::from("John"); let greeting = str_prepend(&name, "Hi, ".to_string()); // ここで name の所有権を貸す println!("{}", greeting); // "Hi, John" println!("{}", name); // 所有権が引き続きあるので参照できる(コンパイルエラーにならない) }
ミュータブルな変数は明示的に mut
キーワードで指定する必要があり、できるだけイミュータブルに宣言することで意図せぬ上書きを防ぐことができます。
PHP にも値渡し、参照渡しはありますが、文法上の区別はないのでコードを読んだだけではわかりません(もちろん言語仕様を理解している人であればわかりますが、見た目の違いがない、という意味です)。また、変数はデフォルトでミュータブルなので、意図せぬ上書きが起こりえます。
<?php class MyString { public function __construct(public string $content) {} } // $str1 は参照渡し、$str2 は値渡し function str_prepend(MyString $str1, string $str2): MyString { $new_str = $str1; // ここも参照 $new_str->content = $str2 . $str1->content; return $new_str; } $name = new MyString('John'); $greeting = str_prepend($name, 'Hi, '); var_dump($greeting, $name); // 両方とも Hi, John になってしまう /* object(MyString)#1 (1) { ["content"]=> string(8) "Hi, John" } object(MyString)#1 (1) { ["content"]=> string(8) "Hi, John" } */
上の例は本来であればメソッドにするでしょうから(Rust のコードに寄せたコードに無理やりしてるので)、間違いは起こらないとは思いますが、他のクラスのメソッドにオブジェクトを渡すシチュエーションでは起こりえます。
Rust は明示的に所有権がどこにあるのかを指定するのでわかりやすいですね。
2. 構造体と列挙型
struct は構造体を定義するためのキーワードで、Rust のユーザー定義型の中核をなすものです。
// domain/health_score.rs #[derive(Debug)] // println!("{:?}", ...) で出力できるように Debug トレイトを使う pub struct HealthScore { weight: f32, height: f32, }
メソッド方式(オブジェクトと関数を .
で繋いで呼び出す方式)の関数を定義するには impl
ブロックの中で関数定義する必要があります。
impl HealthScore { pub fn new(weight: f32, height: f32) -> Self { Self { weight, height, } } pub fn bmi(&self) -> f32 { let height_in_meter = self.height / 100.0; self.weight / (height_in_meter * height_in_meter) } } // main.rs mod domain; use crate::domain::health_score::HealthScore; fn main() { let score = HealthScore::new(70.0, 170.0); println!("score={:?}", score); // score=HealthScore { weight: 70.0, height: 170.0 } println!("BMI={}", score.bmi()); // BMI=24.221453 // 各フィールドは private なのでアクセスできない(コンパイルエラー) // println!("weight={:.1}, height={:.1}", score.weight, score.height); }
pub
のついた要素(型、構造体のフィールド、メソッド、関数、etc.)しかモジュールの外からアクセスできないので、構造体のフィールドに外からアクセスしたい場合は以下のようにする必要があります。
#[derive(Debug)] pub struct HealthScore { pub weight: f32, pub height: f32, }
derive
アトリビュートを使うと、既存のトレイトから振る舞いを追加することができます。たとえば、PartialEq トレイトを derive するとインスタンス同士で等価判定ができるようになります。
#[derive(PartialEq)] pub struct HealthScore { weight: f32, height: f32, } let score = HealthScore::new(70.0, 170.0); let next_score = HealthScore::new(71.0, 170.0); println!("score and next_score equals?: {}", score == next_score); // false
タプル構造体とかユニット様(Unit-like)構造体とか、ちょっと変わったバリエーションもあります。
// タプル構造体 struct BookId(u32); // 値はいくつでも指定できる let bookId = BookId(1); // bookId.0 で値を取り出せる println!("{}", bookId.0); // ユニット様構造体 struct Unit; // 中身がない impl Unit { fn do_something() {} } impl SomeTrait for Unit { fn do_something_else() {} } Unit.do_something();
列挙型に関しては、ちょっと面白くて、各列挙子に値を与えることができます。
たとえば以下のような、摂氏と華氏のどちらかで気温を持つケースで考えてみます。どちらも32ビット整数の値を持たせています。
pub enum Temperature { Celsius(i32), Fahrenheit(i32), }
列挙子の値を取り出すときは以下のようにします。
impl Temperature { pub fn to_string(&self) -> String { match self { Self::Celsius(v) => format!("{}°C", v), Self::Fahrenheit(v) => format!("{}°F", v), } }
いまのところ以下のように列挙型の値に対してメソッドスタイルによる値の取得はできないようです。
let temp_in_c = Temperature::Celsius(10); // println!("{}", temp_in_c.value);
列挙子によって値を持つものと持たないものがありうるので、まぁこれは当たり前ですね。
Rust には Option 型が標準パッケージ(core パッケージ)に入っていて、null 安全性が高められていますが、Option は enum で実装されています。
pub enum Option<T> { None, Some(T), } // client let v = Some(5); let num = v.unwrap_or(0); // または v.unwrap_or_default() // num = 5 let v: Option<usize> = None; let num = v.unwrap_or(0); // または v.unwrap_or_default() // num = 0
3. トレイト
Rust には継承がないので、多態を表現するには代わりにトレイトを使います。PHP の interface に近いですが、デフォルトの実装を持つことができる点が異なります。
またしてもあまりいい例が浮かばないですが、2つの時間を持つ構造体とと2つの二次元上の位置を持つ構造体を共通に扱って2要素間の差異を取得するトレイトを作ってみます。
// domain/span.rs use chrono::{DateTime, Utc}; pub struct TimeSpan { pub from: DateTime<Utc>, pub to: DateTime<Utc>, } pub struct Point2DSpan { pub from: usize, pub to: usize, } pub trait Span { fn diff(&self) -> usize; } impl Span for TimeSpan { fn diff(&self) -> usize { (self.to - self.from).num_hours() as usize } } impl Span for Point2DSpan { fn diff(&self) -> usize { self.to - self.from } }
diff(&self) -> usize
という共通の振る舞いを異なる構造体に適用して、どちらが与えられても同じシグネチャの関数が呼ばれるようにします。
use chrono::{Utc, Duration}; use crate::domain::span::{TimeSpan, Point2DSpan, Span}; fn main() { let spans: Vec<(&str, Box<dyn Span>)> = vec![ ("TimeSpan", Box::new(TimeSpan { from: Utc::now() - Duration::hours(2), to: Utc::now() })), ("Point2DSpan", Box::new(Point2DSpan { from: 0, to: 3 })), ]; for (name, span) in spans { println!("{}: {}", name, span.diff()); } /* TimeSpan: 2 Point2DSpan: 3 */ }
動的ディスパッチを実装するためには Box<dyn Trait>
の形で受け取る必要があって(Box はヒープに領域を確保して任意の型の値を格納する構造体です)、この辺がじゃっかんめんどくさいですが、明示的に動的であることがコードで表現されているので、メリットもあると思います(ちなみに、「動的」であっても指定されたトレイトを実装していないオブジェクトが渡された場合にはちゃんとコンパイルエラーになります)。
動的型付け言語と比べるとやはり、堅牢性と柔軟性のトレードオフになるので、記述量が増えるのは仕方ないですね。Rust では型推論が行われるので、たいていの場合は型を明示する必要はないですが、上のケース(Vec の中身)とか Option な値に None を指定するケースとか、あとキャストするときとかは、型を明示する必要があります。が、昔の Java みたいに型推論のない言語に比べるとかなりストレスはかなり少ないです。
あとは型関連でいうとジェネリクスもありますが、一般的な使い方とあまり変わらない感じでした。
4. その他気になったところ
if が式
match 式もそうですが、値を返せるのがうれしい。
let x = if a { 1 } else { 0 };
とかできます。
以下みたいにカーリーブレイスなしでも書けるようになるとさらにうれしい。
// シンタックスエラー // let x = if a then 1 else 0;
無名関数(クロージャ)の書き方
無名関数の書き方がちょっと特殊で、|
で区切って書きます。
let r = 1..=10; println!("{}", r.fold(0, |acc, v| acc + v)); // 55
型推論もちゃんと効いてくれます。複数行に渡る場合はカーリーブレイスを使ってブロックにすればOK。
高機能な match 式
PHP にも最近入った match 式ですが、Rust のはそれよりちょっと高機能です。
let v = Some(5); let is_over = match v { Some(a) if a >= 5 => true, Some(_) => false, None => false, }; /* ホントは以下でOK let is_over = match v { Some(a) if a >= 5 => true, _ => false, }; */
まず、各アーム(マッチ式内の =>
で分けられた節)で値の取り出しができるので、enum やタプルをマッチさせるときに便利です。値を無視する場合には _
で無視できます。さらに左辺には追加の条件式(ガードという)を書くことができるので細かい条件指定が可能です。もちろん、パターンを網羅してない場合はコンパイルエラーになるので、記述漏れを防ぐことができます。
タプルをデストラクチャして値を取り出すことができて、..
で範囲指定して無視することができます。
let t = (true, 5, 10); let is_over = match t { (true, a, b) if a >= 5 && b >= 10 => true, (true, ..) => false, (false, ..) => false, }; /* ホントは以下でOK let is_over = match t { (true, a, b) if a >= 5 && b >= 10 => true, _ => false, }; */
型エイリアス
既存の型に別名を与えて識別することができます。
type BookId = u32;
ただし、タプル構造体と違って、既存の型と同列に扱われるので(あくまでも別名)、以下のコードはコンパイルエラーになりません。
type BookId = u32; type AuthorId = u32; fn find(bookId: BookId, authorId: AuthorId) {} let bid: BookId = 1; let aid: AuthorId = 2; find(aid, bid); // BookId と AuthorId が逆
あとはマクロとか async/await とか気になってますがその辺はまだ。
おわりに
最初のうちはコンパイルエラー出まくりでしたが(主に所有権とか型絡み)、慣れてくると徐々に減ってきました。エラーメッセージが親切なので、読めばたいていは間違いにすぐ気づけるので、それほど詰まりはしないですが。
オブジェクト指向周りは、継承がないので、PHP 脳で書くと基底クラスに共通処理持たせたくなりますが、トレイトでなんとかなりそうな気配はあります。
自分は仕事ではウェブアプリケーション書くのがほとんどなので、ウェブアプリケーションにフィットするかっていうとややトゥーマッチな感じもしますが、個人的には Go よりかは書き心地は良く感じました。
ただ、まだまだ使ってない機能がたくさんあって、複雑度も高そうなので、この先どうなるかはわかりませんが、あくまでも第一印象ってことで。
ウェブアプリケーションも actix で簡単なのを作ってみたので、いずれその感想も記事にできたらしようと思います。