Rustにマクロがあると同僚に言ったら、これは悪いように思えました。 以前は同じ反応をしていましたが、Rustはマクロが必ずしも悪いわけではないことを示しました。
それらはどこでどのように適用するのが適切ですか? カットの下を見てください。
マクロに注意する必要がある理由
マクロはメタプログラミングの一種です。マクロはコードを操作するコードです。 メタプログラミングは、悪いコードを書くことから身を守るのが容易ではないため、悪い評判を得ています。 例としては、Cの#define
があります 。これは、予測不能な方法でコードと簡単にやり取りできます 。また、JavaScriptのeval
は、 コードインジェクションのリスクを高めます 。
Rustのマクロについて
これらの問題の多くは、必要なツールを使用して解決できますが、マクロはこれらのツールの一部を提供します。
- 手動で記述するのではなく、冗長/些細なコード(定型文)を生成します。
- 新しい構文が追加される前に言語を拡張し、言語のスペースを閉じます。
- パフォーマンスの最適化-実行時に以前に実行されていたアクションの一部が-1-コンパイル段階で実行されるようになったため。
これらの目標を達成するために、Rustには2種類の-2-マクロが含まれています。 これらは異なる名前(手続き型、宣言型、 macro_rules
など)で知られていますが、これらの名前はやや紛らわしいと思います。 幸いなことに、これらはそれほど重要ではないので、それらを機能的および属性と呼びます 。
2種類のマクロがある理由は、さまざまなタスクに適しているためです。
- 機能的:コードに簡単に含めることができます。
- 属性:周囲のコードに適合しないコードを生成するのにより適しています。
他のすべての点で、アプリケーションの結果は似ています。コンパイラはコンパイル時にマクロを「消去」し、マクロから生成されたコードで置き換え、「通常の」非マクロコード-3-でコンパイルします。 2種類のマクロの実装は大きく異なりますが、ここでは詳しく説明しません。
なぜ機能マクロなのか
関数マクロは、関数のように実行できます。 このタイプのマクロには!
通話中:
let x = action(); // let y = action!(); //
関数を使用できるのに、なぜマクロを使用するのですか? 関数マクロは関数とは何の関係もないことを覚えておく必要があります-関数マクロは使いやすいように関数に似ています。 したがって、問題はこのタイプのマクロが関数よりも優れているかどうかではなく、ソースコードを変更する機能が必要かどうかです。
有用な声明
assert!
見てみましょうassert!
、ある条件が満たされていることを確認するために使用され、そうでない場合はパニックを引き起こします。 これらは実行時にチェックされるので、メタプログラミングはここで何を提供しますか? assert!
時に出力されるメッセージを見てみましょうassert!
失敗する:
fn main() { let mut vec = Vec::new(); // vec.push(1); // assert!(vec.is_empty()) // - assert! // : // thread 'main' panicked at 'assertion failed: vec.is_empty()', src\main.rs:4 }
このメッセージには、確認中の条件が含まれています。 つまり、マクロはソースコードに基づくエラーメッセージを作成しますが、プログラムに手動で挿入しなくても意味のあるエラーメッセージを取得します。
タイプセーフな文字列フォーマット
多くのプログラミング言語は、行-4-の出力形式の設定をサポートしています。 Rustも例外ではなく、 format!
文字列形式の設定もサポートしていformat!
。 しかし、問題はまだ残っています。なぜメタプログラミングを使用して問題を解決する必要があるのでしょうか? println!
見てみましょうprintln!
(内部でformat!
を使用して、渡された文字列を処理します)-5-。
fn main() { // println!("{} is {} in binary", 2, 10); // : 2 is 10 in binary // println!("{0} is {0:b} in binary", 3) // : 3 is 11 in binary }
format!
する多くの理由がありformat!
マクロ-6-として実装されていますが、コンパイル時に文字列を部分に分割し、分析して、渡された引数の処理がタイプセーフかどうかを確認できることを強調したいと思います。 コードを変更すると、コンパイルエラーが発生する可能性があります。
fn main() { println!("{} is {} in binary", 2/*, 10*/); // : , println!("{0} is {0:b} in binary", "3") // : }
他の多くの言語では、これらのエラーは実行時に表示されますが、Rustではマクロを使用してコンパイル時にこのチェックを実行し、ランタイムチェックをチェックせずに文字列形式を処理する強力なコードを生成できます。
簡単なロギング
この例では、言語のエコシステムについて少し見ていきましょう。 Rustには、メインロギングフロントエンドとして使用されるログパッケージがあります 。 他のログソリューションと同様に、異なるログレベルを提供しますが、他のソリューションとは異なり、これらのレベルは関数ではなくマクロによって表されます。
ロギングは、 file!
使用方法におけるメタプログラミングの力を示していfile!
とline!
; これらのマクロを使用すると、ソースコード内のログ関数の呼び出しの正確な場所を確立できます。 例を見てみましょう。 log
はフロントエンドであるため、バックエンドであるflexi_loggerパッケージを追加します。
#[macro_use] extern crate log; extern crate flexi_logger; use flexi_logger::{Logger, LogSpecification, LevelFilter}; fn main() { // `trace` let log_config = LogSpecification::default(LevelFilter::Trace).build(); Logger::with(log_config) .format(flexi_logger::opt_format) // Specify how we want the logs formatted .start() .unwrap(); // . info!("Fired up and ready!"); complex_algorithm() } fn complex_algorithm() { debug!("Running complex algorithm."); for x in 0..3 { let y = x * 2; trace!("Step {} gives result {}", x, y) } }
このプログラムは印刷します:
[2018-01-25 14:48:42.416680 +01:00] INFO [src\main.rs:16] Fired up and ready! [2018-01-25 14:48:42.418680 +01:00] DEBUG [src\main.rs:22] Running complex algorithm. [2018-01-25 14:48:42.418680 +01:00] TRACE [src\main.rs:25] Step 0 gives result 0 [2018-01-25 14:48:42.418680 +01:00] TRACE [src\main.rs:25] Step 1 gives result 2 [2018-01-25 14:48:42.418680 +01:00] TRACE [src\main.rs:25] Step 2 gives result 4
ご覧のとおり、ログにはファイル名と行番号が含まれています。
- このデータを取得するためのリードタイムのオーバーヘッドなしでこの情報を受け取ります。
- 情報は正確で有用です。
前者の場合、コンパイラは必要な情報を実行可能ファイルに挿入し、必要に応じて印刷できます。 コンパイル時にこの問題を解決しなかった場合、実行時にスタックを調べる必要がありました。エラーが発生し、パフォーマンスが低下します。
ロギングマクロを関数に置き換えても、 file!
呼び出すことができfile!
とline!
:
fn info(input: String) { // info! Log::log( logger(), RecordBuilder::new() .args(input) .file(Some(file!())) .line(Some(line!())) .build() ) }
そして、このコードは次を出力します:
[2018-01-25 14:48:42.416680 +01:00] INFO [src\loggers\info.rs:7] Fired up and ready!
ファイル名と行番号は、 ロギング機能が呼び出された場所を示すため、 役に立ちません。 言い換えれば、最初の例は、 file!
を置くことによって生成されたコードに置き換えられたマクロを使用したという理由だけでfile!
とline!
ソースコードに直接アクセスし、必要な情報を提供します(ファイル名と行番号は現在、実行可能ファイルにあります)-8-。
なぜ属性マクロ
Rustには、コードのタグ付けに必要な属性の概念が含まれています 。 たとえば、テスト関数は次のようになります。
#[test] // <- fn my_test() { assert!(1 > 0) }
cargo test
を実行すると、この機能が起動します。 属性マクロを使用すると、「ネイティブ」属性に似ているが異なる効果を持つ新しい属性を作成できます。 現時点では、重要な制限があります:安定版ブランチのコンパイラでは、派生属性を使用するマクロのみが機能しますが、カスタム属性は夜間ビルドで機能します 。 以下の違いを考慮してください。
属性マクロによって提供される利点を考慮して、ソースコードを操作できるコードとできないコードを比較することをお勧めします。
冗長コードの取得(定型)
derive
属性は 、Rustで特性実装を生成するために使用されます。 PartialEq
見てみましょう。
#[derive(PartialEq, Eq)] struct Data { content: u8 } fn main() { let data = Data { content: 2 }; assert!(data == Data { content: 2 }) }
ここでは、インスタンスが等しいかどうかを確認する( ==
使用する)インスタンスを持つ構造体を作成し、 PartialEq
の実装を取得します。 PartialEq
自分で実装することもできますが、オブジェクトの等価性のみをチェックするため、実装は簡単です。
impl PartialEq for Data { fn eq(&self, other: &Data) -> bool { self.content == other.content } }
このコードはコンパイラーによって生成されるため、マクロを使用することで時間を節約できますが、さらに重要なことは、同等性をチェックするコードを最新の状態に保つ必要がないことです。 構造にフィールドを追加する場合、 PartialEq
手動実装で検証を変更する必要があります。そうでない場合(たとえば、検証コードの変更を忘れた場合)、さまざまなオブジェクトの検証が成功する可能性があります。
サポートの負担を取り除くことは、属性マクロが提供する大きな利点です。 構造コードを1か所に記述し、検証関数の実装を自動的に受け取りました。コンパイル時間は、検証コードが構造の現在の定義と一致することを保証します。 上記の顕著な例は、データのシリアル化に使用されるserdeパッケージです。マクロを使用しない場合、構造体フィールドの名前にserdeを示す文字列を使用する必要があり 、これらの文字列を構造体-10-の定義に関して最新の状態に保ちます。
利点を引き出す
derive
は、特性の実装だけでなく、属性マクロを使用してコードを生成するための多くの機能の1つです。 これは現在ナイトリービルドで利用可能ですが、 今年安定することを望みます。
現時点で最も顕著なユースケースは、Webサーバーを作成するためのライブラリであるRocketです。 RESTエンドポイントを作成するには、関数に属性を追加する必要があるため、関数には要求を処理するために必要なすべての情報が含まれています。
#[post("/user", data = "<new_user>")] fn new_user(admin: AdminUser, new_user: Form<User>) -> T { //... }
他の言語( FlaskやSpringなど )でWebライブラリを使用した場合、このスタイルはおそらく新しいものではありません。 ここではこれらのライブラリを比較しません。Rustの利点(結果として得られるネイティブコードの高性能など)を利用して、Rustで同様のコードを記述できることに注意してください。
短所
マクロは完璧ではありません。いくつかの欠点を考慮してください。
- マクロからコードを取得してこのコードをコンパイルするのに時間がかかるため、コンパイル時間が長くなります。
- マクロを使用するとコピーペーストに陥りやすくなり、小さな行が大きなコードブロックに展開されるため、マクロはマシンコードのサイズの増加につながる可能性があります。 以前は、これはclapパッケージの問題でした。作者はこの問題について、またコードをダイエットする方法について説明する良いメモを書きました 。
- 生成されたコードをデバッグする必要があるため、デバッグはより困難になります。 幸いなことに、あなたを助けることができるツールがあります。 マクロを使用するときのエラーメッセージの読みやすさと情報内容は、コンパイラーではなく、マクロの作成者に依存します。 繰り返しますが、必要なツールがあります(たとえば、
compiler_error!
およびsynのようなパッケージ)。 - DSLの過負荷(少し主観的なポイント)。 たとえば、
format!
Rustではなく、 DSLであるミニ言語で記述された文字列を受け入れます。 DSLは強力なツールですが、開発者が独自の組み込み言語を作成することに決めた場合、DSLを使用するのは簡単ではありません。 DSLを書くことに決めた場合、優れた機能はより多くの責任を意味し、DSLを実行できるということは、その必要性を意味しないことを忘れないでください。
結論
マクロは、開発に役立つ強力なツールです。 Rustのマクロは前向きな開発であり、それらのアプリケーションが適切な場合があるという考えで、皆さんに刺激を与えられたと思います。
-1-: const fn
可能性と混同しないでください。
-2-:マクロ1.1として知られています。
-3-:生成されたコードでマクロを置き換えることをマクロ拡張と呼びます。
-4-:たとえば、Cのprintf 、C#のString.Format 、Pythonの文字列のフォーマット 。
-5-: format!
println!
マクロで使用できるフォーマット文字列を扱いますprintln!
その他 。
-6-: varargsはformat!
使用しformat!
。 この機能(可変引数)は、 関数のオーバーロードを禁止するという決定と矛盾するため、マクロの使用が非常に適切です。コア言語にサポートを追加する必要はありません。
-7-:Scalaには、コンパイル時のチェックを行う適切な文字列補間の実装があります。 Rustに文字列補間が追加されるかどうかはわかりませんが、同様の例を既に見ています: try!
マクロから言語に組み込まれた機能に進化したため、適切なときにこれが可能になります。
-8-:Rustには問題があります-パニックメソッド(たとえば、 unwrap
やexpect
) は、呼び出しコードに関する情報にアクセスできないため、役に立たないエラーメッセージを生成します 。
PartialEq
: PartialEq
オブジェクトの等価性をチェックするために使用されるタイプ。正確さのためにEq
も使用します。 PartialEq
ドキュメントでは、Rustにこのような区分がある理由について説明しています。
-10-:問題はリフレクションによって解決できますが、適切なruntimeが必要なため、ランタイムのパフォーマンスが低下するため、言語の設計と矛盾するため、Rustではサポートされていません。
-11-:Rocketの作者であるSergio Benitezがこれについて良いプレゼンテーションを行いました。