はじめに
hooq は、 ? 演算子1、 return 、末尾の式などに対し、必要に応じてメソッドを挿入できる属性マクロです。
use hooq::hooq;
#[hooq]
#[hooq::method(.map(|v| v * 2))]
fn double(s: &str) -> Result<u32, Box<dyn std::error::Error>> {
let res = s.parse::<u32>()?;
Ok(res)
}
fn double_expanded(s: &str) -> Result<u32, Box<dyn std::error::Error>> {
let res = s.parse::<u32>().map(|v| v * 2)?;
Ok(res)
}
#[test]
fn test() {
assert_eq!(double("21").unwrap(), double_expanded("21").unwrap());
}
fn main() {
println!("double_hooked: {}", double("21").unwrap());
println!("double: {}", double_expanded("21").unwrap());
}
豊富なプリセットがあり、エラーロギング等をお手軽に Result にフックすることができます。
なぜhooqを使うか?
hooqのモチベーションを、次のような Result 型を返す関数を含むソースコードを例に説明します。
use std::error::Error;
fn load_host_and_port() -> Result<String, Box<dyn Error>> {
// Loading APP_HOST
let host = std::env::var("APP_HOST")?;
// Loading APP_PORT
let port = std::env::var("APP_PORT")?;
// Convert to u16
let port: u16 = port.parse()?;
Ok(format!("{host}:{port}"))
}
fn main() -> Result<(), Box<dyn Error>> {
let host_and_port = load_host_and_port()?;
println!("Server is running on: {}", host_and_port);
// snip
Ok(())
}
#[test]
fn test_load_host_and_port() {
unsafe {
std::env::set_var("APP_HOST", "localhost");
std::env::set_var("APP_PORT", "8080");
}
let host_and_port = load_host_and_port().unwrap();
assert_eq!(host_and_port, "localhost:8080");
}
各環境変数を指定して実行するとエラーなく実行されます。
$ APP_HOST=127.0.0.1 APP_PORT=10 cargo run -q
Server is running on: 127.0.0.1:10
このプログラムがエラーとなるように実行してみます。
$ APP_PORT=10 cargo run -q
Error: NotPresent
main 関数が返した Box<dyn Error> の内容が表示され、(おそらく)何かしらの環境変数が足りていないというエラーが発生しています。
しかしこのエラー表示は酷いです!
- どういうコンテキストのなんのエラーなのかがわからない
- エラーが発生した場所がわからない
おそらくこのRustプログラムを書いた人は、きめ細かいエラーハンドリングを含んだフォーマルなアプリケーションを作りたかったのではなく、ちょっとしたカジュアルなCLIツールを作りたかったのだと思います。しかしRustのエラー表示はサボる人には冷たいものです2。
こんな時に使えるのが hooq 属性マクロです!
use std::error::Error;
use hooq::hooq;
#[hooq]
fn load_host_and_port() -> Result<String, Box<dyn Error>> {
// Loading APP_HOST
let host = std::env::var("APP_HOST")?;
// Loading APP_PORT
let port = std::env::var("APP_PORT")?;
// Convert to u16
let port: u16 = port.parse()?;
Ok(format!("{host}:{port}"))
}
#[hooq]
fn main() -> Result<(), Box<dyn Error>> {
let host_and_port = load_host_and_port()?;
println!("Server is running on: {}", host_and_port);
// snip
Ok(())
}
#[test]
fn test_load_host_and_port() {
unsafe {
std::env::set_var("APP_HOST", "localhost");
std::env::set_var("APP_PORT", "8080");
}
let host_and_port = load_host_and_port().unwrap();
assert_eq!(host_and_port, "localhost:8080");
}
#[hooq] を付けるだけであら不思議、エラーのスタックトレースもどきが出力されるようになります!
$ APP_PORT=10 cargo run -q
[mdbook-source-code/index/src/main.rs:8:41] NotPresent
8> std::env::var("APP_HOST")?
|
[mdbook-source-code/index/src/main.rs:21:45] NotPresent
21> load_host_and_port()?
|
Error: NotPresent
どうやら APP_HOST 環境変数が足りていなかったようですね。エラー発生箇所についても、8行目、21行目と伝搬していったということがわかります。
一体 hooq マクロが何をしたのか、それは 後ほど 解説していきます。
-
旧tryマクロ ↩
-
この規模なら
unwrapを使えばよいだろうって…?君のような勘のいいガキは嫌いだよ…。そうは言ってもResult型が使えないのは不便なはずなので、hooqマクロの意義は依然としてあるでしょう。 ↩
ドキュメントリンク集
hooq 関係のドキュメント類リンク集です。
- hooq Reference 日本語版 (本ドキュメント)
- GitHub
- docs.rs
- crates.io
チュートリアル
本章では3ページ+まとめ1ページにわたりhooqの基本的な使い方を紹介していきます。
hooq クレートは下記の方法で導入済みのものとします。
hooqマクロの導入方法
hooqクレートは他のクレート同様に cargo add で追加できます。
cargo add hooq
Cargo.tomlに最新バージョンを記載することでも可能です。最新バージョンは crates.io から確認できます。
hooqにもいくつかのfeatureはありますが、よく使うものに関してはdefault featureに含めているので、通常の利用範囲では追加する必要はありません。featureについては features にまとめています。
#[hooq] を付与しエラー発生行を取得
はじめに で出した次の例を引き続き使って、hooqマクロが一体ソースコードにどのような細工をしたのかを解説し、hooqの使い方を紹介していきます。
use std::error::Error;
use hooq::hooq;
#[hooq]
fn load_host_and_port() -> Result<String, Box<dyn Error>> {
// Loading APP_HOST
let host = std::env::var("APP_HOST")?;
// Loading APP_PORT
let port = std::env::var("APP_PORT")?;
// Convert to u16
let port: u16 = port.parse()?;
Ok(format!("{host}:{port}"))
}
#[hooq]
fn main() -> Result<(), Box<dyn Error>> {
let host_and_port = load_host_and_port()?;
println!("Server is running on: {}", host_and_port);
// snip
Ok(())
}
#[test]
fn test_load_host_and_port() {
unsafe {
std::env::set_var("APP_HOST", "localhost");
std::env::set_var("APP_PORT", "8080");
}
let host_and_port = load_host_and_port().unwrap();
assert_eq!(host_and_port, "localhost:8080");
}
#[hooq] 属性マクロを関数に付与すると、次のhooqマクロデフォルトのメソッド inspect_err が各 ? の手前、 return の返り値や関数末尾の後ろ(ただし関数シグネチャの返り値型が Result などフック対象の型の場合)に挿入(フック)されるようになります。
.inspect_err(|e| {
let path = $path;
let line = $line;
let col = $col;
let expr = ::hooq::summary!($source);
::std::eprintln!("[{path}:{line}:{col}] {e:?}\n{expr}");
})
$path のようなものはhooq固有のメタ変数です。詳細は メタ変数 にて触れていますが、デフォルトメソッドに登場するものについては以下の通りです。
| メタ変数 | リテラル種別 | 説明 |
|---|---|---|
$path | 文字列 | クレートルート等からのファイル相対パス |
$line | 整数 | メソッドがフックされた行 |
$col | 整数 | メソッドがフックされた列 |
$source | 対象式のトークン列 | フックされる対象の式(ログ表示用) |
#[hooq] が施された load_host_and_port 関数は次のように展開されます。 eprintln! 等まで展開されてしまうため一致はしませんが cargo expand で確かめると似たような出力が得られるでしょう。
---
source: mdbook-source-code/index/tests/test.rs
expression: "format!(\"{stdout}\")"
---
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use std::error::Error;
use hooq::hooq;
fn load_host_and_port() -> Result<String, Box<dyn Error>> {
let host = std::env::var("APP_HOST")
.inspect_err(|e| {
let path = "mdbook-source-code/index/src/main.rs";
let line = 8usize;
let col = 41usize;
let expr = " 8> std::env::var(\"APP_HOST\")?\n |";
::std::eprintln!("[{0}:{1}:{2}] {3:?}\n{4}\n", path, line, col, e, expr);
})?;
let port = std::env::var("APP_PORT")
.inspect_err(|e| {
let path = "mdbook-source-code/index/src/main.rs";
let line = 11usize;
let col = 41usize;
let expr = " 11> std::env::var(\"APP_PORT\")?\n |";
::std::eprintln!("[{0}:{1}:{2}] {3:?}\n{4}\n", path, line, col, e, expr);
})?;
let port: u16 = port
.parse()
.inspect_err(|e| {
let path = "mdbook-source-code/index/src/main.rs";
let line = 14usize;
let col = 33usize;
let expr = " 14> port.parse()?\n |";
::std::eprintln!("[{0}:{1}:{2}] {3:?}\n{4}\n", path, line, col, e, expr);
})?;
Ok(format!("{0}:{1}", host, port))
}
fn main() -> Result<(), Box<dyn Error>> {
let host_and_port = load_host_and_port()
.inspect_err(|e| {
let path = "mdbook-source-code/index/src/main.rs";
let line = 21usize;
let col = 45usize;
let expr = " 21> load_host_and_port()?\n |";
::std::eprintln!("[{0}:{1}:{2}] {3:?}\n{4}\n", path, line, col, e, expr);
})?;
{
::std::io::_print(format_args!("Server is running on: {0}\n", host_and_port));
};
Ok(())
}
line!() マクロは非推奨!
行数取得について、「line!() マクロを使えばよいのでは?わざわざ $line メタ変数を用意されても認知負荷が上がる」という声をいただきました。
しかしこちらは意図的な設計で、まさしく line!() マクロがうまく機能しないことがメタ変数導入のきっかけとなっています。
実際に利用していただくとよくわかりますが、 line!()マクロはフックが行われる行を指しません。 #[hooq] が存在する行 (あるいは #[hooq::method(...)] の行)を出力してしまいます。これは欲しい情報ではないでしょう。ゆえにフックが行われた行を正確に取得するために $line メタ変数を設けています。
詳細はこちらの記事に書いています: (WIP)
#[hooq] だけでも上記のように展開されるおかげで、 はじめに に掲載した次の実行結果が得られ、ソースコードのどの行でエラーが発生したかすぐにわかるようになります。
$ APP_PORT=10 cargo run -q
[mdbook-source-code/index/src/main.rs:8:41] NotPresent
8> std::env::var("APP_HOST")?
|
[mdbook-source-code/index/src/main.rs:21:45] NotPresent
21> load_host_and_port()?
|
Error: NotPresent
ところで、デフォルトでフックされるメソッドも十分素敵ですが、カスタムしたいですよね…?メソッドのカスタマイズは 次のレッスン から扱います。
#[hooq::method(...)] でフックをカスタム
フックされるメソッドはユーザーが設定することが可能です。
プロジェクトのCargo.tomlの name を表示するプログラム例で説明します。tomlファイルを読めるようにしたいためtomlクレートを導入します。
cargo add toml
ひとまず指定されたパスのファイルをtomlファイルとして読み取る機構を作ります。
use hooq::hooq;
#[hooq]
#[hooq::method(.$so_far.inspect(|_| {
println!("Success: `{}` @ Line {}: Col: {}", stringify!($source), $line, $col);
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());
let _cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;
// snip
Ok(())
}
そのままhooqマクロを適用しても良いですが、今回はフックをカスタムしてみるというお題なのでやってみましょう。エラーなくうまくいった場合もわかるように、 inspect メソッドも追加してみます。
フックするメソッドを指定したい時は、 #[hooq::method(...)] 属性を使います。現在設定済みのフックメソッドを表す .$so_far メタ変数を使うことで、追加分として inspect メソッドを入れることができます。
use hooq::hooq;
#[hooq]
#[hooq::method(.$so_far.inspect(|_| {
println!("Success: `{}` @ Line {}: Col: {}", stringify!($source), $line, $col);
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());
let _cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;
// snip
Ok(())
}
失敗の場合いままで通りログが出ます。
[mdbook-source-code/tutorial-1/src/main.rs:10:81] Os { code: 2, kind: NotFound, message: "No such file or directory" }
10> std::fs::read_to_string(path)?
|
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }
成功の場合もログを出力するようになりました!
Success: `std :: fs :: read_to_string(path) ?` @ Line 10: Col: 81
Success: `toml :: from_str(& std :: fs :: read_to_string(path) ?) ?` @ Line 10: Col: 83
次は名前を表示する機構を作りましょう。tomlから目的のフィールドを toml::Value::get を使うことで抽出していきます。 toml::Value::get は Option を返します。ok_or_else を使うことで、 Option 型を Result 型に変換することが可能なので、今回はそのようにします。
use hooq::hooq;
fn display_name(val: &toml::Value) -> Result<(), String> {
let name = val
.get("package")
.ok_or_else(|| format!("get package [Line: {}]", line!()))?
.get("name")
.ok_or_else(|| format!("get name [Line: {}]", line!()))?
.as_str()
.ok_or_else(|| format!("as_str [Line: {}]", line!()))?;
println!("name: {name}");
Ok(())
}
#[hooq]
#[hooq::method(.$so_far.inspect(|_| {
println!("Success: `{}` @ Line {}: Col: {}", stringify!($source), $line, $col);
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());
let cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;
display_name(&cargo_toml)?;
Ok(())
}
…このような恣意的な例を持ち出して私が何を言いたいか、もうお分かりですね? .ok_or_else(...) はボイラープレートです!hooqマクロを使えばこの記述は簡略化できます!
use hooq::hooq;
#[hooq]
#[hooq::method(.ok_or_else(|| {
format!("{} [Line: {}, {}]",
stringify!($source),
$line,
$nth
)
}))]
fn display_name(val: &toml::Value) -> Result<(), String> {
let name = val.get("package")?.get("name")?.as_str()?;
println!("name: {name}");
Ok(())
}
#[hooq]
#[hooq::method(.$so_far.inspect(|_| {
println!("Success: `{}` @ Line {}: Col: {}", stringify!($source), $line, $col);
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());
let cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;
display_name(&cargo_toml)?;
Ok(())
}
今回、何番目の ? 演算子であるかを示す $nth メタ変数も入れてみました。エラーになるように実行させると次のようになります。
Success: `std :: fs :: read_to_string(path) ?` @ Line 26: Col: 80
Success: `toml :: from_str(& std :: fs :: read_to_string(path) ?) ?` @ Line 26: Col: 82
[mdbook-source-code/tutorial-3/src/main.rs:28:30] "val.get(\"package\") ? .get(\"name\") ? [Line: 12, 2nd ?]"
28> display_name(&cargo_toml)?
|
Error: "val.get(\"package\") ? .get(\"name\") ? [Line: 12, 2nd ?]"
package フィールドはあるものの name フィールドがないtomlを読ませました。想定通り、2番目の ? でエラーになったことが示されています。
#[hooq::skip_all] でフックをスキップ
「4は 404 等を連想させて不吉1だからタイトルに4が入っているかチェックするぜ!」と言われたのでバリデーションを付けることになりました。こんな感じでしょうか?
use hooq::hooq;
#[hooq]
#[hooq::method(.ok_or_else(|| {
format!("{} [Line: {}, {}]",
stringify!($source),
$line,
$nth
)
}))]
fn display_name_by_mista(val: &toml::Value) -> Result<(), String> {
let name = val.get("package")?.get("name")?.as_str()?;
if name.contains("4") {
return Err(format!(
"name `{name}` contains '4'. Guido Mista disallow this."
));
}
println!("Mista「name: {name}」");
Ok(())
}
#[hooq]
#[hooq::method(.$so_far.inspect(|_| {
println!("Success: `{}` @ Line {}: Col: {}", stringify!($source), $line, $col);
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());
let cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;
display_name_by_mista(&cargo_toml)?;
Ok(())
}
hooqは return Err(...); にもメソッドをフックすることを試みるため、 Result 型に ok_or_else メソッドが存在しないというコンパイルエラーになってしまいます2。
error[E0599]: no method named `ok_or_else` found for enum `Result<T, E>` in the current scope
--> mdbook-source-code/tutorial-4-compile-error/src/main.rs:15:9
|
15 | return Err(format!(
| ^^^^^^
|
help: there is a method `or_else` with a similar name
|
15 - return Err(format!(
15 + or_else Err(format!(
|
For more information about this error, try `rustc --explain E0599`.
error: could not compile `tutorial-4-compile-error` (bin "tutorial-4-compile-error") due to 1 previous error
ここは今回はフックしたくない場所です。そのような場合は #[hooq::skip_all] を付与することで、フックさせないようにできます!
use hooq::hooq;
#[hooq]
#[hooq::method(.ok_or_else(|| {
format!("{} [Line: {}, {}]",
stringify!($source),
$line,
$nth
)
}))]
fn display_name_by_mista(val: &toml::Value) -> Result<(), String> {
let name = val.get("package")?.get("name")?.as_str()?;
#[hooq::skip_all]
if name.contains("4") {
return Err(format!(
"name `{name}` contains '4'. Guido Mista disallow this."
));
}
println!("Mista「name: {name}」");
Ok(())
}
#[hooq]
#[hooq::method(.$so_far.inspect(|_| {
println!("Success: `{}` @ Line {}: Col: {}", stringify!($source), $line, $col);
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());
let cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;
display_name_by_mista(&cargo_toml)?;
Ok(())
}
完成したプログラム全体
use hooq::hooq;
#[hooq]
#[hooq::method(.ok_or_else(|| {
format!("{} [Line: {}, {}]",
stringify!($source),
$line,
$nth
)
}))]
fn display_name_by_mista(val: &toml::Value) -> Result<(), String> {
let name = val.get("package")?.get("name")?.as_str()?;
#[hooq::skip_all]
if name.contains("4") {
return Err(format!(
"name `{name}` contains '4'. Guido Mista disallow this."
));
}
println!("Mista「name: {name}」");
Ok(())
}
#[hooq]
#[hooq::method(.$so_far.inspect(|_| {
println!("Success: `{}` @ Line {}: Col: {}", stringify!($source), $line, $col);
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());
let cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;
display_name_by_mista(&cargo_toml)?;
Ok(())
}
無事にコンパイルが通り、名前に4が入っている時はエラーとなります。
Success: `std :: fs :: read_to_string(path) ?` @ Line 33: Col: 80
Success: `toml :: from_str(& std :: fs :: read_to_string(path) ?) ?` @ Line 33: Col: 82
[mdbook-source-code/tutorial-4/src/main.rs:35:39] "name `tutorial-4` contains '4'. Guido Mista disallow this."
35> display_name_by_mista(&cargo_toml)?
|
Error: "name `tutorial-4` contains '4'. Guido Mista disallow this."
#[hooq::skip_all] の他にも、途中でhooqの挙動を変更するための属性はいくつかあります。 #[hooq::method(...)] もその一つで、関数の冒頭だけではなく、個別の式や文に付与することも可能です。詳細が知りたい方は属性を参照してください。
ここまでカスタムフックを設定する方法を解説しました。しかし関数ごとに毎回 #[hooq::method(...)] で目当てのメソッドを設定するのはちょっと面倒ですよね?
次のレッスンではあらかじめカスタムフックを設定しておいたり、hooqクレート自体が用意している便利な設定を適用するための フレーバー機能 について解説したいと思います。
-
でもこれで行くとサーバーサイドエンジニアなどには
5の方が不吉そうですね。 ↩ -
hooqマクロのSpan設定の影響でエラーの補足がおかしくなってしまっていますね。もう少しマシなSpanに直したいと思います。 ↩
フレーバーでプリセットを作成/利用する
前のレッスン の最後に、 Option 型のメソッド ok_or_else が Result 型にはないためにフックをスキップするという話をしました。
そもそも Result と Option の両方にあるメソッドをフックすることはできないでしょうか…? anyhowクレートが提供するメソッド Context::with_context がまさにうってつけです。 Option 型に対して .with_context(...) を呼ぶと、 None の時は anyhow::Result の Err バリアントへと変換を行ってくれます。
use anyhow::{Context, Result};
use hooq::hooq;
#[hooq]
#[hooq::method(.with_context(|| {
let path = $path;
let line = $line;
let col = $col;
let expr = ::hooq::summary!($source);
format!("[{path}:{line}:{col}]\n{expr}")
}))]
fn display_name_by_mista(val: &toml::Value) -> Result<()> {
let name = val.get("package")?.get("name")?.as_str()?;
if name.contains("4") {
return Err(anyhow::anyhow!(
"name `{name}` contains '4'. Guido Mista disallow this."
));
}
println!("Mista「name: {name}」");
Ok(())
}
#[hooq]
#[hooq::method(.with_context(|| {
let path = $path;
let line = $line;
let col = $col;
let expr = ::hooq::summary!($source);
format!("[{path}:{line}:{col}]\n{expr}")
}))]
fn main() -> Result<()> {
let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());
let cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;
display_name_by_mista(&cargo_toml)?;
Ok(())
}
エラーが発生するように実行すると、 .with_context(...) の分だけエラーがスタックされ、トレースが得られます。
Error: [mdbook-source-code/tutorial-4-with-anyhow/src/main.rs:41:39]
41> display_name_by_mista(&cargo_toml)?
|
Caused by:
0: [mdbook-source-code/tutorial-4-with-anyhow/src/main.rs:17:9]
17> return Err(anyhow::anyhow!(
18| "name `{name}` contains '4'. Guido Mista disallow this."
19| ))
|
1: name `tutorial-4-with-anyhow` contains '4'. Guido Mista disallow this.
anyhowクレートはとても良く使われるクレートであり、そして .with_context(...) はここまでで示した通りとても便利なメソッドです。
そこで、このフックは頻出であろうと考えhooqではanyhowに対し特別なプリセット… anyhow フレーバー を設けています。hooqではプリセットのことをわかりやすさのためフレーバー(flavor)と呼称しています。anyhowにとどまらずlog, eyreやtracingといったクレートに対してもフレーバーを用意しています。
今まで #[hooq] と記載していた属性マクロ呼び出しを #[hooq(フレーバー名)] とすることでフレーバーを指定できます。
use anyhow::Result;
use hooq::hooq;
#[hooq(anyhow)]
fn display_name_by_mista(val: &toml::Value) -> Result<()> {
let name = val.get("package")?.get("name")?.as_str()?;
if name.contains("4") {
return Err(anyhow::anyhow!(
"name `{name}` contains '4'. Guido Mista disallow this."
));
}
println!("Mista「name: {name}」");
Ok(())
}
#[hooq(anyhow)]
fn main() -> Result<()> {
let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());
let cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;
display_name_by_mista(&cargo_toml)?;
Ok(())
}
結果はフレーバーを使わないソースコードと同一です!
あらかじめ用意されているフレーバーについてはフレーバーのページを参照してください。
よく使う設定をフレーバーとして用意する
hooqクレート側で予め用意したフレーバーだけでなく、ユーザーが事前に用意したフレーバーを用いることも可能です。
フレーバーはクレートルート( CARGO_MANIFEST_DIR が示すディレクトリ)に hooq.toml というファイルを設置することで可能です。以下は hooq.toml の設定例です。
[my_flavor]
method = """.inspect_err(|_| {
eprintln!("Error @ Line {}: Col: {}\n{}", $line, $col, ::hooq::summary!($source));
})
.inspect(|_| {
println!("Success @ Line {}: Col: {}\n{}", $line, $col, ::hooq::summary!($source));
})"""
hook_targets = ["?", "return", "tail_expr"]
tail_expr_idents = ["Ok", "Err"]
result_types = ["Result"]
hook_in_macros = true
[my_flavor.ok_or_else]
method = """
.ok_or_else(|| {
format!("[Line: {}, {}]\n{}",
$line,
$nth,
::hooq::summary!($source),
)
})
.$so_far"""
詳細はフレーバーページにて解説していますが、この時の設定項目の意味は次の通りです。
| 設定項目 | 効果 |
|---|---|
method | フックするメソッドを設定 |
hook_targets | フック対象を3種から設定 |
tail_expr_idents | 末尾式 / return にてこの設定項目の識別子(ident)である時、フックを行う |
result_types | #[hooq] 対象がここに指定された型の関数である時、返り値はフック対象であるとみなされる |
my_flavor.ok_or_else というテーブルは、 my_flavor の サブフレーバー になります。設定項目を my_flavor から引き継ぎつつ、新たに設定を上書きすることが可能です。
設定可能なすべての項目と設定値による挙動(優先度等)については、属性とフレーバーのページをそれぞれ参照してください。
設定したフレーバーはソースコードから利用できるようになります。
use hooq::hooq;
#[hooq(my_flavor)]
fn display_name_by_mista(val: &toml::Value) -> Result<(), String> {
// Method can be overridden by the one in flavor!
#[hooq::method = my_flavor::ok_or_else]
let name = val.get("package")?.get("name")?.as_str()?;
if name.contains("4") {
return Err(format!(
"name `{name}` contains '4'. Guido Mista disallow this."
));
}
println!("Mista「name: {name}」");
Ok(())
}
#[hooq(my_flavor)]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());
let cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;
display_name_by_mista(&cargo_toml)?;
Ok(())
}
#[hooq::method = フレーバー名] という記法が出てきました。こちらはフレーバーの設定項目を部分適用するための機能です。その他の設定項目に関しても同様に部分適用が可能です。詳しくは属性の方を参照ください。
言い換えると、 #[hooq(フレーバー名)] で指定したフレーバーのmethodには $so_far を含めることはできず、 #[hooq(my_flavor::ok_or_else)] はフレーバーの指定方法としては正しいのですがコンパイルエラーになってしまいます。
#[hooq::method = my_flavor::ok_or_else] は上書きに当たるため .$so_far の利用が可能です。
まとめ
3つのレッスンを通してhooqの基本的な使い方を紹介してきました。
レッスンでは触れられなかった細かい仕組みや仕様がまだまだあるので、気になった機能の紹介はぜひ個別のドキュメントを参照いただきたいです。
他手段との比較
すべての関数に #[hooq] (や #[hooq(anyhow)] ) マクロを付与すると、エラーのスタックトレースに近いものを得られることを本チュートリアルで紹介しました!
本節ではおまけとして、スタックトレース(に近いもの)を得る他の手段との比較表を以下に示します。
凡例:
- 🌈: とても良い
- ✅: 良い
- ⚠️: あまり良くない
- ❌: 良くない
比較表解説:
- 学習コスト・自由度
- ⚠️
BacktraceはRUST_LIB_BACKTRACE=1環境変数の定義が必要な上、OSスレッドに依存するため利用にはRustの制御フローとは別の知識が求められます。 - ⚠️
tracingはスタックトレースの取得を目的とした場合は過剰です。とはいえ、慣れていれば程よい選択肢と言えます。 - 🌈
hooqは関数の頭にマクロを付けるだけです!
- ⚠️
- 型定義の容易さ:
- ⚠️
Backtraceをthiserrorと併用する場合、予めエラー型のフィールドに含める必要があります。エラーを細分化しているほど後付けが大変になるか、あるいはエラー型の表現がシンプルではなくなるでしょう。 - ✅
tracingには特に制約がないです。 - ✅
hooqも、任意のエラーハンドリングクレートと相性が良いです!
- ⚠️
- マクロレス:
- 🌈
Backtraceはマクロを利用しなくて良いのが利点です! - ❌
tracingで手軽にスタックトレース相当の情報を得るには、#[tracing::instrument(err)]等がほぼ必須です。 - ❌
hooqは属性マクロクレートなので、マクロを使いたくない場合には利用できません。
- 🌈
- 情報量制御:
- ⚠️
Backtraceが出力する通常のバックトレースは情報量が多すぎます😵- 生のバックトレースは、非同期の場合全く役に立ちませんし、多くの場合では過剰でしょう。
- ただし、
color-eyreクレートを利用することで改善され、実用的になり得ます。
- ✅
tracingは非同期の場合でも関数をたどることができます。一方、「何行目の?演算子か?」といった詳細な情報を得るには手動でロギングを入れるしかありません。 - 🌈
hooqは (#[tracing::instrument]と同様に)#[hooq]を付けた関数についてのみトレースされるのでほしい箇所だけ的確に得られます。その上、?演算子やreturnの位置まで取得でき、より細かい情報を得られます!- 属性マクロなので、test時や特定のfeatureが有効な時だけ
#[cfg_attr(..., hooq(...))]で条件付き付与、といったことが可能です! - 💡
tracingと併用が可能なので、tracingの情報取得粒度を増やす使い方もできます!詳しくはフレーバーの tracing を見てください
- 属性マクロなので、test時や特定のfeatureが有効な時だけ
- ⚠️
- プラットフォームサポート:
- ⚠️
Backtraceはプラットフォームによっては利用不可であることが公式ドキュメントに記述されています。 - ✅ 通常のログ収集用途であればプラットフォームによる制約はないでしょう。
- 🌈
?にメソッドを挿入するだけなので、プラットフォーム依存の機能に頼ることはありません。各プラットフォームで工夫した使い方が可能です。- 💡
#[hooq::method(.unwrap()!)]で、?をunwrapのエイリアスとして利用する方法などもあります!
- 💡
- ⚠️
リファレンス
本節ではチュートリアルでは取り扱わなかった詳細部分を説明します。
目次
| ページ | 概要 |
|---|---|
| 属性 | #[hooq(...)] や #[hooq::属性(...)] などhooqに存在する属性に関する説明をします。主にhooqマクロの挙動に影響します。 |
| メソッド | フックされるメソッドに関する説明をします。 |
| メタ変数 | フックされるメソッド内で使用できるメタ変数に関する説明をします。主にデバッグ・ロギングに役立つフックのメタ情報を扱えます。 |
| フレーバー | 属性での煩雑な設定をまとめ上げる機能に関する説明をします。組み込みのフレーバーとユーザーが設定できるフレーバーがあります。 |
| features | hooqクレートが持つfeatureに関する説明をします。 |
属性
hooqマクロの挙動は、 #[hooq(ルートメタ)] におけるルートメタおよび、途中に挿入される不活性属性 ( #[hooq::属性(...)] ) によって変更できます。
本ページでは属性による設定可能項目および設定値のデフォルトを解説します。
クイックリファレンス
| 名前 | 種別 | 説明 |
|---|---|---|
| flavor | マクロルートのメタ | 指定したフレーバーの設定を適用する |
| trait_use | マクロルートのメタ | 指定したパス( XXX )について use XXX as _; をアイテムの前に挿入する |
| method | 不活性属性 | 挿入/置換するメソッド(置換の場合は式)を設定する |
| skip_all / skip | 不活性属性 | 本属性を付与した式へのフックは行わないようになる |
| hook_targets | 不活性属性 | ?, return, 末尾式(tail_expr)それぞれについてフックを行うかを切り替え(デフォルトは3種すべてにフック) |
| tail_expr_idents | 不活性属性 | 末尾式に来た時にフックを行うidentを指定(デフォルトでは Err ) |
| ignore_tail_expr_idents | 不活性属性 | フック対象であった場合でもフックを行わないidentを指定(デフォルトでは Ok ) |
| result_types | 不活性属性 | return と末尾式にフックを行う関数の返り値型を指定(デフォルトは Result ) |
| hook_in_macros | 不活性属性 | マクロ内にもフックを行うかを指定(デフォルトは true ) |
| binding | 不活性属性 | 指定したリテラル・式で置換されるメタ変数を作成 |
- マクロルートのメタ: 属性マクロ本体
#[hooq(...)]の...部分に指定する属性(メタ) - 不活性属性: 属性マクロ本体(
#[hooq],#[hooq(...)])ではなく、その後に来るアイテムの随所に付与される属性。#[hooq::属性(...)]のようなフォーマットで指定
すべての属性を付与してみたソースコードは以下のような感じです。
use hooq::hooq;
mod sub {
pub trait Trait {}
}
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq(flavor = "hook", trait_use(sub::Trait))] // Attribute macro root.
#[hooq::method(.inspect_err(|_| { let _ = "error!"; }))] // All following attributes are inert.
#[hooq::hook_targets("?", "return", "tail_expr")]
#[hooq::tail_expr_idents("Err")]
#[hooq::ignore_tail_expr_idents("Ok")]
#[hooq::result_types("Result")]
#[hooq::hook_in_macros(true)]
#[hooq::binding(xxx = "xxx_value")]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
#[hooq::skip_all]
if failable(false)? {
failable(())?;
}
#[hooq::skip]
if failable(false)? {
// Next line is not skipped.
failable(())?;
}
#[hooq::method(.inspect_err(|_| { let _ = $xxx; }))]
failable(())?;
Ok(())
}
上記のソースコードでマクロが展開されると次のようになります。
use hooq::hooq;
mod sub {
pub trait Trait {}
}
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[allow(unused)]
use sub::Trait as _;
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _ = "error!";
})?;
if failable(false)? {
failable(())?;
}
if failable(false)? {
failable(())
.inspect_err(|_| {
let _ = "error!";
})?;
}
failable(())
.inspect_err(|_| {
let _ = "xxx_value";
})?;
Ok(())
}
ルートメタ
hooq属性マクロ( #[hooq] )をアイテムに付与する際に、メタ部分で指定できる事項が2つ存在します。
| 名前 | 付与方法 |
|---|---|
| flavor | #[hooq(FLAVOR_NAME)] or #[hooq(flavor = "FLAVOR_NAME")] |
| trait_use | #[hooq(trait_use(TRAIT_PATH, ...))] or #[hooq(trait_uses(TRAIT_PATH, ...))] |
解説は各項でします。
flavor
#[hooq(FLAVOR_NAME)] または #[hooq(flavor = "FLAVOR_NAME")] というフォーマットでベースとなる設定をフレーバーから設定します。
use hooq::hooq;
#[hooq(my_flavor)]
fn main() -> Result<(), Box<dyn std::error::Error>> {
func()?;
Ok(())
}
#[hooq(my_flavor::sub_flavor)]
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq(flavor = "my_flavor.sub_flavor")]
fn func() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
Ok(())
}
hooq.toml の内容は以下であるとします。
[my_flavor]
method = """.inspect(|_| {
let _ = $tag;
})"""
bindings = { tag = "\"my_flavor\"" }
[my_flavor.sub_flavor]
bindings = { tag = "\"my_flavor.sub_flavor\"" }
tail_expr_idents = ["Ok"]
この時フレーバーの設定が読まれ展開後は以下のようになります。
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn main() -> Result<(), Box<dyn std::error::Error>> {
func()
.inspect(|_| {
let _ = "my_flavor";
})?;
Ok(())
}
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
.inspect(|_| {
let _ = "my_flavor.sub_flavor";
})
}
fn func() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect(|_| {
let _ = "my_flavor.sub_flavor";
})?;
Ok(())
.inspect(|_| {
let _ = "my_flavor.sub_flavor";
})
}
例ではサブフレーバーも利用しています。サブフレーバーは、元となるフレーバーに上書きで設定を施したフレーバーです。
サブフレーバーは文字列の場合 . か ::、パスのような形で直接渡す場合 :: で区切られた名前空間で表し、ネストさせる(つまり、サブフレーバーにさらにサブフレーバーを設ける)ことができます。
フレーバーのさらなる情報、特にanyhowなどのhooqが予め用意しているフレーバーや、hooq.tomlへの書き方等についてはフレーバーのページを参照してください。
trait_use
#[hooq(trait_use(トレイトパス, ...))] というフォーマットで指定します。 trait_use ではなく trait_uses でも構いません1。この時指定されたトレイトパスに対して #[allow(unused)] use トレイトパス as _; というuse文が、 #[hooq(...)] を付与したアイテムの前に出力されます。
use hooq::hooq;
mod sub {
pub trait Inserted {
fn inserted(self) -> Self;
}
impl<T, E> Inserted for Result<T, E> {
fn inserted(self) -> Self {
self
}
}
}
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq(trait_use(sub::Inserted))]
#[hooq::method(.inserted())]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
Ok(())
}
展開後の main 関数とその上に出力されるuse文を抜粋すると次のようになっています。
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
mod sub {
pub trait Inserted {
fn inserted(self) -> Self;
}
impl<T, E> Inserted for Result<T, E> {
fn inserted(self) -> Self {
self
}
}
}
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[allow(unused)]
use sub::Inserted as _;
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(()).inserted()?;
Ok(())
}
例で示したように、 trait_use はフックするメソッドを呼び出すのに必要なトレイトを一緒に出力する目的で設けています。もちろんこの属性指定を利用せず直接use文を書いてしまっても構いませんが、例えば #[cfg_attr(test, hooq(flavor = "test", trait_use(Trait)))] のようにコンパイル条件付きでhooqの利用をしたい場合などに、記述を多少見やすくする効果が期待できます。
あるいは、hooq.tomlでも trait_use は(trait_uses フィールドで)指定できるため、 システム上一応属性でも設定可能にしたとも言えます。例えばanyhowフィーチャーでは with_context メソッドを挿入しますが、この時同時に必要なトレイトである anyhow::Context をuse文で導入しています2。
method
| 名前 | 付与方法 |
|---|---|
| method | #[hooq::method(...)] |
#[hooq::method(.method_name())] のようなフォーマットで、フックするメソッドを設定できる不活性属性です。
use hooq::hooq;
#[hooq]
#[hooq::method(.inspect_err(|_| { let _ = "specified @ root"; }))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
#[hooq::method(.inspect_err(|_| { let _ = "specified @ inner"; }))]
failable(())?;
Ok(())
}
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
上の例のように、「 #[hooq] マクロのすぐ下」・「関数内部」のどちらでも設定の変更を行うことができます。これは以降紹介する不活性属性で共通の性質です。 main 関数は以下のように展開されます。
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _ = "specified @ root";
})?;
failable(())
.inspect_err(|_| {
let _ = "specified @ inner";
})?;
Ok(())
}
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq::method(...)] には 挿入モード と 置換モード の2つのモードがあります。
.(ドット)で始まる場合は挿入モードです。対象の式と?の間(あるいはreturnや末尾式なら単に式の末尾)にメソッドを挿入します。- 上記以外は置換モードになり、与えられた式によって対象の式を置換します。対象の式は
$exprメタ変数で得られるので、例えばfn func<T, E>(_r: Result<T, E>) {}などに対して#[hooq::method(func($expr))]のように書くことで関数で対象式をラップするといった記述が可能です。
その他、フックするメソッド内では $line や $source といった メタ変数 を利用することが可能です。
ユーザーが特に何も指定しない場合、defaultフレーバーの設定値である以下のメソッドが挿入されます。
.inspect_err(|e| {
let path = $path;
let line = $line;
let col = $col;
let expr = ::hooq::summary!($source);
::std::eprintln!("[{path}:{line}:{col}] {e:?}\n{expr}");
})
モードやメタ変数に関する詳細はメソッドとメタ変数のページを参照してください。
フックの付与をスキップする
| 名前 | 付与方法 |
|---|---|
| skip_all | #[hooq::skip_all] |
| skip | #[hooq::skip] |
skip_all
hooqは(デフォルトの設定では)かなり貪欲にメソッドを ? 等にフックしてきます。「この ? にはフックしたくない!」といった要望は自然に起きると思います。そのような場合はフック付与を行いたくない式・文に #[hooq::skip_all] を付けてください。
use hooq::hooq;
#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
#[hooq::skip_all]
let f = || -> Option<()> {
optional(())?; // If the hook is applied, an compile error occurs.
Some(())
};
let _ = failable(f())?;
Ok(())
}
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn optional<T>(val: T) -> Option<T> {
Some(val)
}
付与した式とその内部にはフックされなくなります。
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(()).inspect_err(|_| {})?;
let f = || -> Option<()> {
optional(())?;
Some(())
};
let _ = failable(f()).inspect_err(|_| {})?;
Ok(())
}
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn optional<T>(val: T) -> Option<T> {
Some(val)
}
skip
#[hooq::skip] は滅多に利用することはないであろう不活性属性になりますが、一応用意した #[hooq::skip_all] の亜種になります。
skip_allでは付与対象全体でフックがスキップされましたが、skipでは 付与した(親)スコープでのみ フックのスキップが起きます。
skipは、末尾式がネストしてしまっており、そのままロギングをフックするとログが見辛くなってしまう場合等に有用です。
use hooq::hooq;
#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
fn func1() -> Result<(), String> {
match failable(failable(()))? {
Ok(()) => Ok(()),
Err(s) => Err(s),
}
}
#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
fn func2() -> Result<(), String> {
#[hooq::skip]
match failable(failable(()))? {
Ok(()) => Ok(()),
Err(s) => Err(s),
} // Not hooked here.
}
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() {
let _ = func1();
let _ = func2();
}
#[hooq::skip] がついている func2 の方ではmatch式の外につく inspect_err がなくなっていることがわかります。
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn func1() -> Result<(), String> {
match failable(failable(())).inspect_err(|_| {})? {
Ok(()) => Ok(()),
Err(s) => Err(s).inspect_err(|_| {}),
}
.inspect_err(|_| {})
}
fn func2() -> Result<(), String> {
match failable(failable(()))? {
Ok(()) => Ok(()),
Err(s) => Err(s).inspect_err(|_| {}),
}
}
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() {
let _ = func1();
let _ = func2();
}
フック対象判定を制御する
以下は各式に対して、フックするかしないかをカスタマイズするための不活性属性群になります。
| 名前 | 付与方法 | ... の部分が取りうる値 |
|---|---|---|
| hook_targets | #[hooq::hook_targets(...)] | ? or return or tail_expr の中から複数 |
| tail_expr_idents | #[hooq::tail_expr_idents(...)] | Err など |
| ignore_tail_expr_idents | #[hooq::ignore_tail_expr_idents(...)] | Ok など |
| result_types | #[hooq::result_types(...)] | Result など |
| hook_in_macros | #[hooq::hook_in_macros(...)] | true or false |
設定値詳細は各項目にて示します。ここには、設定の適用優先順位を記します3。
skip_allが付与されている場合はフックしないskipの場合は子スコープを除いた同スコープ内についてのみフックしない
- 対象式がマクロ呼び出し内部にあり、かつ
hook_in_macrosがfalseである場合はフックしない ?へのフックの場合hook_targetsに?が含まれていればフックする
returnへのフックの場合hook_targetsにreturnが含まれていなければフックしない- 返り値の識別子が
tail_expr_identsに含まれていればフックする - 関数の返り値型が
result_typesに含まれ、かつ返り値の識別子がignore_tail_expr_identsに含まれない場合フックする
- 末尾式へのフックの場合
hook_targetsにtail_exprが含まれていなければフックしない- 末尾式の識別子が
tail_expr_identsに含まれていればフックする - 関数・クロージャが持つブロックの末尾式であり、かつその関数・クロージャの返り値型が
result_typesに含まれ、かつ返り値の識別子がignore_tail_expr_identsに含まれない場合フックする
hook_targets
? 演算子(Question Operator)、 return、 末尾式( tail_expr )の3つについて、それぞれフックするかしないかを指定できます。デフォルトでは3種類すべてフックされます。
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
#[hooq::hook_targets("?")]
fn target_question() -> Result<(), String> {
failable(())?;
if failable(false)? {
return Err("error".into());
}
if failable(true)? {
Ok(())
} else {
Err("error".into())
}
}
#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
#[hooq::hook_targets("return")]
fn target_return() -> Result<(), String> {
failable(())?;
if failable(false)? {
return Err("error".into());
}
if failable(true)? {
Ok(())
} else {
Err("error".into())
}
}
#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
#[hooq::hook_targets("tail_expr")]
fn target_tail_expr() -> Result<(), String> {
failable(())?;
if failable(false)? {
return Err("error".into());
}
if failable(true)? {
Ok(())
} else {
Err("error".into())
}
}
fn main() {
let _ = target_question();
let _ = target_return();
let _ = target_tail_expr();
}
展開結果は次の通りとなります。
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn target_question() -> Result<(), String> {
failable(()).inspect_err(|_| {})?;
if failable(false).inspect_err(|_| {})? {
return Err("error".into());
}
if failable(true).inspect_err(|_| {})? { Ok(()) } else { Err("error".into()) }
}
fn target_return() -> Result<(), String> {
failable(())?;
if failable(false)? {
return Err("error".into()).inspect_err(|_| {});
}
if failable(true)? { Ok(()) } else { Err("error".into()) }
}
fn target_tail_expr() -> Result<(), String> {
failable(())?;
if failable(false)? {
return Err("error".into());
}
if failable(true)? { Ok(()) } else { Err("error".into()).inspect_err(|_| {}) }
.inspect_err(|_| {})
}
fn main() {
let _ = target_question();
let _ = target_return();
let _ = target_tail_expr();
}
今回は個別に書きましたが、 #[hooq::hook_targets("?", "return")] のように一つだけ抜く書き方等ももちろんできます。
tail_expr_idents
return の返り値あるいは末尾式が指定した識別子である際は、関数の返り値型が result_types に含まれるかに関わらずフックを行います。デフォルトは Err で、カンマ区切りで複数指定可能です。
識別子にはパス( xxx::yyy::Zzz )は認められず、単体( Zzz )である必要があります。
一致に関しては、パス中で最後の識別子(xxx::yyy::Zzz なら Zzz)で判別されます。
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
fn main() -> Result<(), String> {
let _: Result<(), String> = {
let _: Result<(), String> = {
let res = "error".to_string();
Err(res)
};
failable(())
};
#[hooq::tail_expr_idents("Err", "failable")]
let _: Result<(), String> = {
let _: Result<(), String> = {
let res = "error".to_string();
Err(res)
};
failable(()) // This will be hooked because of tail_expr_idents.
};
Ok(())
}
tail_expr_idents のおかげで、ブロックの返り値などに Err が含まれる場合、そこにもフックが行われるようになります。 Err 以外にこのような性質を持たせたい識別子がある際は tail_expr_idents に加えることで同様の挙動になります。
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), String> {
let _: Result<(), String> = {
let _: Result<(), String> = {
let res = "error".to_string();
Err(res).inspect_err(|_| {})
};
failable(())
};
let _: Result<(), String> = {
let _: Result<(), String> = {
let res = "error".to_string();
Err(res).inspect_err(|_| {})
};
failable(()).inspect_err(|_| {})
};
Ok(())
}
ignore_tail_expr_idents
tail_expr_idents とは対照的に、 return の返り値あるいは末尾式が指定した識別である際は関数の返り値型が result_types に含まれている場合でも フックを行いません 。デフォルトは Ok で、カンマ区切りで複数指定可能です。
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
fn main() {
let f = || -> Result<(), String> { failable(()) };
#[hooq::ignore_tail_expr_idents("failable")]
let g = || -> Result<(), String> { failable(()) };
#[hooq::tail_expr_idents("!failable")]
let h = || -> Result<(), String> { failable(()) };
f().unwrap();
g().unwrap();
h().unwrap();
}
上記例にあるように、 ignore_tail_expr_idents を利用せずとも、 tail_expr_idents への指定において頭に ! (Exclamation Mark) を付けることでも同様の設定を行うことが可能です。展開結果は以下の通りとなります。
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() {
let f = || -> Result<(), String> { failable(()).inspect_err(|_| {}) };
let g = || -> Result<(), String> { failable(()) };
let h = || -> Result<(), String> { failable(()) };
f().unwrap();
g().unwrap();
h().unwrap();
}
同じ識別子が tail_expr_idents と ignore_tail_expr_idents の両方に含まれる場合、機構が単純なため フックされてしまいます。なるべく ! (Exclamation Mark) を利用して tail_expr_idents 経由で設定した方が確実です。
Errの例
Err はデフォルトで tail_expr_idents に含まれるので、 ignore_tail_expr_idents で指定してもフックされてしまいます。
use hooq::hooq;
#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
fn main() {
let f = || -> Result<(), String> { Err("error!".to_string()) };
#[hooq::ignore_tail_expr_idents("Err")]
let g = || -> Result<(), String> { Err("error!".to_string()) };
#[hooq::tail_expr_idents("!Err")]
let h = || -> Result<(), String> { Err("error!".to_string()) };
f().unwrap_err();
g().unwrap_err();
h().unwrap_err();
}
クロージャ g ではフックしてほしくないのにフックされていることが確認できます。
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn main() {
let f = || -> Result<(), String> { Err("error!".to_string()).inspect_err(|_| {}) };
let g = || -> Result<(), String> { Err("error!".to_string()).inspect_err(|_| {}) };
let h = || -> Result<(), String> { Err("error!".to_string()) };
f().unwrap_err();
g().unwrap_err();
h().unwrap_err();
}
result_types
hooqマクロを付与した関数の返り値がフック対象であるかを判別するための識別子を設定する不活性属性です。関数の返り値型の識別子が result_types で指定した識別子と一致する時、 return や末尾式でフックを行うようになります。デフォルトは Result で、カンマ区切りで複数指定可能です。
tail_expr_idents等と同様に、識別子にはパス( xxx::yyy::Zzz )は認められず、単体( Zzz )である必要があります。
一致に関しては、パス中で最後の識別子(xxx::yyy::Zzz なら Zzz)で判別されます。
Result 型以外に独自で別な型を扱う際などに有用です。
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
type MyResult = Result<(), String>;
#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
fn func1() -> MyResult {
let _ = || -> Result<(), String> { failable(()) };
failable(())
}
#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
#[hooq::result_types("MyResult")]
fn func2() -> MyResult {
// No longer hooked.
let _ = || -> Result<(), String> { failable(()) };
failable(())
}
#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
#[hooq::result_types("Result", "MyResult")]
fn func3() -> MyResult {
let _ = || -> Result<(), String> { failable(()) };
let _ = || {
// Not hooked because return type of the closure is unknown.
failable(())
};
failable(())
}
fn main() {
let _ = func1();
let _ = func2();
let _ = func3();
}
展開結果は次のようになります。
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
type MyResult = Result<(), String>;
fn func1() -> MyResult {
let _ = || -> Result<(), String> { failable(()).inspect_err(|_| {}) };
failable(())
}
fn func2() -> MyResult {
let _ = || -> Result<(), String> { failable(()) };
failable(()).inspect_err(|_| {})
}
fn func3() -> MyResult {
let _ = || -> Result<(), String> { failable(()).inspect_err(|_| {}) };
let _ = || { failable(()) };
failable(()).inspect_err(|_| {})
}
fn main() {
let _ = func1();
let _ = func2();
let _ = func3();
}
hook_in_macros
関数風マクロ呼び出し内に存在する対象の式に対し、フックを行うかを決定します。デフォルトは true でありマクロ呼び出し内までフックが行われます。
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("{}", failable("hello")?);
#[hooq::hook_in_macros(false)]
println!("{}", failable("world")?);
Ok(())
}
展開結果は次の通りです。 println! マクロも展開されている点に注意してください。
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
{
::std::io::_print(format_args!("{0}\n", failable("hello").inspect_err(|_| {})?));
};
{
::std::io::_print(format_args!("{0}\n", failable("world")?));
};
Ok(())
}
関数風マクロの引数部分はRustの文法に従った構文になっているとは限らず、フックを行う場合に解析に少しコストがかかるためオフにできる不活性属性を設けました。マクロの内側までフックする必要がない場合はこちらの設定を false にすることで多少コンパイル時間が短くなるかもしれません(多分)。
binding
メタ変数 において、ユーザーが自由に式を保存できる バインディング 機能があります。同じ意味を表すいくつかの書き方があります。
| 付与方法 | 備考 |
|---|---|
#[hooq::binding(xxx = ...)] | |
#[hooq::var(xxx = ...)] | |
#[hooq::xxx = ...] | この方法で記述する場合 xxx は他不活性属性と名前衝突不可 |
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| { let _ = $xxx; }))]
#[hooq::xxx = 10]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
#[hooq::binding(xxx = "in block")]
{
failable(())?;
#[hooq::var(xxx = 42)]
failable(())?;
failable(())?;
}
failable(())?;
Ok(())
}
展開結果は次のようになります。
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _ = 10;
})?;
{
failable(())
.inspect_err(|_| {
let _ = "in block";
})?;
failable(())
.inspect_err(|_| {
let _ = 42;
})?;
failable(())
.inspect_err(|_| {
let _ = "in block";
})?;
}
failable(())
.inspect_err(|_| {
let _ = 10;
})?;
Ok(())
}
バインディング については当該ページの方も参照してください。
flavor を利用した設定の部分適用
ここまで紹介してきた不活性属性による設定は、フレーバーを用いた部分適用が可能です。部分適用は #[hooq::属性 = フレーバー名] で行います。
また、すべての設定を不活性属性で上書きしたい場合は #[hooq::flavor = フレーバー名]、存在するユーザー定義のバインディングを上書きしたい場合は #[hooq::bindings = フレーバー名] という記法が使えます。
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
type MyResult = Result<(), String>;
#[hooq]
#[hooq::method(.inspect_err(|_| {
let _x = $xxx;
let _y = $yyy;
}))]
#[hooq::xxx = "from root"]
#[hooq::yyy = "from root"]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
// Not hooked.
let _ = || -> MyResult { failable(()) };
#[hooq::method = "my_flavor"]
// Method will be changed.
failable(())?;
#[hooq::result_types = "my_flavor"]
// Hooked now.
let _ = || -> MyResult { failable(()) };
#[hooq::bindings = "my_flavor"]
// Bindings will be changed.
failable(())?;
#[hooq::flavor = "my_flavor"]
// All will be changed.
failable(())?;
Ok(())
}
hooq.tomlの内容は以下である時、
[my_flavor]
method = """.inspect_err(|_| {
let _ = "from my_flavor";
let _x = $xxx;
let _y = $yyy;
})"""
result_types = ["Result", "MyResult"]
bindings = { xxx = "\"xxx from my_flavor\"", yyy = "\"yyy from my_flavor\"" }
展開結果は次のようになります。
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
type MyResult = Result<(), String>;
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _x = "from root";
let _y = "from root";
})?;
let _ = || -> MyResult { failable(()) };
failable(())
.inspect_err(|_| {
let _ = "from my_flavor";
let _x = "from root";
let _y = "from root";
})?;
let _ = || -> MyResult {
failable(())
.inspect_err(|_| {
let _x = "from root";
let _y = "from root";
})
};
failable(())
.inspect_err(|_| {
let _x = "xxx from my_flavor";
let _y = "yyy from my_flavor";
})?;
failable(())
.inspect_err(|_| {
let _ = "from my_flavor";
let _x = "xxx from my_flavor";
let _y = "yyy from my_flavor";
})?;
Ok(())
}
-
開発中に表記ゆれが起きてしまい両方残していますが、機能的には変わりません ↩
-
置換モードを用いて書き方を工夫すれば実はトレイトのuseは不要だったりしますが、hooqマクロでは人間が良く書く直感的な記法の方を挿入する形を採用しています。 ↩
-
フローチャートで示したかったのですがmdbook-mermaidがうまく機能しなかったため普通に箇条書きで示しています。 ↩
メソッド
不活性属性 #[hooq::method(...)] やフレーバーを通して、hooqによってフックされるメソッドを指定することが可能です。 ... の部分に指定するメソッドには2つのモードがあります。
#[hooq::method(.inspect_err(|_| ...))] のように . 始まりで記述されている場合、 挿入モード となり、付与対象の式と ? の間に式が挿入されるようになります。 #[hooq::method(wrapper($expr))] のように . が冒頭にはない場合、 置換モード となり、付与対象の式が指定されたメソッド(というよりは変換後の式)に置換されます。
挿入/置換先の式表現には、 $ マークで始まるメタ変数が使えます。詳細はメタ変数のページを確認してください。
挿入モード
. (ドット)で始まる場合は内容がメソッドであると判断し、設定されたメソッドを挿入します。hooqの基本的なモードです。
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {
let _ = "inserted mode";
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
#[hooq::method(.inspect_err(|_| {
let _ = "before chainned";
}).$so_far)]
failable(())?;
#[hooq::method(.$so_far.inspect_err(|_| {
let _ = "after chainned";
}))]
failable(())?;
Ok(())
}
.$so_far を使うことで、それ以前に設定されたメソッドにつなげたり、逆にそのメソッドを後ろに付けたりすることも可能です1。
展開結果は次のようになります。
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _ = "inserted mode";
})?;
failable(())
.inspect_err(|_| {
let _ = "before chainned";
})
.inspect_err(|_| {
let _ = "inserted mode";
})?;
failable(())
.inspect_err(|_| {
let _ = "inserted mode";
})
.inspect_err(|_| {
let _ = "after chainned";
})?;
Ok(())
}
置換モード
. (ドット)で始まらない場合は内容が置換される関数であると判断し、設定された式で元の式を置換します。 置換される対象の式は $expr メタ変数を利用してアクセスします。( $expr を使わずに書くことも可能ですが、おそらく役には立たないでしょう。)
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn wrapper<T, E>(r: Result<T, E>) -> Result<T, E>
where
E: std::fmt::Debug,
{
if let Err(e) = &r {
println!("Error occurred: {:?}", e);
}
r
}
#[hooq]
#[hooq::method(wrapper($expr))]
fn main() -> Result<(), String> {
failable(())?;
Ok(())
}
展開された main 関数内では対象の式が置換されています。
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn wrapper<T, E>(r: Result<T, E>) -> Result<T, E>
where
E: std::fmt::Debug,
{
if let Err(e) = &r {
{
::std::io::_print(format_args!("Error occurred: {0:?}\n", e));
};
}
r
}
fn main() -> Result<(), String> {
wrapper(failable(()))?;
Ok(())
}
置換モードでは $expr を利用してください!
$expr メタ変数も $source メタ変数もともにフックされる元の式を表します。ただし、 $expr メタ変数は置換モード等で元の式を参照するために、 $source メタ変数はデバッグやロギングにおいてフックされる式を参照するためにそれぞれ用います。
両者の違いは、式の内側へのフックの有無です。 $expr は内側へのフックを済ませてから再帰的に現在のフック処理に渡されているため、フックがない元々の式にはなっていません。一方で $source はフックが施される前の元々の式を表すので、置換モードにて(ロギング文字列を得る目的以外で)利用するとフックが適切に行われなくなります。
! (Exclamation Mark) の末尾付与による ? (Question Operator) の削除
? 演算子 (Question Operator) へフックする場合に限り、フックするメソッドの末尾に ! (Exclamation Mark) を付与すると末尾の ? 演算子を取り除く機能があります。
本機能を利用するには
consume-questionfeatureが必要 です。
cargo add hooq --features consume-question
例えば以下のように ? を .unwrap() として扱いたい場合などに便利です。
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, &'static str> {
Ok(val)
}
#[hooq]
#[hooq::hook_targets("?")]
#[hooq::method(.unwrap()!)]
fn main() {
failable(())?;
}
展開結果は次のようになります。
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, &'static str> {
Ok(val)
}
fn main() {
failable(()).unwrap();
}
-
$so_farは前回のフックが挿入モードの場合先頭のドットを落としてメソッドを保存しているので、.を先頭に付ける仕組みになっています。チェインが視覚的にわかるためこのようにしています。 ↩
メタ変数
#[hooq::method(...)] の ... 内部では特殊な値に置換される メタ変数 を利用することが可能です。メタ変数は $ で始まります。
クイックリファレンス
| 名前 | リテラル種別 | 説明 |
|---|---|---|
$line | usize整数 | フック対象がある行番号 |
$column or $col | usize整数 | フック対象がある列番号 |
$path | 文字列 | フック対象があるファイルの相対パス |
$file | 文字列 | フック対象があるファイルの名前 |
$source | 式 | デバッグ・ロギング用に用いる、挿入/置換対象の式 ( $expr との違いに注意 ) |
$count or $nth | 文字列 | 何番目の置換対象であるかを表示 |
$fn_name or $fnname | 文字列 | フック対象がある関数の名前 |
$fn_sig or $fnsig | 文字列 | フック対象がある関数のシグネチャ |
$xxx (一例) | (任意) | #[hooq::xxx = ...] という不活性属性によるユーザー定義のメタ変数 |
$bindings or $vars | HashMap | メタ変数バインディングすべて |
$hooq_meta or $hooqmeta | hooq::HooqMeta | $line・$col・$path・$file・$source・$count・$bindings をひとまとめにした構造体を表す |
$expr | 式 | 置換用に用いる、置換対象の式 ( $source との違いに注意 ) |
$so_far or $sofar | 式 | 主に挿入用に用いる、これまでに設定されているフック |
$expr、 $so_far は特殊なフックを作るため一旦除くとして、それ以外のすべてのメタ変数を利用した例は以下になります。
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::xxx = "user defined binding."]
#[hooq::method(.inspect_err(|_| {
// Fundamental information provided by hooq.
let _line = $line;
let _column = $column;
let _path = $path;
let _file = $file;
let _source = stringify!($source);
let _count = $count;
let _fn_name = $fn_name;
let _fn_sig = $fn_sig;
// Meta vars defined by user.
let _xxx = $xxx;
let _bindings = $bindings;
// All information summarized up to this point.
let _hooq_meta = $hooq_meta;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
Ok(())
}
展開されるとこのようになります。
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _line = 28usize;
let _column = 17usize;
let _path = "mdbook-source-code/meta-vars-all/src/main.rs";
let _file = "main.rs";
let _source = "failable(()) ?";
let _count = "1st ?";
let _fn_name = "main";
let _fn_sig = "fn main() -> Result < (), Box < dyn std :: error :: Error > >";
let _xxx = "user defined binding.";
let _bindings = ::std::collections::HashMap::from([
(
::std::string::ToString::to_string("xxx"),
{
let expr = ::std::string::ToString::to_string(
"\"user defined binding.\"",
);
let value: ::std::rc::Rc<dyn ::std::any::Any> = ::std::rc::Rc::new(
"user defined binding.",
);
::hooq::BindingPayload {
expr,
value,
}
},
),
]);
let _hooq_meta = ::hooq::HooqMeta {
line: 28usize,
column: 17usize,
path: "mdbook-source-code/meta-vars-all/src/main.rs",
file: "main.rs",
source_str: "failable(()) ?",
count: "1st ?",
bindings: ::std::collections::HashMap::from([
(
::std::string::ToString::to_string("xxx"),
{
let expr = ::std::string::ToString::to_string(
"\"user defined binding.\"",
);
let value: ::std::rc::Rc<dyn ::std::any::Any> = ::std::rc::Rc::new(
"user defined binding.",
);
::hooq::BindingPayload {
expr,
value,
}
},
),
]),
};
})?;
Ok(())
}
フック対象の情報を得るためのメタ変数
フック対象が存在する行数・列数・関数名・ファイル名・ファイルパス・フック対象のトークン列・何番目の ? であるか等、フック対象の情報を表すメタ変数を紹介します。
line
| 名前 | リテラル種別 | 説明 |
|---|---|---|
$line | usize整数 | フック対象がある行番号 |
フック対象が何行目にあるかを表すusize整数に置き換えられます。
line!() マクロは正確な行数を表示しないため、行数を得たい場合は $line を利用してください。
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {
let _line = $line;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
Ok(())
}
展開結果:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _line = 12usize;
})?;
Ok(())
}
column
| 名前 | リテラル種別 | 説明 |
|---|---|---|
$column or $col | usize整数 | フック対象がある列番号 |
フック対象が何列目にあるかを表すusize整数に置き換えられます。
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {
let _column = $column;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
Ok(())
}
展開結果:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _column = 17usize;
})?;
Ok(())
}
path
| 名前 | リテラル種別 | 説明 |
|---|---|---|
$path | 文字列 | フック対象があるファイルの相対パス |
フック対象が存在するファイルの、クレートルート( CARGO_MANIFEST_DIR )からの相対パスに置き換えられます。
しかしながら相対パスの起点は、ワークスペースを利用している場合などにクレートルートではない時があります。手続きマクロからはファイルの正確な絶対パスが取れない仕組みになっているらしいため、hooqでは絶対パスを取得するためのメタ変数は設けられていません。
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {
let _path = $path;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
Ok(())
}
展開結果:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _path = "mdbook-source-code/meta-vars-path/src/main.rs";
})?;
Ok(())
}
file
| 名前 | リテラル種別 | 説明 |
|---|---|---|
$file | 文字列 | フック対象があるファイルの名前 |
フック対象が存在するファイルの名前に置き換えられます。
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {
let _file = $file;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
Ok(())
}
展開結果:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _file = "main.rs";
})?;
Ok(())
}
source
| 名前 | リテラル種別 | 説明 |
|---|---|---|
$source | 式 | デバッグ・ロギング用に用いる、挿入/置換対象の式 |
hooqマクロによるフックが一切施される前のフック対象トークン列(式)を得られます。デバッグ用途での使用を想定したメタ変数です。
一方で $expr は内部にすでにフックが施された状態の式です。 $expr は置換モード等で元の式を埋め込む場所を決めるために使用されます。
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {
let _source = stringify!($source);
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
Ok(())
}
展開結果:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _source = "failable(()) ?";
})?;
Ok(())
}
$source メタ変数は単体ではなく以下の文字列化マクロと併用することを想定して作られています。
stringify!マクロhooq::summary!マクロhooq::source_excerpt_helpersモジュール以下にあるマクロ
hooq::summary! マクロを使うと程よく改行が施され、読みやすいエラーメッセージを作成できます。
use hooq::hooq;
#[hooq]
#[hooq::method(.inspect_err(|_| {
let source = ::hooq::summary!($source);
eprintln!("{source}");
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
Err((
"aaaaaaaaaaaaaaaaaaaa",
"bbbbbbbbbbbbbbbbbbbb",
"cccccccccccccccccccc",
"dddddddddddddddddddd",
"errorerrorerrorerrorerror",
)
.4
.into())
}
実行結果(eprintln! による表示部分):
10> Err((
...
15| "erro..rror",
16| )
17| .4
18| .into())
|
count
| 名前 | リテラル種別 | 説明 |
|---|---|---|
$count or $nth | 文字列 | 何番目の置換対象であるかを表示 |
フック対象がそれぞれの種別( ? か、 return か、末尾式か)で関数内において何番目であるかを指し示します。
本メタ変数は $line の取得がnightlyでしか不可能であった時の開発の名残です。 $line の方が基本的にはわかりやすいでしょう。
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {
let _count = $count;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
Ok(())
}
展開結果:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _count = "1st ?";
})?;
Ok(())
}
fn_name
| 名前 | リテラル種別 | 説明 |
|---|---|---|
$fn_name or $fnname | 文字列 | フック対象がある関数の名前 |
フック対象が存在する関数名に置き換わります。ネストされている場合一番内側の関数名を指し、クロージャ内の場合は関数の中のクロージャであることが明記されます。
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {
let _fn_name = $fn_name;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
(|| -> Result<(), String> {
failable(())?;
Ok(())
})()?;
Ok(())
}
展開結果:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _fn_name = "main";
})?;
(|| -> Result<(), String> {
failable(())
.inspect_err(|_| {
let _fn_name = "__closure_in_main__";
})?;
Ok(())
})()
.inspect_err(|_| {
let _fn_name = "main";
})?;
Ok(())
}
fn_sig
| 名前 | リテラル種別 | 説明 |
|---|---|---|
$fn_sig or $fnsig | 文字列 | フック対象がある関数のシグネチャ |
フック対象が存在する関数・クロージャのシグネチャに置き換わります。
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {
let _fn_sig = $fn_sig;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
(|| -> Result<(), String> {
failable(())?;
Ok(())
})()?;
Ok(())
}
展開結果:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _fn_sig = "fn main() -> Result < (), Box < dyn std :: error :: Error > >";
})?;
(|| -> Result<(), String> {
failable(())
.inspect_err(|_| {
let _fn_sig = "| | -> Result < (), String > {}";
})?;
Ok(())
})()
.inspect_err(|_| {
let _fn_sig = "fn main() -> Result < (), Box < dyn std :: error :: Error > >";
})?;
Ok(())
}
ユーザー定義のメタ変数(バインディング)
メタ変数は、組み込み以外の名前ならばユーザーにより定義することが可能です。
#[hooq::xxx = ...] のように不活性属性を用いるか、hooq.tomlファイル内の bindings テーブルに書くことでメタ変数を定義できます。
定義方法の詳細はそれぞれのページを参照してください。
本ページでは不活性属性を用いた例を掲載します。
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
enum CauseKind {
DataBase,
Server,
}
#[hooq]
// Can be defined in the format #[hooq::xxx = value]
#[hooq::string = "hello!"] // string literal
#[hooq::integer = 10] // integer literal
#[hooq::cause_kind = CauseKind::Server] // some value
#[hooq::method(.inspect_err(|_| {
let _string = $string;
let _integer = $integer;
let _cause_kind = $cause_kind;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
// Overriding meta variables.
#[hooq::cause_kind = CauseKind::DataBase]
failable(())?;
Ok(())
}
式であればなんでも設定可能です。ただしその式にそのまま置き換わるだけなので、書き方によっては所有権等の影響を受けることもあるでしょう。
展開結果:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
enum CauseKind {
DataBase,
Server,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _string = "hello!";
let _integer = 10;
let _cause_kind = CauseKind::Server;
})?;
failable(())
.inspect_err(|_| {
let _string = "hello!";
let _integer = 10;
let _cause_kind = CauseKind::DataBase;
})?;
Ok(())
}
bindings
| 名前 | リテラル種別 | 説明 |
|---|---|---|
$bindings or $vars | HashMap | メタ変数バインディングすべて |
ユーザー定義のメタ変数(バインディング)を HashMap<String, BindingPayload> としてすべて取得します。
BindingPayload にはバインディングの式を文字列化したものと、 Rc<dyn Any> を用いた値が保存されています。
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
enum CauseKind {
#[allow(unused)]
DataBase,
Server,
}
#[hooq]
// Can be defined in the format #[hooq::xxx = value]
#[hooq::string = "hello!"] // string literal
#[hooq::integer = 10] // integer literal
#[hooq::cause_kind = CauseKind::Server] // some value
#[hooq::method(.inspect_err(|_| {
let _bindings = $bindings;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
Ok(())
}
展開結果:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
enum CauseKind {
#[allow(unused)]
DataBase,
Server,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _bindings = ::std::collections::HashMap::from([
(
::std::string::ToString::to_string("cause_kind"),
{
let expr = ::std::string::ToString::to_string(
"CauseKind :: Server",
);
let value: ::std::rc::Rc<dyn ::std::any::Any> = ::std::rc::Rc::new(
CauseKind::Server,
);
::hooq::BindingPayload {
expr,
value,
}
},
),
(
::std::string::ToString::to_string("integer"),
{
let expr = ::std::string::ToString::to_string("10");
let value: ::std::rc::Rc<dyn ::std::any::Any> = ::std::rc::Rc::new(
10,
);
::hooq::BindingPayload {
expr,
value,
}
},
),
(
::std::string::ToString::to_string("string"),
{
let expr = ::std::string::ToString::to_string("\"hello!\"");
let value: ::std::rc::Rc<dyn ::std::any::Any> = ::std::rc::Rc::new(
"hello!",
);
::hooq::BindingPayload {
expr,
value,
}
},
),
]);
})?;
Ok(())
}
hooq_meta
| 名前 | リテラル種別 | 説明 |
|---|---|---|
$hooq_meta or $hooqmeta | hooq::HooqMeta | $line・$col・$path・$file・$source・$count・$bindings をひとまとめにした構造体を表す |
hook フレーバー のために設けられたメタ変数で、メタ変数として得られる情報をhooq::HooqMetaとしてまとめます。
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::string = "hello!"]
#[hooq::method(.inspect_err(|_| {
let _hooq_meta = $hooq_meta;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
Ok(())
}
展開結果:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _hooq_meta = ::hooq::HooqMeta {
line: 13usize,
column: 17usize,
path: "mdbook-source-code/meta-vars-hooq_meta/src/main.rs",
file: "main.rs",
source_str: "failable(()) ?",
count: "1st ?",
bindings: ::std::collections::HashMap::from([
(
::std::string::ToString::to_string("string"),
{
let expr = ::std::string::ToString::to_string("\"hello!\"");
let value: ::std::rc::Rc<dyn ::std::any::Any> = ::std::rc::Rc::new(
"hello!",
);
::hooq::BindingPayload {
expr,
value,
}
},
),
]),
};
})?;
Ok(())
}
hook フレーバーの方も合わせてご確認ください。
高度なフックを作成するためのメタ変数
ここまで紹介したメタ変数は基本的にはロギングやデバッグ等でメタ情報を得るためのものでした。
残りの2つ $expr および $so_far はメソッドの記述を補助するメタ変数になります。
expr
| 名前 | リテラル種別 | 説明 |
|---|---|---|
$expr | 式 | 置換用に用いる、置換対象の式 |
内部に対しhooqマクロによるフックが施された後のフック対象トークン列(式)を得られます。メソッドの置換モード において、フック対象(置換対象)を表すのに用いるメタ変数です。
先に説明した通り $source の方は一切フックが施されていない状態の式です。 $source はデバッグ・ロギング出力で元の式を表示するのに使用します。
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn wrapper<T, E>(r: Result<T, E>) -> Result<T, E>
where
E: std::fmt::Debug,
{
if let Err(e) = &r {
println!("Error occurred: {:?}", e);
}
r
}
#[hooq]
#[hooq::method(wrapper($expr))]
fn main() -> Result<(), String> {
failable(())?;
Ok(())
}
展開結果:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn wrapper<T, E>(r: Result<T, E>) -> Result<T, E>
where
E: std::fmt::Debug,
{
if let Err(e) = &r {
{
::std::io::_print(format_args!("Error occurred: {0:?}\n", e));
};
}
r
}
fn main() -> Result<(), String> {
wrapper(failable(()))?;
Ok(())
}
so_far
| 名前 | リテラル種別 | 説明 |
|---|---|---|
$so_far or $sofar | 式 | 主に挿入用に用いる、これまでに設定されているフック |
すでに設定済みのフックを表すメタ変数です。途中で挿入するメソッドを追加したくなった際などに用いることができます。
挿入モード用のフックが入っている場合、意図的に $so_far の中身からは先頭の . (ドット)を抜いてあるので、挿入したい箇所では .$so_far のようにドットを付けて記述します。
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {
let _ = "inserted mode";
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
#[hooq::method(.inspect_err(|_| {
let _ = "before chainned";
}).$so_far)]
failable(())?;
#[hooq::method(.$so_far.inspect_err(|_| {
let _ = "after chainned";
}))]
failable(())?;
Ok(())
}
展開結果:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _ = "inserted mode";
})?;
failable(())
.inspect_err(|_| {
let _ = "before chainned";
})
.inspect_err(|_| {
let _ = "inserted mode";
})?;
failable(())
.inspect_err(|_| {
let _ = "inserted mode";
})
.inspect_err(|_| {
let _ = "after chainned";
})?;
Ok(())
}
フレーバー
フレーバー はhooqマクロのフックするメソッドや挙動等をまとめたプリセットのことを指します。hooqではあらかじめ以下のフレーバーを用意しています。
組み込みフレーバーのクイックリファレンス
| フレーバー名 | feature | 内容 |
|---|---|---|
| default | - | 何も指定しない場合に設定されるフレーバー。hooq.tomlで上書き可 |
| empty | - | 全く何もフックしない場合に用いるフレーバー。上書きは不可 |
| hook | - | hooq::HooqMeta を引数に取る hook メソッドを挿入するフレーバー。ユーザー定義のトレイト経由での利用を想定。上書き可 |
| anyhow | anyhow | with_context メソッドを挿入するフレーバー。上書き可 |
| eyre | eyre | wrap_err_with メソッドを挿入するフレーバー。上書き可 |
| log | log | ::log::error! を呼び出す inspect_err メソッドを挿入するフレーバー。上書き可 |
| tracing | tracing | ::tracing::error! を呼び出す inspect_err メソッドを挿入するフレーバー。上書き可 |
一応feature名を記載しましたが、フレーバーに関係するfeatureはdefault featureに含まれているので明示的にCargo.tomlの features に含める必要はありません。
フレーバーはユーザーが自分で定義することも可能です。クレートのルート( CARGO_MANIFEST_DIR が指し示すディレクトリ) に置いた hooq.toml という名前のtomlファイルにて定義します。詳細は次節にて解説します。
ユーザー定義のフレーバー
hooq.tomlファイルはテーブル名をフレーバー名とし、テーブルに次のフィールドを記述することで設定できます。
| フィールド名 | 型 | 取りうる値の説明 |
|---|---|---|
| trait_uses | 文字列配列 | トレイトパス |
| method | 文字列 | フックするメソッド |
| hook_targets | 文字列配列 | "?" or "return" or "tail_expr" |
| tail_expr_idents | 文字列配列 | "Err" などフックしたい識別子 |
| ignore_tail_expr_idents | 文字列配列 | Ok などフックしたくない識別子 |
| result_types | 文字列配列 | "Result" などの返り値型 |
| hook_in_macros | 真偽値 | true or false |
| bindings | インラインテーブル | 任意のバインディング。文字列リテラル指定時は \" で囲む必要がある点に注意 |
empty を除いたhooqで用意している組み込みフレーバーをテーブル名にすることも可能であり、その場合設定値の上書きとなります。
フレーバーテーブルが持つ bindings 以外のテーブルは サブフレーバー となります。サブフレーバーは親フレーバーの設定値を引き継ぎ、部分的に設定を変更したフレーバーとなります。
設定項目の説明やフレーバーの適用方法等は属性を参照してください。
hooq.tomlの例:
[my_flavor]
trait_uses = ["sub::MyTrait"]
method = """.inspect_err(|_| {
let _ = $xxx;
})"""
hook_targets = ["?", "return", "tail_expr"]
tail_expr_idents = ["Err"]
ignore_tail_expr_idents = ["Ok"]
result_types = ["Result"]
hook_in_macros = true
bindings = { xxx = "\"my_flavor\"" }
[my_flavor.sub_flavor]
bindings = { xxx = "\"my_flavor_sub\"" }
使用例:
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
mod sub {
pub trait MyTrait {}
}
#[hooq(my_flavor)]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
#[hooq::flavor = my_flavor::sub_flavor]
failable(())?;
Ok(())
}
展開結果:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
mod sub {
pub trait MyTrait {}
}
#[allow(unused)]
use sub::MyTrait as _;
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _ = "my_flavor";
})?;
failable(())
.inspect_err(|_| {
let _ = "my_flavor_sub";
})?;
Ok(())
}
default
何も指定しなかった場合( #[hooq] として付与した場合)の設定となるフレーバーです。
次のような設定になっています。(ドキュメントの整合性を保つためソースコードから直接抜粋しています。以降同様)
use std::collections::HashMap;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::{LazyLock, Mutex};
use proc_macro2::TokenStream;
use syn::{Expr, Path, parse_quote};
pub use crate::impls::flavor::flavor_path::FlavorPath;
use crate::impls::flavor::toml_load::HooqToml;
use crate::impls::inert_attr::context::HookTargetSwitch;
use crate::impls::method::Method;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;
mod flavor_path;
mod presets;
mod toml_load;
#[derive(Debug, Clone)]
pub struct Flavor {
pub trait_uses: Vec<Path>,
pub method: Method,
pub hook_targets: HookTargetSwitch,
pub tail_expr_idents: Vec<String>,
pub ignore_tail_expr_idents: Vec<String>,
pub result_types: Vec<String>,
pub hook_in_macros: bool,
pub bindings: HashMap<String, Rc<Expr>>,
pub sub_flavors: HashMap<String, Flavor>,
}
impl Default for Flavor {
fn default() -> Self {
Self {
trait_uses: Vec::new(),
method: default_method(),
hook_targets: HookTargetSwitch {
question: true,
return_: true,
tail_expr: true,
},
tail_expr_idents: vec!["Err".to_string()],
ignore_tail_expr_idents: vec!["Ok".to_string()],
result_types: vec!["Result".to_string()],
hook_in_macros: true,
bindings: HashMap::new(),
sub_flavors: HashMap::new(),
}
}
}
fn default_method() -> Method {
// NOTE:
// $path や $line は eprintln! に直接埋め込みたいところだが、
// CI側のテストの関係でこのようになっている
let excerpted_helpers_path = crate::impls::utils::get_source_excerpt_helpers_name_space();
let res: TokenStream = parse_quote! {
.inspect_err(|e| {
let path = $path;
let line = $line;
let col = $col;
let expr = #excerpted_helpers_path ::excerpted_pretty_stringify!($source);
::std::eprintln!("[{path}:{line}:{col}] {e:?}\n{expr}");
})
};
Method::try_from(res).expect(UNEXPECTED_ERROR_MESSAGE)
}
#[derive(Debug)]
pub struct FlavorStore {
flavors: HashMap<String, Flavor>,
}
// NOTE:
// 本当はパース後のFlavorsをグローバルに保持したいが、OnceLockはSync制約があるため
// TokenStreamを使っているFlavorsはそのままでは保持できない
// そのため変換確認だけ行った状態でHooqTomlをグローバルに保持し、
// 実際にFlavorsが必要な時にはunwrapを許可することにした
#[derive(Debug, Clone)]
pub struct CheckedHooqToml {
pub inner: HooqToml,
}
pub struct TomlStore {
inner: Mutex<HashMap<String, CheckedHooqToml>>,
}
pub static LOADED_HOOQ_TOML: LazyLock<TomlStore> = LazyLock::new(|| TomlStore {
inner: Mutex::new(HashMap::new()),
});
impl TomlStore {
fn load() -> Result<Option<CheckedHooqToml>, String> {
if let Some(checked_hooq_toml) = LOADED_HOOQ_TOML.get() {
return Ok(Some(checked_hooq_toml));
}
let dir_path = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let path = PathBuf::from(dir_path).join("hooq.toml");
if let Ok(false) | Err(_) = path.try_exists() {
return Ok(None);
}
let content = std::fs::read_to_string(&path)
.map_err(|e| format!("failed to read file `{}`: {}", path.display(), e))?;
let hooq_toml: HooqToml = toml::from_str(&content)
.map_err(|e| format!("failed to parse toml from file `{}`: {}", path.display(), e))?;
let checked_hooq_toml = CheckedHooqToml::try_from(hooq_toml)?;
LOADED_HOOQ_TOML.set(checked_hooq_toml.clone());
Ok(Some(checked_hooq_toml))
}
// NOTE:
// 異なるCargoプロジェクトを同じタイミングでVSCodeで開いていた時に
// 違うTomlStoreの内容が違うプロジェクトに供給されている事象が見られた
//
// 効果がどれぐらいあるかは懐疑的であるが、少しでも軽減すべく
// プロジェクトごとに保存領域を分けるようにした
fn set(&self, checked_hooq_toml: CheckedHooqToml) {
let key = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
self.inner.lock().unwrap().insert(key, checked_hooq_toml);
}
fn get(&self) -> Option<CheckedHooqToml> {
let key = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
self.inner.lock().unwrap().get(&key).cloned()
}
}
impl FlavorStore {
fn new() -> Self {
let flavors = presets::preset_flavors();
Self { flavors }
}
pub fn with_hooq_toml() -> Result<Self, String> {
let mut flavors = Self::new();
if let Some(hooq_toml) = TomlStore::load()? {
// 変換が成功することはCheckedHooqTomlの生成時に確認済み
toml_load::apply::update_flavors(&mut flavors.flavors, hooq_toml.inner).unwrap();
}
Ok(flavors)
}
fn get_flavor_inner(&self, path: &FlavorPath) -> Option<Flavor> {
let mut path = path.iter();
let mut current: &Flavor = self.flavors.get(path.next()?)?;
for name in path {
current = current.sub_flavors.get(name)?;
}
Some(current.clone())
}
pub fn get_flavor(&self, path: &FlavorPath) -> Result<Flavor, String> {
self.get_flavor_inner(path).ok_or_else(|| {
format!(
"flavor `{}` is not found. available flavors:
{}",
path.join("::"),
self.all_flavor_names()
.into_iter()
.map(|name| format!(" - {name}"))
.collect::<Vec<_>>()
.join("\n")
)
})
}
pub fn all_flavor_names(&self) -> Vec<String> {
fn collect_names(flavor: &Flavor, prefix: &str, names: &mut Vec<String>) {
let current_name = format!("{prefix}::");
for (sub_name, sub_flavor) in &flavor.sub_flavors {
let full_name = format!("{current_name}{sub_name}");
names.push(full_name.clone());
collect_names(sub_flavor, &full_name, names);
}
}
let mut names = Vec::new();
for (name, flavor) in &self.flavors {
names.push(name.clone());
collect_names(flavor, name, &mut names);
}
names
}
}
impl TryFrom<HooqToml> for FlavorStore {
type Error = String;
fn try_from(value: HooqToml) -> Result<Self, Self::Error> {
let mut flavors = Self::new();
toml_load::apply::update_flavors(&mut flavors.flavors, value)?;
Ok(flavors)
}
}
impl TryFrom<HooqToml> for CheckedHooqToml {
type Error = String;
// ロード時に変換可能かをあらかじめ確認する
fn try_from(value: HooqToml) -> Result<Self, Self::Error> {
let _ = FlavorStore::try_from(value.clone())?;
Ok(Self { inner: value })
}
}
default_method() で設定しているメソッドは次の通りです。(コメントは気にしないでください。)
use std::collections::HashMap;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::{LazyLock, Mutex};
use proc_macro2::TokenStream;
use syn::{Expr, Path, parse_quote};
pub use crate::impls::flavor::flavor_path::FlavorPath;
use crate::impls::flavor::toml_load::HooqToml;
use crate::impls::inert_attr::context::HookTargetSwitch;
use crate::impls::method::Method;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;
mod flavor_path;
mod presets;
mod toml_load;
#[derive(Debug, Clone)]
pub struct Flavor {
pub trait_uses: Vec<Path>,
pub method: Method,
pub hook_targets: HookTargetSwitch,
pub tail_expr_idents: Vec<String>,
pub ignore_tail_expr_idents: Vec<String>,
pub result_types: Vec<String>,
pub hook_in_macros: bool,
pub bindings: HashMap<String, Rc<Expr>>,
pub sub_flavors: HashMap<String, Flavor>,
}
impl Default for Flavor {
fn default() -> Self {
Self {
trait_uses: Vec::new(),
method: default_method(),
hook_targets: HookTargetSwitch {
question: true,
return_: true,
tail_expr: true,
},
tail_expr_idents: vec!["Err".to_string()],
ignore_tail_expr_idents: vec!["Ok".to_string()],
result_types: vec!["Result".to_string()],
hook_in_macros: true,
bindings: HashMap::new(),
sub_flavors: HashMap::new(),
}
}
}
fn default_method() -> Method {
// NOTE:
// $path や $line は eprintln! に直接埋め込みたいところだが、
// CI側のテストの関係でこのようになっている
let excerpted_helpers_path = crate::impls::utils::get_source_excerpt_helpers_name_space();
let res: TokenStream = parse_quote! {
.inspect_err(|e| {
let path = $path;
let line = $line;
let col = $col;
let expr = #excerpted_helpers_path ::excerpted_pretty_stringify!($source);
::std::eprintln!("[{path}:{line}:{col}] {e:?}\n{expr}");
})
};
Method::try_from(res).expect(UNEXPECTED_ERROR_MESSAGE)
}
#[derive(Debug)]
pub struct FlavorStore {
flavors: HashMap<String, Flavor>,
}
// NOTE:
// 本当はパース後のFlavorsをグローバルに保持したいが、OnceLockはSync制約があるため
// TokenStreamを使っているFlavorsはそのままでは保持できない
// そのため変換確認だけ行った状態でHooqTomlをグローバルに保持し、
// 実際にFlavorsが必要な時にはunwrapを許可することにした
#[derive(Debug, Clone)]
pub struct CheckedHooqToml {
pub inner: HooqToml,
}
pub struct TomlStore {
inner: Mutex<HashMap<String, CheckedHooqToml>>,
}
pub static LOADED_HOOQ_TOML: LazyLock<TomlStore> = LazyLock::new(|| TomlStore {
inner: Mutex::new(HashMap::new()),
});
impl TomlStore {
fn load() -> Result<Option<CheckedHooqToml>, String> {
if let Some(checked_hooq_toml) = LOADED_HOOQ_TOML.get() {
return Ok(Some(checked_hooq_toml));
}
let dir_path = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let path = PathBuf::from(dir_path).join("hooq.toml");
if let Ok(false) | Err(_) = path.try_exists() {
return Ok(None);
}
let content = std::fs::read_to_string(&path)
.map_err(|e| format!("failed to read file `{}`: {}", path.display(), e))?;
let hooq_toml: HooqToml = toml::from_str(&content)
.map_err(|e| format!("failed to parse toml from file `{}`: {}", path.display(), e))?;
let checked_hooq_toml = CheckedHooqToml::try_from(hooq_toml)?;
LOADED_HOOQ_TOML.set(checked_hooq_toml.clone());
Ok(Some(checked_hooq_toml))
}
// NOTE:
// 異なるCargoプロジェクトを同じタイミングでVSCodeで開いていた時に
// 違うTomlStoreの内容が違うプロジェクトに供給されている事象が見られた
//
// 効果がどれぐらいあるかは懐疑的であるが、少しでも軽減すべく
// プロジェクトごとに保存領域を分けるようにした
fn set(&self, checked_hooq_toml: CheckedHooqToml) {
let key = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
self.inner.lock().unwrap().insert(key, checked_hooq_toml);
}
fn get(&self) -> Option<CheckedHooqToml> {
let key = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
self.inner.lock().unwrap().get(&key).cloned()
}
}
impl FlavorStore {
fn new() -> Self {
let flavors = presets::preset_flavors();
Self { flavors }
}
pub fn with_hooq_toml() -> Result<Self, String> {
let mut flavors = Self::new();
if let Some(hooq_toml) = TomlStore::load()? {
// 変換が成功することはCheckedHooqTomlの生成時に確認済み
toml_load::apply::update_flavors(&mut flavors.flavors, hooq_toml.inner).unwrap();
}
Ok(flavors)
}
fn get_flavor_inner(&self, path: &FlavorPath) -> Option<Flavor> {
let mut path = path.iter();
let mut current: &Flavor = self.flavors.get(path.next()?)?;
for name in path {
current = current.sub_flavors.get(name)?;
}
Some(current.clone())
}
pub fn get_flavor(&self, path: &FlavorPath) -> Result<Flavor, String> {
self.get_flavor_inner(path).ok_or_else(|| {
format!(
"flavor `{}` is not found. available flavors:
{}",
path.join("::"),
self.all_flavor_names()
.into_iter()
.map(|name| format!(" - {name}"))
.collect::<Vec<_>>()
.join("\n")
)
})
}
pub fn all_flavor_names(&self) -> Vec<String> {
fn collect_names(flavor: &Flavor, prefix: &str, names: &mut Vec<String>) {
let current_name = format!("{prefix}::");
for (sub_name, sub_flavor) in &flavor.sub_flavors {
let full_name = format!("{current_name}{sub_name}");
names.push(full_name.clone());
collect_names(sub_flavor, &full_name, names);
}
}
let mut names = Vec::new();
for (name, flavor) in &self.flavors {
names.push(name.clone());
collect_names(flavor, name, &mut names);
}
names
}
}
impl TryFrom<HooqToml> for FlavorStore {
type Error = String;
fn try_from(value: HooqToml) -> Result<Self, Self::Error> {
let mut flavors = Self::new();
toml_load::apply::update_flavors(&mut flavors.flavors, value)?;
Ok(flavors)
}
}
impl TryFrom<HooqToml> for CheckedHooqToml {
type Error = String;
// ロード時に変換可能かをあらかじめ確認する
fn try_from(value: HooqToml) -> Result<Self, Self::Error> {
let _ = FlavorStore::try_from(value.clone())?;
Ok(Self { inner: value })
}
}
使用例:
use hooq::hooq;
#[hooq]
fn main() -> Result<(), Box<dyn std::error::Error>> {
Err("Hello, world!".into())
}
実行結果:
[mdbook-source-code/flavor-default/src/main.rs:5:5] "Hello, world!"
5> Err("Hell..rld!".into())
|
Error: "Hello, world!"
この設定はhooq.tomlで上書きが可能です。その場合、 #[hooq] とした際の設定値が上書きした設定値になります。
empty
全くフックを行わないフレーバーです。 #[cfg_attr(feature = "...", hooq(empty))] のようにコンパイル条件分岐のような特殊な用法を想定しています。(何もフックをしないものの、不活性属性のハンドリングは行うので有用な指定です。)
次のような設定になっています。
use std::collections::HashMap;
use proc_macro2::TokenStream;
use syn::parse_quote;
use crate::impls::flavor::Flavor;
use crate::impls::inert_attr::context::HookTargetSwitch;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;
pub fn empty_flavor() -> Flavor {
Flavor {
trait_uses: Vec::new(),
method: empty_method().try_into().expect(UNEXPECTED_ERROR_MESSAGE),
hook_targets: HookTargetSwitch {
question: false,
return_: false,
tail_expr: false,
},
tail_expr_idents: Vec::new(),
ignore_tail_expr_idents: Vec::new(),
result_types: Vec::new(),
hook_in_macros: false,
bindings: HashMap::new(),
sub_flavors: HashMap::new(),
}
}
fn empty_method() -> TokenStream {
parse_quote! {
$expr
}
}
特殊なフレーバーであるため 唯一上書き不可 としています。
hook
hookフレーバーの設定は次の通りです。(コメント部分は気にしないでください。)
use proc_macro2::TokenStream;
use syn::parse_quote;
use crate::impls::flavor::Flavor;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;
pub fn hook_flavor() -> Flavor {
Flavor {
// NOTE: Traitの存在を前提とするflavorだがユーザーが決定する必要あり
// trait_uses: Vec::new(), // Default と同じ
method: hook_method().try_into().expect(UNEXPECTED_ERROR_MESSAGE),
..Default::default()
}
}
fn hook_method() -> TokenStream {
parse_quote! {
.hook(|| {
$hooq_meta
})
}
}
ユーザー側でトレイトを定義し、そこでロギングをする場合に便利なフレーバーです。hooq.tomlを使いたくない際に便利です。
使用例:
use hooq::hooq;
mod my_error {
pub trait MyHook {
fn hook(self, meta_fn: impl FnOnce() -> hooq::HooqMeta) -> Self;
}
impl<T, E> MyHook for Result<T, E>
where
E: std::fmt::Debug,
{
fn hook(self, meta_fn: impl FnOnce() -> hooq::HooqMeta) -> Self {
if let Err(e) = &self {
let meta = meta_fn();
eprintln!(
"[{}:{}:{}] error occurred: {:?}",
meta.file, meta.line, meta.column, e
);
}
self
}
}
}
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq(hook, trait_uses(my_error::MyHook))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
Ok(())
}
第2引数部分を meta_fn というクロージャにしているのは、遅延評価のためです。ここをクロージャにしない場合すべての場所で HooqMeta が生成されてしまうため処理が重くなってしまいます。
展開結果:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
mod my_error {
pub trait MyHook {
fn hook(self, meta_fn: impl FnOnce() -> hooq::HooqMeta) -> Self;
}
impl<T, E> MyHook for Result<T, E>
where
E: std::fmt::Debug,
{
fn hook(self, meta_fn: impl FnOnce() -> hooq::HooqMeta) -> Self {
if let Err(e) = &self {
let meta = meta_fn();
{
::std::io::_eprint(
format_args!(
"[{0}:{1}:{2}] error occurred: {3:?}\n",
meta.file,
meta.line,
meta.column,
e,
),
);
};
}
self
}
}
}
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[allow(unused)]
use my_error::MyHook as _;
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.hook(|| {
::hooq::HooqMeta {
line: 33usize,
column: 17usize,
path: "mdbook-source-code/flavor-hook/src/main.rs",
file: "main.rs",
source_str: "failable(()) ?",
count: "1st ?",
bindings: ::std::collections::HashMap::from([]),
}
})?;
Ok(())
}
anyhow
anyhowfeature が必要ですが、defaultに含まれています。
anyhowクレート と共に使うことを想定したフレーバーです。
次の設定になっています。
use proc_macro2::TokenStream;
use syn::parse_quote;
use crate::impls::flavor::Flavor;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;
pub fn anyhow_flavor() -> Flavor {
Flavor {
trait_uses: vec![parse_quote! { ::anyhow::Context }],
method: anyhow_method().try_into().expect(UNEXPECTED_ERROR_MESSAGE),
..Default::default()
}
}
fn anyhow_method() -> TokenStream {
let excerpted_helpers_path = crate::impls::utils::get_source_excerpt_helpers_name_space();
parse_quote! {
.with_context(|| {
let path = $path;
let line = $line;
let col = $col;
let expr = #excerpted_helpers_path ::excerpted_pretty_stringify!($source);
format!("[{path}:{line}:{col}]\n{expr}")
})
}
}
.with_context(...) メソッドを利用するために、 anyhow::Context トレイトをuseしています。
使用例:
use hooq::hooq;
#[hooq(anyhow)]
fn func1() -> anyhow::Result<i32> {
Err(anyhow::anyhow!("Error in func1"))
}
#[hooq(anyhow)]
fn func2() -> anyhow::Result<i32> {
let res = func1()?;
println!("{res}");
Ok(res)
}
#[hooq(anyhow)]
fn main() -> anyhow::Result<()> {
func2()?;
Ok(())
}
実行結果:
Error: [mdbook-source-code/flavor-anyhow/src/main.rs:19:12]
19> func2()?
|
Caused by:
0: [mdbook-source-code/flavor-anyhow/src/main.rs:10:22]
10> func1()?
|
1: [mdbook-source-code/flavor-anyhow/src/main.rs:5:5]
5> Err(anyhow::anyhow!("Error in func1"))
|
2: Error in func1
eyre
eyrefeature が必要ですが、defaultに含まれています。
eyreクレート と共に使うことを想定したフレーバーです。anyhowとはuseしているトレイト、呼び出しているメソッドが異なるだけでほぼ同じです。
次の設定になっています。
use proc_macro2::TokenStream;
use syn::parse_quote;
use crate::impls::flavor::Flavor;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;
pub fn eyre_flavor() -> Flavor {
Flavor {
trait_uses: vec![parse_quote! { ::eyre::WrapErr }],
method: eyre_method().try_into().expect(UNEXPECTED_ERROR_MESSAGE),
..Default::default()
}
}
fn eyre_method() -> TokenStream {
let excerpted_helpers_path = crate::impls::utils::get_source_excerpt_helpers_name_space();
parse_quote! {
.wrap_err_with(|| {
let path = $path;
let line = $line;
let col = $col;
let expr = #excerpted_helpers_path ::excerpted_pretty_stringify!($source);
format!("[{path}:{line}:{col}]\n{expr}")
})
}
}
.wrap_err_with(...) メソッドを利用するために、 eyre::WrapErr トレイトをuseしています。
使用例:
use hooq::hooq;
#[hooq(eyre)]
fn func1() -> eyre::Result<i32> {
Err(eyre::eyre!("Error in func1"))
}
#[hooq(eyre)]
fn func2() -> eyre::Result<i32> {
let res = func1()?;
println!("{res}");
Ok(res)
}
#[hooq(eyre)]
fn main() -> eyre::Result<()> {
func2()?;
Ok(())
}
実行結果:
Error: [mdbook-source-code/flavor-eyre/src/main.rs:19:12]
19> func2()?
|
Caused by:
0: [mdbook-source-code/flavor-eyre/src/main.rs:10:22]
10> func1()?
|
1: [mdbook-source-code/flavor-eyre/src/main.rs:5:5]
5> Err(eyre::eyre!("Error in func1"))
|
2: Error in func1
Location:
mdbook-source-code/flavor-eyre/src/main.rs:5:9
log
logfeature が必要ですが、defaultに含まれています。
logクレート と共に使うことを想定したフレーバーです。
次の設定になっています。
use proc_macro2::TokenStream;
use syn::parse_quote;
use crate::impls::flavor::Flavor;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;
pub fn log_flavor() -> Flavor {
Flavor {
method: log_method().try_into().expect(UNEXPECTED_ERROR_MESSAGE),
..Default::default()
}
}
fn log_method() -> TokenStream {
let excerpted_helpers_path = crate::impls::utils::get_source_excerpt_helpers_name_space();
parse_quote! {
.inspect_err(|e| {
let path = $path;
let line = $line;
let col = $col;
let expr = #excerpted_helpers_path ::excerpted_pretty_stringify!($source);
::log::error!("({path}:{line}:{col}) {e}\n{expr}");
})
}
}
使用例:
use hooq::hooq;
#[hooq(log)]
fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::Builder::new()
.format_timestamp(None)
.filter_level(log::LevelFilter::Error)
.init();
Err("Hello, world!".into())
}
実行結果:
[ERROR flavor_log] (mdbook-source-code/flavor-log/src/main.rs:10:5) Hello, world!
10> Err("Hell..rld!".into())
|
Error: "Hello, world!"
tracing
tracingfeature が必要ですが、defaultに含まれています。
tracingクレート と共に使うことを想定したフレーバーです。
次の設定になっています。
use proc_macro2::TokenStream;
use syn::parse_quote;
use crate::impls::flavor::Flavor;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;
pub fn tracing_flavor() -> Flavor {
Flavor {
method: tracing_method().try_into().expect(UNEXPECTED_ERROR_MESSAGE),
..Default::default()
}
}
fn tracing_method() -> TokenStream {
let excerpted_helpers_path = crate::impls::utils::get_source_excerpt_helpers_name_space();
parse_quote! {
.inspect_err(|e| {
let path = $path;
let line = $line;
let col = $col;
let expr = #excerpted_helpers_path ::one_line_stringify!($source);
::tracing::error!(
path,
line,
col,
error = %e,
expr,
);
})
}
}
#[tracing::instrument] と併用する場合、 #[hooq(tracing)] が先に適用される必要があるため、 #[tracing::instrument] より上に #[hooq(tracing)] を書くことを推奨します。
使用例:
use hooq::hooq;
use tracing::instrument;
#[hooq(tracing)]
#[instrument]
fn func1() -> Result<i32, String> {
Err("Error in func1".into())
}
#[hooq(tracing)]
#[instrument]
fn func2() -> Result<i32, String> {
println!("func2 start");
let res = func1()?;
println!("func2 end: {res}");
Ok(res)
}
#[hooq(tracing)]
#[instrument]
fn func3() -> Result<i32, String> {
println!("func3 start");
let res = func2()?;
println!("func3 end: {res}");
Ok(res)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let format = tracing_subscriber::fmt::format()
.with_ansi(false)
.without_time();
tracing_subscriber::fmt().event_format(format).init();
func3()?;
Ok(())
}
実行結果:
func3 start
func2 start
ERROR func3:func2:func1: flavor_tracing: path="mdbook-source-code/flavor-tracing/src/main.rs" line=7 col=5 error=Error in func1 expr="Err(\"Error in func1\".into())"
ERROR func3:func2: flavor_tracing: path="mdbook-source-code/flavor-tracing/src/main.rs" line=15 col=22 error=Error in func1 expr="func1()?"
ERROR func3: flavor_tracing: path="mdbook-source-code/flavor-tracing/src/main.rs" line=27 col=22 error=Error in func1 expr="func2()?"
Error: "Error in func1"
features
hooqクレートにもいくつかのfeaturesが存在します。本ページに表としてまとめておきます。
| feature名 | default | 説明 |
|---|---|---|
| default | o | デフォルトfeature。 consume-question 以外を含む |
| full | x | すべてのfeatureを含むfeature |
| anyhow | o | anyhowフレーバーを提供するfeature |
| eyre | o | eyreフレーバーを提供するfeature |
| log | o | logフレーバーを提供するfeature |
| tracing | o | tracingフレーバーを提供するfeature |
| consume-question | x | ! (Exclamation Mark)による ? 演算子 (Question Operator) の削除を行えるようになるfeature |
consume-question のみがデフォルトから外れた機能であり、フレーバーを提供するためのfeatureはデフォルトで含まれています。
レシピ・アイデア集
本章では、hooqを活用することで実現できるような事柄のうち、小ネタ的なものを集めました。
実用的な使い方はチュートリアルを通してすでに示しました。本章の内容は実用性があまりない可能性がありますが、一方で(作者も知らないような)hooqの可能性を探求できるような内容を集めました。何かしらのインスピレーションに寄与できれば幸いです。
目次
| ページ | 概要 |
|---|---|
| パーフェクトcolor-eyre | color-eyre は BACKTRACE や SPANTRACE を取る機能があります。これらを取りつつ、さらにhooqでも情報を取得することで最強のエラートレーシングを実現してみます。 |
| mod以下の関数に一括適用 | 実はhooqマクロは関数以外にもアイテムであれば付与できます。そのコード例です。 |
| Option型を返す関数での利用 | tail_expr_idents や result_types の設定項目を変更することで、 Option 型を返す関数を対象としたhooq設定にします。 |
特定のfeatureが有効時に .unwrap() にする | #[cfg_attr(...)] を使うことでコンパイル条件によって挙動を変える方法を示します。 |
match 脱糖の再発明 | ? 演算子を match 式に置き換える脱糖をhooqでやってみます。 |
パーフェクトcolor-eyre
color-eyre クレートは、現在Rustに存在する “スタックトレースに類するもの” (BACKTRACEとSPANTRACE) を得る手段としてはおそらく最も進んでおり無視できない存在でしょう。
というわけで、 公式サンプル に手を加えて、もう一つの “スタックトレースに類するもの” を追加してあげましょう!
新規プロジェクトを cargo new で作成し、Cargo.toml は次のようにします。 ( hooq の依存については最新バージョンに置き換えてください。 )
[features]
default = ["capture-spantrace"]
capture-spantrace = []
[dependencies]
color-eyre = "0.6.5"
hooq = { path = "../../hooq" } # Please rewrite `hooq = "*"`
tracing = "0.1.43"
tracing-error = "0.2.1"
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
次に、 ::eyre::WrapErr ではなく color_eyre::eyre::WrapErr をuseしたいので、その上書きを行うhooq.tomlをCargo.tomlと同じ階層に置きます。そのほかの設定は組み込みの eyreフレーバー から引き継がれます!
[eyre]
trait_uses = ["color_eyre::eyre::WrapErr"]
そしてmain.rsを書きます。 use hooq::hooq; し、 #[instrument] の上に #[hooq(eyre)] を付けます1。
use color_eyre::Section;
use color_eyre::eyre::{Report, WrapErr};
use hooq::hooq;
use tracing::{info, instrument};
#[hooq(eyre)]
#[instrument]
fn main() -> Result<(), Report> {
#[cfg(feature = "capture-spantrace")]
install_tracing();
color_eyre::install()?;
read_config()
}
#[cfg(feature = "capture-spantrace")]
fn install_tracing() {
use tracing_error::ErrorLayer;
use tracing_subscriber::prelude::*;
use tracing_subscriber::{EnvFilter, fmt};
let fmt_layer = fmt::layer().with_target(false).without_time();
let filter_layer = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new("info"))
.unwrap();
tracing_subscriber::registry()
.with(filter_layer)
.with(fmt_layer)
.with(ErrorLayer::default())
.init();
}
#[hooq(eyre)]
#[instrument]
fn read_file(path: &str) -> Result<(), Report> {
info!("Reading file");
std::fs::read_to_string(path).map(drop)
}
#[hooq(eyre)]
#[instrument]
fn read_config() -> Result<(), Report> {
read_file("fake_file")
.wrap_err("Unable to read config")
.suggestion("try using a file that exists next time")
}
BACKTRACEも見たいので、これを環境変数 RUST_LIB_BACKTRACE=1 を設定した状態で実行してみます。
公式例では .wrap_err() による付与部分で理由しか書かれていなかった部分も、hooqのおかげでスタックトレースもどきに化けています!
Error:
0: [mdbook-source-code/recipe-color-eyre/src/main.rs:14:5]
14> read_config()
|
1: [mdbook-source-code/recipe-color-eyre/src/main.rs:45:5]
45> read_file("fake_file")
46| .wrap_err("Unab..nfig")
47| .suggestion("try ..time")
|
2: Unable to read config
3: [mdbook-source-code/recipe-color-eyre/src/main.rs:39:5]
39> std::fs::read_to_string(path).map(drop)
|
4: No such file or directory (os error 2)
Location:
mdbook-source-code/recipe-color-eyre/src/main.rs:39
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ SPANTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
0: recipe_color_eyre::read_file with path="fake_file"
at mdbook-source-code/recipe-color-eyre/src/main.rs:36
1: recipe_color_eyre::read_config
at mdbook-source-code/recipe-color-eyre/src/main.rs:43
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ BACKTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⋮ 4 frames hidden ⋮
5: <E as eyre::context::ext::StdError>::ext_report::h0e9fcf40b567f05e
at /home/USER/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/eyre-0.6.12/src/context.rs:26
⋮ 1 frame hidden ⋮
7: recipe_color_eyre::read_file::h74782b4bb409e5a9
at /home/USER/workspace/hooq/mdbook-source-code/recipe-color-eyre/src/main.rs:39
8: recipe_color_eyre::read_config::h8139da57eb93337e
at /home/USER/workspace/hooq/mdbook-source-code/recipe-color-eyre/src/main.rs:45
9: recipe_color_eyre::main::h9c27f45de169a3be
at /home/USER/workspace/hooq/mdbook-source-code/recipe-color-eyre/src/main.rs:14
10: core::ops::function::FnOnce::call_once::h724113526cc98cab
at /home/USER/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:250
11: std::sys::backtrace::__rust_begin_short_backtrace::h9e20a34286867e4d
at /home/USER/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/backtrace.rs:158
⋮ 14 frames hidden ⋮
Suggestion: try using a file that exists next time
Run with COLORBT_SHOW_HIDDEN=1 environment variable to disable frame filtering.
Run with RUST_BACKTRACE=full to include source snippets.
多分これが一番詳細だと思います。
-
スナップショットテストの関係で時刻を出力しない
.without_time()も付与しています。 ↩
mod以下の関数に一括適用
実はhooqマクロは関数だけではなく処理構文を含むアイテムに付与が可能です。
ここでいう“アイテム“とは、Rustの手続きマクロ(あるいは文法)用語で、関数・mod・implブロック・traitブロック…など、直接ファイル直下に置ける文法要素( Things that can appear directly inside of a module or scope. )です。ただし処理構文を含まないアイテムについては無関係です。詳細はsynクレートの Item を見てください。
ともかく、( mod xxx; という形ではない、中括弧を持った)modが定義されている時に、modに #[hooq] マクロを付けるとその内側の関数については #[hooq] を付けなくてもフックが行われるようになります。
この再帰的性質はmodに限りません。関数の中に関数やクロージャがネストしている場合、内側の関数やクロージャに対しても #[hooq] は設定に従い貪欲にメソッドをフックします。(コンパイルエラーにならないうちは、その方がデバッグがはかどりますよね?コンパイルエラーになる場合は #[hooq::skip_all] を付けてください。)
というわけで、良い機会なのでhooqの性質がわかるパズルを用意してみました。コメントに(英語で)理由を付記しましたが、関数やクロージャの中の値がどういう場合にフックされるか考えながら読んでみてください。
use hooq::hooq;
#[hooq] // attribute macro root exist only here
#[hooq::method(.inspect_err(|_| {}))]
mod hoge {
pub fn bar() -> i32 {
// NOT hook target because this function does not return Result
42
}
pub fn failable<T>(val: T) -> Result<T, ()> {
// The return type of this function is Result
// but "Ok" is specified as ignore_tail_expr_idents by default
// so NOT hook target
Ok(val)
}
#[hooq::tail_expr_idents("Ok", "Err")]
pub fn _failable_2<T>(val: T) -> Result<T, ()> {
if failable(false).unwrap() {
// ↑ above is NOT hook target
// ↓ below: hook target because the return type of this function is Result
return failable(val);
}
// hook target because "Ok" is now specified as tail_expr_idents
Ok(val)
}
pub fn fuga() -> Result<(), ()> {
// hook target because of `?`
failable(())?;
let _ = || {
// hook target because of `?`
failable(())?;
// hook target because of `?`
if failable(false)? {
// hook target because "Err" is specified as tail_expr_idents by default
return Err(());
}
// NOT hook target because "Ok" is specified as ignore_tail_expr_idents by default
Ok(())
};
let _ = || {
// NOT hook target because "Ok" is specified as ignore_tail_expr_idents by default
Result::<(), ()>::Ok(())
};
let _ = {
let _ = bar();
// hook target because "Err" is specified as tail_expr_idents by default
// even if this is the tail expression of the block not a closure
Result::<(), ()>::Err(())
};
let _ = || -> Result<(), ()> {
// hook target because hooq can know this closure returns Result
failable(())
};
let _ = || {
// NOT hook target because hooq cannot know this closure returns Result
failable(())
};
// NOT hook target because "Ok" is specified as ignore_tail_expr_idents by default
Ok(())
}
}
fn main() {
let _ = hoge::bar();
let _ = hoge::failable(123);
let _ = hoge::fuga();
}
展開結果は次のようになります。かなり自然に、しかしマクロにしては1貪欲にフックを仕掛けられていると思います。
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
mod hoge {
pub fn bar() -> i32 {
42
}
pub fn failable<T>(val: T) -> Result<T, ()> {
Ok(val)
}
pub fn _failable_2<T>(val: T) -> Result<T, ()> {
if failable(false).unwrap() {
return failable(val).inspect_err(|_| {});
}
Ok(val).inspect_err(|_| {})
}
pub fn fuga() -> Result<(), ()> {
failable(()).inspect_err(|_| {})?;
let _ = || {
failable(()).inspect_err(|_| {})?;
if failable(false).inspect_err(|_| {})? {
return Err(()).inspect_err(|_| {});
}
Ok(())
};
let _ = || { Result::<(), ()>::Ok(()) };
let _ = {
let _ = bar();
Result::<(), ()>::Err(()).inspect_err(|_| {})
};
let _ = || -> Result<(), ()> { failable(()).inspect_err(|_| {}) };
let _ = || { failable(()) };
Ok(())
}
}
fn main() {
let _ = hoge::bar();
let _ = hoge::failable(123);
let _ = hoge::fuga();
}
-
手続きマクロにはあまり高度なリフレクション機能がないので、型推論で
Result型かどうか判別したりはできません。 ↩
Option型を返す関数での利用
属性でフック対象判定を制御する 方法を紹介していましたが、イマイチピンとこなかったと思います。そもそも滅多に触ることはない設定だろうなと作者も考えています。
それでもなるべくイメージが付きやすい例ということで、 ( Result 型ではなく) Option 型にフックすることのみを目的とした設定を施してみましょう。
次の例では、main関数内に定義した関数にフックをしています。(ネストできることについては mod以下の関数に一括適用を参照してください。) Some であったときに中身を確認するメソッドを挿入したいというシナリオです。
tail_expr_idents: 今回、SomeにのみフックしたいのでSomeを指定ignore_expr_idents:Noneにフックを仕掛けるのは無駄なので(ほかに条件がそろっている時でも)Noneにはフックしないようにするresult_types:Option型関数の時は末尾式やreturnに来る値の型をフック対象だとみなすようになる
use std::fmt::Debug;
use hooq::hooq;
#[hooq]
#[hooq::method(.my_inspect())]
#[hooq::tail_expr_idents("Some")]
#[hooq::ignore_tail_expr_idents("None")]
#[hooq::result_types("Option")]
fn main() {
fn option_fn_1() -> Option<i32> {
// hook target
Some(42)
}
fn option_fn_2<T: Debug>(flag: bool, val: T) -> Option<T> {
// hook target
let _ = option_fn_1()?;
// hook target because the return type of the function is Option
if flag {
// hook target
Some(val)
} else {
// NOT hook target
None
}
}
fn result_fn_1() -> Result<i32, ()> {
// NOT hook target because the return type of the function is Result not Option
Ok(42)
}
fn result_fn_2() -> Result<i32, ()> {
// HOOK TARGET because of `?`
// so, #[hooq::skip_all] is needed
#[hooq::skip_all]
let _ = result_fn_1()?;
// NOT hook target because the return type of the function is Result not Option
Ok(42)
}
let _ = option_fn_1();
let _ = option_fn_2(true, 123);
let _ = result_fn_1();
let _ = result_fn_2();
}
trait MyInspect {
fn my_inspect(self) -> Self;
}
impl<T> MyInspect for Option<T>
where
T: Debug,
{
fn my_inspect(self) -> Self {
match self {
Some(val) => {
println!("Inspecting value: {:?}", val);
Some(val)
}
None => None,
}
}
}
前レシピのmod以下の関数に一括適用と同じぐらいのパズルになってしまいましたね…
展開結果は次のようになります。
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use std::fmt::Debug;
use hooq::hooq;
fn main() {
fn option_fn_1() -> Option<i32> {
Some(42).my_inspect()
}
fn option_fn_2<T: Debug>(flag: bool, val: T) -> Option<T> {
let _ = option_fn_1().my_inspect()?;
if flag { Some(val).my_inspect() } else { None }.my_inspect()
}
fn result_fn_1() -> Result<i32, ()> {
Ok(42)
}
fn result_fn_2() -> Result<i32, ()> {
let _ = result_fn_1()?;
Ok(42)
}
let _ = option_fn_1();
let _ = option_fn_2(true, 123);
let _ = result_fn_1();
let _ = result_fn_2();
}
trait MyInspect {
fn my_inspect(self) -> Self;
}
impl<T> MyInspect for Option<T>
where
T: Debug,
{
fn my_inspect(self) -> Self {
match self {
Some(val) => {
{
::std::io::_print(format_args!("Inspecting value: {0:?}\n", val));
};
Some(val)
}
None => None,
}
}
}
ともかく本節で主張したかったのは、基本的にはフック対象の型や識別子は Result, Ok, Err から変更することはないかもしれませんが、それでもhooqは対象となる型・識別子の設定を完全に固定しているわけではなく、一応設定変更可能にしているということです。
将来的に Tryトレイト 周りがstableになった時などに利用の幅が広がればと考えています。
特定のfeatureが有効時に .unwrap() にする
メソッド にて説明している通り、 consume-question feature を有効にしている時に限り、 ! をメソッド末尾に付けることで ? を削除することが可能です。
これを利用すると、例えば ? を unwrap の別名として利用できたりします。
例として、 unwrap featureが有効な時だけ ? を .unwrap() に置き換える例を紹介します。Cargo.tomlのfeaturesが以下のように設定されているとします。
[dependencies]
hooq = { version = "*", features = ["consume-question"] }
# ..
[features]
unwrap = []
hooq.tomlに予めフレーバーを用意しておきます。末尾式や return に同様の置換を行われると困るため、 hook_targets の方で ? でのみ置き換わるように指定しておきます。
[unwrap]
method = """.unwrap()!"""
hook_targets = ["?"]
main.rsを次のようにします。
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[cfg_attr(not(feature = "unwrap"), hooq(empty))]
#[cfg_attr(feature = "unwrap", hooq(unwrap))]
fn process(flag: bool) -> Result<(), String> {
if flag {
return Err("An error occurred".into());
}
let _ = failable(42)?;
Ok(())
}
#[cfg_attr(not(feature = "unwrap"), hooq(empty))]
#[cfg_attr(feature = "unwrap", hooq(unwrap))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
process(false)?;
Ok(())
}
unwrap featureがついていない時の展開結果は、emptyフレーバーが適用されるので何も変化が起きていません。
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn process(flag: bool) -> Result<(), String> {
if flag {
return Err("An error occurred".into());
}
let _ = failable(42)?;
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
process(false)?;
Ok(())
}
一方で、 unwrap featureをつけている時の展開結果では、狙い通り .unwrap() に置き換わっています!
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn process(flag: bool) -> Result<(), String> {
if flag {
return Err("An error occurred".into());
}
let _ = failable(42).unwrap();
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
process(false).unwrap();
Ok(())
}
本節では2つのことを紹介しました。
?を.unwrap()など特殊なものにも置き換えられること#[cfg_attr(..., hooq(...))]でfeatureによってフック内容を変えられること
前者は(作者は詳しくありませんが、)unsafe Rustやno_std環境などでも ? を導入できる足がかりにできるかもしれません。後者は今回の例に限らず、featureを利用してデバッグ情報を調整する手段として有効活用できそうです!
match 脱糖の再発明
特定のfeatureが有効時に .unwrap() にする にて consume-question feature有効時に ! を使うことで ? を別なものに置換できることを示しました。
これを利用すると ? を match に置き換えることも可能です。 match 脱糖の再発明ですね。面白い例なので載せておこうと思います。
まず、忘れずに consume-question を有効にしておいてください。
cargo add hooq --features consume-question
hooq.tomlにて置換結果となる match 式を定義します。 $expr が置換元の式を表します!フレーバー名ですが、 match は予約語なので my_match としています。
[my_match]
method = """
match $expr {
Ok(val) => val,
Err(err) => return Err(From::from(err)),
}!
"""
後は、いつも通りmain.rsを書くだけです。ただし再発明はClippyに怒られる可能性があるので、一応 #[allow(clippy::question_mark)] を関数の頭に付けましょう。
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq(my_match)]
#[allow(clippy::question_mark)]
fn main() -> Result<(), String> {
let _ = failable(42)?;
Ok(())
}
展開結果においては狙い通り match 式になっています!
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[allow(clippy::question_mark)]
fn main() -> Result<(), String> {
let _ = match failable(42) {
Ok(val) => val,
Err(err) => return Err(From::from(err)),
};
Ok(())
}