はじめに
私が今一番好きな言語は Rust です。
Rust は書いていて楽しいですし、学びが多い言語だと感じます。
そこで今回は私が特に好きな Rust の特徴を紹介しつつ、Rust の良さを伝えていきたいと思います。
第 1 回目はこちらに記載しましたのでもしよろしければご覧ください。
今回の記事は私個人の意見であり、他の言語と比べた Rust の優位性を述べたいわけではありません。
あくまで好きなところ、書いていて楽しいところです。
第 2 回目は Rust のマクロについてです。
そもそもマクロとは
まず、好きなところを述べる前にマクロの概要を説明します。
Rust のマクロはコードを生成するための機能です。1
標準で用意されているマクロも多く、利用せずにプログラムを書くことはほとんどないかと思います。2
そのぐらい便利で Rust には欠かせない機能になっているかと思います。
Rust では以下のようにMACRO_NAME!
のように書くことでマクロの機能を利用することができます。!
が特徴的な記号です。
let v = vec![1, 2, 3];
上のコードだけ見ると関数のように見えますが、実際にはコンパイル時にコードが展開されます。
上のコードはコンパイル時に以下のように展開されます。3
let v = {
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
};
つまり、マクロによってコードが生成されるのです。
繰り返し書く必要のある定型的なコードを簡潔に書くことができます。
また、コンパイル時にコードを生成しているため、実行時のオーバーヘッドがないのも大きなメリットかと思います。
ちなみに Rust は型制約がとても強い言語で、可変長引数がサポートされていません。
マクロは可変長引数の関数のように振る舞うこともできるため、Rust では欠かせない機能になっているのかなと思います。
// Rustは可変長引数の関数を作ることができない
let v = Vec::from(1,2,3,4,5,6,7,8);
// 代わりにマクロを使う
let v = vec![1,2,3,4,5,6,7,8];
そしてマクロは自分で作成することも可能です!後ろでも記述していますがここが一つの推しポイントです!
Rust のきついルールをマクロで捻じ曲げるようなことができるので、マクロを使いこなすことができると Rust の楽しさが倍増するかもしれません。
マクロの種類について
Rust には大きく分けて 2 種類のマクロがあります。
宣言的マクロと手続きマクロです。
宣言的マクロ
宣言的マクロはマクロを作成する際の基本形です。
マクロの引数をパターンで定義し、それに応じてコードを生成するマクロコードを記述します。
例えば、任意の数の構造体にあるマーカートレイトを実装したいとします。
普通に実装すると以下のようになります。
trait Object{};
impl Object for Pen{};
impl Object for Apple{};
impl Object for Book{};
...
前の記事でも言いましたがマーカートレイトは振る舞いなしで型を区別できるので便利です。
ただ、上のように毎回同じようなコードを書く必要があり、面倒です。
そこでマクロを使うと以下のように書くことができます。
macro_rules! impl_object {
($($t:ty),*) => {
$(
impl Object for $t{};
)*
};
}
impl_object!(Pen, Apple, Book);
たったこれだけです!もし構造体が増えてもimpl_object!
マクロの引数を追加するだけで対応できます。
上のコードは最終的に地道に書いたコードと同じように展開されます。
詳細な説明は行いませんが、宣言的マクロには以下のような特徴があります。
-
パターンマッチを使って可変長の引数を受け取ることができる
-
$($t:ty),*
の$t:ty
は型のリテラルを$t
に束縛していることを指し、*
は 0 回以上の繰り返しを意味します
-
-
引数で受け取ったパターンを使い、宣言的にコードを生成することができる
宣言的マクロは強力な機能を持っているため、多くの場面で利用されています。
手続きマクロ
手続きマクロは宣言的マクロよりも実装が複雑なマクロです。
宣言的マクロはパターンマッチによりマッチした部分を利用してコードを展開しますが、手続きマクロはパターンではなく、コードを入力として受け取り、コードを出力します。
そのため、宣言的マクロよりも記述することは難しいですが、より柔軟にコードを生成することができます。
基本的に手続きマクロを作成することは少ないかもしれませんが、Rust の標準ライブラリや人気のある外部ライブラリには多くの手続きマクロが用意されています。
例えば、derive
マクロは手続きマクロです。このマクロを使わずに Rust を書くことはほとんどないかと思います。
#[derive(Debug,Clone)]
struct Pen {
color: String,
}
上のderive
マクロはDebug
トレイトとClone
トレイトを自動で実装してくれています。4
つまり、自分でDebug
トレイトやClone
トレイトを実装する必要がないということです。
また、derive
により宣言的に trait の実装を表現できるているので可読性も高いと思います。
手続きマクロは手続きマクロ作成専用の外部ライブラリを利用して作ることが多く、難易度が高いマクロです。
ただ、手続きマクロを使うことで非常に強力な機能を提供することができるため、利用する側は非常に楽に外部ライブラリの機能を使うことができます。
ここまでがマクロの概要となります。何となく、便利さがわかってもらえれば嬉しいです。
次からは私がマクロの何が好きかを書いていきたいと思います。
2-1 書いていて楽しい
一番の理由はこれです。マクロは書くのがとても楽しいです。
宣言的マクロにしろ、手続きマクロにしろ、マクロの作成自体は難易度があるため、実際にうまく書けた時、マクロを利用するだけで多くの定型文を書く必要がなくなった時、などなどとても達成感を感じます。
欠点としてマクロは生成されたコードを想像しながらデバッグをする必要があります。
意味不明な箇所でエラーが起きたりしますが、それはマクロがコードを生成しているからです。
また、読み手側もマクロが生成された後のコードを想像しながら読む必要があります。(マクロを読む必要がないぐらいに完璧にコードが書かれていれば問題ないかもしれませんが)
そのため、むやみやたらにマクロを使うのは本番では避けた方が良いかもしれません。
ただ、遊びで書く分には最高に楽しい機能なので、是非挑戦してみてください。
私も手続きマクロに過去挑戦したこともあるのでよろしければ是非ご覧ください。
2-2 cfg マクロが便利で好き
Rust にはcfg
マクロがあります。このマクロも開発する際には欠かせない機能です。
cfg
マクロはコンパイル時にコードを条件分岐するためのマクロです。
よく利用されるのが#[cfg(test)]
です。これはテスト時のみコンパイルされるコードを記述するために使われます。
#[cfg(test)]
mod tests {
#[test]
fn test() {
assert_eq!(1, 1);
}
}
上のように書くと、cargo test
を実行した時のみmod tests
がコンパイルされます。
つまり、本番のバイナリにはテストコードが含まれず、バイナリを小さく保つことができます。
また応用として fake の作成にも#[cfg(test)]
は利用できます。
例えば現在時刻を返す関数を使っている場合、テスト時には固定の時刻を返したいです。
依存性の注入も一つの手ですが、#[cfg(test)]
を使うことで簡単にテスト時のみの挙動を実装することができます。
#[cfg(test)]
fn now() -> DateTime<Utc> {
Utc.ymd(2021, 1, 1).and_hms(0, 0, 0)
}
#[cfg(not(test))]
fn now() -> DateTime<Utc> {
Utc::now()
}
上のように書くことで、テスト時には固定の時刻を返し、本番時には現在時刻を返すことができます。
また、not
やand
などの論理演算子を使うことで複数の条件を組み合わせることができるのもcfg
マクロの特徴です。
もう一つ個人的によく使うのが OS ごとのコード分岐です。
例えば、Windows と Linux で異なるコードを書く場合、cfg
マクロを使うことで簡単に対応できます。
#[cfg(target_os = "windows")]
fn run() {
println!("windows");
}
#[cfg(target_os = "linux")]
fn run() {
println!("linux");
}
これにより OS ごとに異なるコードを記述することができ、問題なく OS の独自機能を利用することができます。
上のように同じ関数名でなくとも、ある OS にしかない機能を使う場合はcfg
マクロを使って対応することができます。
// say コマンドは macOS にしかない
// この関数は macOS でのみ実行可能
#[cfg(target_os = "macos")]
fn say_command(s: &str) {
Command::new("say").arg(s).output().unwrap();
}
他にも feature flag の実装もcfg
マクロを使って行うことができます。
このようにcfg
マクロはコンパイル時にコードを条件分岐するためのマクロであり、非常に便利な機能だと感じています。
2-3 強力な外部ライブラリのマクロが便利で好き
これを Rust 自体の機能と言うか微妙なラインですが、Rust の外部ライブラリはマクロをふんだんに使っているものが多いです。
これにより、マクロを利用するだけで外部ライブラリの機能を使うことができます。
例えば、serde
ライブラリは Rust のシリアライズ/デシリアライズライブラリですが、derive
マクロを使うだけでシリアライズ/デシリアライズを実装することができます。
#[derive(Serialize, Deserialize)]
struct Person {
name: String,
age: u32,
#[serde(rename = "firstName")]
first_name: String,
}
たったこれだけの記述で構造体のシリアライズ/デシリアライズを実装することができます。
また上の例だとfirst_name
に対してfirstName
という名前でシリアライズ/デシリアライズを行うことをマクロで指定してできています。
そして私の好きなライブラリの一つ Clap は CLI ツール作成に便利な多くのマクロを利用可能です。
CLI ツールは help メッセージや引数のパース、オプションの設定などが必要ですが、Clap を使うことでこれらの機能を簡単に実装することができます。
以下は私が以前作成した CLI ツールのコードです。意味を説明せずともマクロによって何が提供されているのかはわかりやすいのではないかと思います。
#[derive(Parser)]
struct Cli {
#[clap(
short = 'd',
long = "domain",
default_value = "oreil.ly",
help = r#"The domain name to open in the browser. You can override this by setting the `DEFAULT_OURL_DOMAIN` environment variable."#
)]
domain: String,
#[clap(help = "The URL path to open in the browser.")]
path: String,
#[clap(
short = 'b',
long = "bitly",
help = "Use bit.ly to shorten the URL.",
default_value = "false"
)]
bitly: bool,
#[clap(
short = 'o',
long = "oreil",
help = "Use oreil.ly to shorten the URL.",
default_value = "false"
)]
oreil: bool,
}
このような強力なマクロを作成するのはなかなか大変だと思いますが、利用する方は楽かつ宣言的に機能を利用できるところがとても良いと感じます。
ぜひいろんなライブラリのマクロを使ってみてください!
終わりに
今回は Rust の好きなところの 2 つ目としてマクロについて書いてみました。
マクロは書く時は楽しく、利用するときはとても強力で便利な機能です。
頑張ればマクロでできないことはない5ので、マクロを使いこなすことで Rust の楽しさが倍増するかもしれません。
ぜひ、マクロを使って Rust の楽しさを味わってみてください。
もし間違いや、誤解を招くような記述があれば指摘していただけると幸いです。
ここまで読んでいただきありがとうございました!
後少し続ける予定です。引き続きよろしくお願いいたします。