Rust:なぜマクロが必要なのか

Rustにマクロがあると同僚に言ったら、これは悪いように思えました。 以前は同じ反応をしていましたが、Rustはマクロが必ずしも悪いわけではないことを示しました。


それらはどこでどのように適用するのが適切ですか? カットの下を見てください。


マクロに注意する必要がある理由


マクロはメタプログラミングの一種です。マクロはコードを操作するコードです。 メタプログラミングは、悪いコードを書くことから身を守るのが容易ではないため、悪い評判を得ています。 例としては、Cの#defineがあります 。これは、予測不能な方法でコードと簡単にやり取りできます 。また、JavaScriptのevalは、 コードインジェクションのリスクを高めます


Rustのマクロについて


これらの問題の多くは、必要なツールを使用して解決できますが、マクロはこれらのツールの一部を提供します。



これらの目標を達成するために、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 { //... } 

他の言語( FlaskSpringなど )でWebライブラリを使用した場合、このスタイルはおそらく新しいものではありません。 ここではこれらのライブラリを比較しません。Rustの利点(結果として得られるネイティブコードの高性能など)を利用して、Rustで同様のコードを記述できることに注意してください。


短所


マクロは完璧ではありません。いくつかの欠点を考慮してください。



結論


マクロは、開発に役立つ強力なツールです。 Rustのマクロは前向きな開発であり、それらのアプリケーションが適切な場合があるという考えで、皆さんに刺激を与えられたと思います。


-1-: const fn 可能性と混同しないでください。
-2-:マクロ1.1として知られています。
-3-:生成されたコードでマクロを置き換えることをマクロ拡張と呼びます。
-4-:たとえば、Cのprintf 、C#のString.Format 、Pythonの文字列のフォーマット
-5-: format! println!マクロで使用できるフォーマット文字列を扱いますprintln! その他
-6-: varargsformat!使用しformat! 。 この機能(可変引数)は、 関数のオーバーロード禁止するという決定と矛盾するため、マクロの使用が非常に適切です。コア言語にサポートを追加する必要はありません。
-7-:Scalaには、コンパイル時のチェックを行う適切な文字列補間の実装があります。 Rustに文字列補間が追加されるかどうかはわかりませんが、同様の例を既に見ています: try! マクロから言語組み込まれた機能に進化したため、適切なときにこれが可能になります。
-8-:Rustには問題があります-パニックメソッド(たとえば、 unwrapexpectは、呼び出しコードに関する情報にアクセスできないため、役に立たないエラーメッセージを生成します
PartialEqPartialEqオブジェクトの等価性をチェックするために使用されるタイプ。正確さのためにEqも使用します。 PartialEq ドキュメントでは、Rustにこのような区分がある理由について説明しています。
-10-:問題はリフレクションによって解決できますが、適切なruntimeが必要なため、ランタイムのパフォーマンスが低下するため、言語の設計と矛盾するため、Rustではサポートされていません。
-11-:Rocketの作者であるSergio Benitezがこれについて良いプレゼンテーションを行いました。



Source: https://habr.com/ru/post/J350716/


All Articles