こんにチュア!本記事は hooqアドベントカレンダー 3日目の記事です!
hooqアドベントカレンダーでは、筆者が作った属性マクロクレートhooqの宣伝目的で、hooqマクロそれ自体の話や、マクロ作成にあたり得た知識などを提供しています!
宣伝(SEO)的な観点だと、そういえばhooqの最も重要なアイデンティティである「? 演算子にメソッドをフックできる」という機能を前面に出した記事は書いてませんでした!というわけで、本記事にしたためたいと思います。
#[hooq] を付けるだけでバックトレースもどきが得られる!
昨日の記事 ではanyhowを利用しましたが、hooqを使えばanyhowを使わなくてもバックトレースもどきを標準エラー出力に出すことができます!
use hooq::hooq;
#[hooq]
fn hoge() -> Result<(), String> {
buddhas_face()?;
Ok(())
}
#[hooq]
fn fuga() -> Result<(), String> {
buddhas_face()?;
hoge()?;
Ok(())
}
#[hooq]
fn bar() -> Result<(), String> {
hoge()?;
fuga()?;
Ok(())
}
#[hooq]
fn main() -> Result<(), String> {
hoge()?;
fuga()?;
bar()?;
Ok(())
}
/// 4回呼ぶと怒られる関数
#[hooq]
fn buddhas_face() -> Result<(), String> {
use std::sync::{LazyLock, Mutex};
static CALLED_COUNT: LazyLock<Mutex<u32>> = LazyLock::new(|| Mutex::new(0));
let mut counter = CALLED_COUNT.lock().unwrap();
*counter += 1;
if *counter > 3 {
return Err("buddhas_face called more than three times".into());
}
Ok(())
}
実行結果は次の通りです!
$ cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/project`
[src/main.rs:51:9] "buddhas_face called more than three times"
51> return Err("budd..imes".into())
|
[src/main.rs:5:19] "buddhas_face called more than three times"
5> buddhas_face()?
|
[src/main.rs:21:11] "buddhas_face called more than three times"
21> hoge()?
|
[src/main.rs:34:10] "buddhas_face called more than three times"
34> bar()?
|
Error: "buddhas_face called more than three times"
プログラムによってはもはやanyhowすら要らないのでは?
hooqが何をしたか?
なぜこのような結果になったかというと、 #[hooq] マクロが各 ? と式の間に次のメソッドを挿入してくれたからです!
.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}");
})
.inspect_err(|e| ...) は Result 型の不変参照を元にアクションを起こせるメソッドです。マイナーなメソッドですが .map_err(|e| ...) で最後になんの変更も加えずに e を返す場合と同じような挙動になるので、hooqのデフォルト挙動にはうってつけのメソッドです!
メソッドの中では、フック対象の行数を得る $line など、フックに関連したメタ変数にアクセスすることができます。
line!() マクロはhooqがフックするメソッド内で記述しても正確な行数を与えてくれません!メタ変数を設けているのはそのためです。
この辺りについて後日記事にしたいと考えています。
hooqマクロによって実際にどうフックが施されたかはcargo-expandというツールを用いることで得られます。
cargo expand の結果は次のようになります!
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn hoge() -> Result<(), String> {
buddhas_face()
.inspect_err(|e| {
let path = "src/main.rs";
let line = 5usize;
let col = 19usize;
let expr = " 5> buddhas_face()?\n |";
{
::std::io::_eprint(
format_args!("[{0}:{1}:{2}] {3:?}\n{4}\n", path, line, col, e, expr),
);
};
})?;
Ok(())
}
// 省略
/// 4回呼ぶと怒られる関数
fn buddhas_face() -> Result<(), String> {
use std::sync::{LazyLock, Mutex};
static CALLED_COUNT: LazyLock<Mutex<u32>> = LazyLock::new(|| Mutex::new(0));
let mut counter = CALLED_COUNT.lock().unwrap();
*counter += 1;
if *counter > 3 {
return Err("buddhas_face called more than three times".into())
.inspect_err(|e| {
let path = "src/main.rs";
let line = 51usize;
let col = 9usize;
let expr = " 51> return Err(\"budd..imes\".into())\n |";
{
::std::io::_eprint(
format_args!(
"[{0}:{1}:{2}] {3:?}\n{4}\n",
path,
line,
col,
e,
expr,
),
);
};
});
}
Ok(())
}
省略なし
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn hoge() -> Result<(), String> {
buddhas_face()
.inspect_err(|e| {
let path = "src/main.rs";
let line = 5usize;
let col = 19usize;
let expr = " 5> buddhas_face()?\n |";
{
::std::io::_eprint(
format_args!("[{0}:{1}:{2}] {3:?}\n{4}\n", path, line, col, e, expr),
);
};
})?;
Ok(())
}
fn fuga() -> Result<(), String> {
buddhas_face()
.inspect_err(|e| {
let path = "src/main.rs";
let line = 12usize;
let col = 19usize;
let expr = " 12> buddhas_face()?\n |";
{
::std::io::_eprint(
format_args!("[{0}:{1}:{2}] {3:?}\n{4}\n", path, line, col, e, expr),
);
};
})?;
hoge()
.inspect_err(|e| {
let path = "src/main.rs";
let line = 14usize;
let col = 11usize;
let expr = " 14> hoge()?\n |";
{
::std::io::_eprint(
format_args!("[{0}:{1}:{2}] {3:?}\n{4}\n", path, line, col, e, expr),
);
};
})?;
Ok(())
}
fn bar() -> Result<(), String> {
hoge()
.inspect_err(|e| {
let path = "src/main.rs";
let line = 21usize;
let col = 11usize;
let expr = " 21> hoge()?\n |";
{
::std::io::_eprint(
format_args!("[{0}:{1}:{2}] {3:?}\n{4}\n", path, line, col, e, expr),
);
};
})?;
fuga()
.inspect_err(|e| {
let path = "src/main.rs";
let line = 23usize;
let col = 11usize;
let expr = " 23> fuga()?\n |";
{
::std::io::_eprint(
format_args!("[{0}:{1}:{2}] {3:?}\n{4}\n", path, line, col, e, expr),
);
};
})?;
Ok(())
}
fn main() -> Result<(), String> {
hoge()
.inspect_err(|e| {
let path = "src/main.rs";
let line = 30usize;
let col = 11usize;
let expr = " 30> hoge()?\n |";
{
::std::io::_eprint(
format_args!("[{0}:{1}:{2}] {3:?}\n{4}\n", path, line, col, e, expr),
);
};
})?;
fuga()
.inspect_err(|e| {
let path = "src/main.rs";
let line = 32usize;
let col = 11usize;
let expr = " 32> fuga()?\n |";
{
::std::io::_eprint(
format_args!("[{0}:{1}:{2}] {3:?}\n{4}\n", path, line, col, e, expr),
);
};
})?;
bar()
.inspect_err(|e| {
let path = "src/main.rs";
let line = 34usize;
let col = 10usize;
let expr = " 34> bar()?\n |";
{
::std::io::_eprint(
format_args!("[{0}:{1}:{2}] {3:?}\n{4}\n", path, line, col, e, expr),
);
};
})?;
Ok(())
}
/// 4回呼ぶと怒られる関数
fn buddhas_face() -> Result<(), String> {
use std::sync::{LazyLock, Mutex};
static CALLED_COUNT: LazyLock<Mutex<u32>> = LazyLock::new(|| Mutex::new(0));
let mut counter = CALLED_COUNT.lock().unwrap();
*counter += 1;
if *counter > 3 {
return Err("buddhas_face called more than three times".into())
.inspect_err(|e| {
let path = "src/main.rs";
let line = 51usize;
let col = 9usize;
let expr = " 51> return Err(\"budd..imes\".into())\n |";
{
::std::io::_eprint(
format_args!(
"[{0}:{1}:{2}] {3:?}\n{4}\n",
path,
line,
col,
e,
expr,
),
);
};
});
}
Ok(())
}
人の手で行うのは難しい量のメソッドが挿入されていることがわかります。 ![]()
フックするメソッドを変更してみる
行数だけ出力するように改変してみましょう! #[hooq::method(...)] という不活性属性(属性マクロ下で用いられる、補助的な属性のこと) を使うことで、フックされるメソッドを指定できます!
#[hooq::method(.inspect_err(|_| {
eprintln!("@ {}", $line);
}))]
use hooq::hooq;
#[hooq]
#[hooq::method(.inspect_err(|_| {
eprintln!("@ {}", $line);
}))]
fn hoge() -> Result<(), String> {
buddhas_face()?;
Ok(())
}
#[hooq]
#[hooq::method(.inspect_err(|_| {
eprintln!("@ {}", $line);
}))]
fn fuga() -> Result<(), String> {
buddhas_face()?;
hoge()?;
Ok(())
}
#[hooq]
#[hooq::method(.inspect_err(|_| {
eprintln!("@ {}", $line);
}))]
fn bar() -> Result<(), String> {
hoge()?;
fuga()?;
Ok(())
}
#[hooq]
#[hooq::method(.inspect_err(|_| {
eprintln!("@ {}", $line);
}))]
fn main() -> Result<(), String> {
hoge()?;
fuga()?;
bar()?;
Ok(())
}
/// 4回呼ぶと怒られる関数
#[hooq]
#[hooq::method(.inspect_err(|_| {
eprintln!("@ {}", $line);
}))]
fn buddhas_face() -> Result<(), String> {
use std::sync::{LazyLock, Mutex};
static CALLED_COUNT: LazyLock<Mutex<u32>> = LazyLock::new(|| Mutex::new(0));
let mut counter = CALLED_COUNT.lock().unwrap();
*counter += 1;
if *counter > 3 {
return Err("buddhas_face called more than three times".into());
}
Ok(())
}
出力結果はシンプルになります。
$ cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/project`
@ 66
@ 8
@ 30
@ 46
Error: "buddhas_face called more than three times"
その他にも色々設定を変えたりできます!
気になった方はぜひmdBookの方を読んでみてほしいです。
まとめ・hooqの示唆
まとめにかこつけたポジショントーク・ポエムです。マサカリ覚悟ですが筆者はそのマサカリに答えられないと思います。(答えられるならhooqなんて作ってないです)
anyhowすら使わないでエラートレースっぽいものを出力できるhooqマクロを使えば、 Result 型をある種の理想形に保てるのではないか?と筆者は考えています。
Rustの Result 型がもてはやされているのは、そのシンプルさにあるのではないでしょうか?欲を言えば皆さん「 Err バリアントが保有する型には任意の型を指定できてほしい」ですよね?(違う?)
しかし実際は、バックトレースの取得等を理由に結局複雑なエラー型を導入せざるを得なくなっています。極論、それなら Result 型を利用しなくてもよいのではないでしょうか?追加の学習コストが必要なら try-catch でも良いのではと思ってしまいます。 try-catch がネストすると地獄かもしれませんが、使う箇所を最小限に絞り、関数のシグネチャでエラーが起きる可能性を明示できればそれで充分です。
Result 型には、ユーザーが自由に触れるRust処理フローの枠内・文脈でエラー値を扱いたいという願望が含まれているんじゃないかと思います。しかし例えばBacktraceを含める必要があったりで、今のRustでは Result 型に対してのユーザーの裁量が減ってしまっているように感じます。
この根本原因は、「エラーがどのように流れてきたかをキャプチャする」みたいな 副作用 を、シンプルに保ちたいはずの Err バリアント値に押し込めてしまっていることにあるんじゃないかと思います。
hooqはその副作用を、型ではなくマクロという形で外に流します。このようなアイデアは今まであったでしょうか1?
「マクロを使いたくない!なんてものを作ってくれたんだ!こんなクレートはいらない!」確かにそうかもしれません。マクロである必要はないかもしれません2。
しかし、hooqは「 Result 型は実態として、入門書にあるようなシンプルな型ではなくなっている」ということを示唆していると思います。エラーロギング・エラーハンドリングの手法が乱立しているRustにおいて、この示唆に対してもう少し答えがほしいです。利用に関しては否定してくださって結構ですので、その代わりに前述の問題提起をしているクレートなのだと認識していただけたら幸いです。