こんにチュア!本記事は hooqアドベントカレンダー 16日目の記事です!
hooqはメソッドを ? 演算子にフックする属性マクロです。本アドカレではhooqの使い方を始め、hooqマクロを作成するにあたり得た知識、Rustのエラーハンドリング・エラーロギング周りの話をまとめています。
昨日の記事では ? 演算子を .unwrap() に置換するというhooqのふざけた?使い方をしました。
「 ? を別なものに置換できるなら、 match 式への置き換え、つまり ? 演算子の脱糖も再現できるのでは?」
というわけで、本記事では実際にこれを行ってみたいと思います!
hooqで再現! ? 演算子の脱糖
? 演算子は現在以下の match 式へと脱糖されます1!
match 対象式 {
Ok(v) => v,
Err(e) => return Err(From::from(e)),
}
これをそのままhooqの設定に使います!まずはお試し用にプロジェクトを作りましょう。
cargo new project
cd project
cargo add hooq --features consume-question
プロジェクトルートに hooq.toml を以下の内容で置きます。
[default]
method = """match $expr {
Ok(v) => v,
Err(e) => return Err(From::from(e)),
}!
"""
hook_targets = ["?"]
-
[default]フレーバーを編集すると#[hooq]として付与した時の挙動を設定できます -
methodフィールドにはexpr?へフックされるメソッドや置換される内容を指定します- 先頭が
.(ドット)で始まっていない場合は置換モードになります -
$expr: 置換対象となる式 -
!: 最後の?を消す指定 (consume-questionフィーチャーが必要)
- 先頭が
-
hook_targets:return,?, 末尾式の3つについて、フックする対象をスイッチします- 今回は
?だけに興味があるので["?"]としました
- 今回は
これで ? が再発明されました!実際に src/main.rs で試してみます。
use hooq::hooq;
use std::error::Error;
#[hooq]
fn add(a: &str, b: &str) -> Result<u32, Box<dyn Error>> {
let num1: u32 = a.parse()?;
let num2: u32 = b.parse()?;
Ok(num1 + num2)
}
fn main() {
println!("{}", add("10", "20").unwrap());
}
$ cargo run -q
30
うーん...ちゃんと実行されましたが再発明の実感がない ![]()
ちなみにcargo expandで確かめられます
hooq 部分をコメントアウトしていつも通りの状態にして cargo expand すると以下のように ? が残ります。
$ cargo expand
Checking project v0.1.0 (/home/namn/workspace/qiita_adv_articles_2025/programs/adv16/project)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.06s
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use std::error::Error;
fn add(a: &str, b: &str) -> Result<u32, Box<dyn Error>> {
let num1: u32 = a.parse()?;
let num2: u32 = b.parse()?;
Ok(num1 + num2)
}
fn main() {
{
::std::io::_print(format_args!("{0}\n", add("10", "20").unwrap()));
};
}
一方で #[hooq] を有効にすると脱糖の様子が見れます。一応これは証拠と言えます。
$ cargo expand
Checking project v0.1.0 (/home/namn/workspace/qiita_adv_articles_2025/programs/adv16/project)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
use std::error::Error;
fn add(a: &str, b: &str) -> Result<u32, Box<dyn Error>> {
let num1: u32 = match a.parse() {
Ok(v) => v,
Err(e) => return Err(From::from(e)),
};
let num2: u32 = match b.parse() {
Ok(v) => v,
Err(e) => return Err(From::from(e)),
};
Ok(num1 + num2)
}
fn main() {
{
::std::io::_print(format_args!("{0}\n", add("10", "20").unwrap()));
};
}
再発明の証拠を確認
再発明したことの証拠を得ましょう!
Err(e) の時に eprintln!("Err with {e:?}"); を間に挟んだりしても良いですがあまり芸がありません。再発明がテーマなので下手なノイズは入れたくないのです。(気になった方はやってみましょう!)
それよりもTryトレイトを実装していない型にも ? を付けられることを示せると面白そうです!ソースコードを以下のようにしてみてください。
use hooq::hooq;
#[derive(Debug)]
enum MyResult {
Ok(usize),
Err(String),
}
use MyResult::{Err, Ok};
#[hooq]
fn func() -> MyResult {
let val = Ok(42);
let res = val? + 1;
Err("beep".to_string())?;
Ok(res)
}
fn main() {
println!("{:?}", func());
}
ちゃんとビルドされて実行されます!
$ cargo run -q
Err("beep")
#[hooq] を取り除くとコンパイルエラーになります!
コンパイルエラー(長いので折り畳み)
// #[hooq] とした時です。
$ cargo run -q
warning: unused import: `hooq::hooq`
--> src/main.rs:1:5
|
1 | use hooq::hooq;
| ^^^^^^^^^^
|
= note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default
error[E0277]: the `?` operator can only be applied to values that implement `Try`
--> src/main.rs:15:15
|
15 | let res = val? + 1;
| ^^^^ the `?` operator cannot be applied to type `MyResult`
|
help: the trait `Try` is not implemented for `MyResult`
--> src/main.rs:4:1
|
4 | enum MyResult {
| ^^^^^^^^^^^^^
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
--> src/main.rs:15:18
|
12 | fn func() -> MyResult {
| --------------------- this function should return `Result` or `Option` to accept `?`
...
15 | let res = val? + 1;
| ^ cannot use the `?` operator in a function that returns `MyResult`
error[E0277]: the `?` operator can only be applied to values that implement `Try`
--> src/main.rs:17:5
|
17 | Err("beep".to_string())?;
| ^^^^^^^^^^^^^^^^^^^^^^^^ the `?` operator cannot be applied to type `MyResult`
|
help: the trait `Try` is not implemented for `MyResult`
--> src/main.rs:4:1
|
4 | enum MyResult {
| ^^^^^^^^^^^^^
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
--> src/main.rs:17:28
|
12 | fn func() -> MyResult {
| --------------------- this function should return `Result` or `Option` to accept `?`
...
17 | Err("beep".to_string())?;
| ^ cannot use the `?` operator in a function that returns `MyResult`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `project2` (bin "project2") due to 4 previous errors; 1 warning emitted
hooqのおかげで ? 演算子の挙動が保たれているということで、再発明されていると言えそうです!
ちょっとした応用
「再発明したから何?」って感じですよね...自分でもそう思います。
一つあるのは、thiserrorでエラー型定義がネストしている場合で、エラー型の変換が多段になってしまった際でも、 From::from(e) ではなく別な変換メソッドを挿入できるようになるので、余分な .map_err(...) をなくせるんじゃないかと考えているのですが、これはなんかthiserror自体の使い方を間違っているのかも...
use hooq::hooq;
use std::any::Any;
use std::error::Error;
use std::num::ParseFloatError;
use std::num::ParseIntError;
#[derive(thiserror::Error, Debug)]
enum ConversionError {
#[error("invalid interger string: {0}")]
InvalidIntegerString(#[from] ParseIntError),
#[error("invalid float string: {0}")]
InvalidFloatString(#[from] ParseFloatError),
}
#[derive(thiserror::Error, Debug)]
enum AppError {
#[error("DB error: {0}")]
DbError(#[from] sqlx::Error),
#[error(transparent)]
ConversionError(#[from] ConversionError),
#[error("Other error: {0}")]
Other(String),
}
trait IntoAppError {
fn into_app_error(self) -> AppError;
}
// ここはいつかマクロで自動生成的なことをしたい...
impl<E: Error + 'static> IntoAppError for E {
fn into_app_error(self) -> AppError {
let s = self.to_string();
let b: Box<dyn Any> = Box::new(self);
if b.is::<ParseIntError>() {
return AppError::ConversionError(ConversionError::InvalidIntegerString(
*b.downcast::<ParseIntError>().unwrap(),
));
}
if b.is::<ParseFloatError>() {
return AppError::ConversionError(ConversionError::InvalidFloatString(
*b.downcast::<ParseFloatError>().unwrap(),
));
}
if b.is::<sqlx::Error>() {
return AppError::DbError(*b.downcast::<sqlx::Error>().unwrap());
}
if b.is::<ConversionError>() {
return AppError::ConversionError(*b.downcast::<ConversionError>().unwrap());
}
if b.is::<AppError>() {
return *b.downcast::<AppError>().unwrap();
}
AppError::Other(s)
}
}
fn run() -> Result<(), AppError> {
Ok::<(), AppError>(())?;
let int_str = "abc";
let _num: i32 = int_str
.parse()
.map_err(ConversionError::InvalidIntegerString)?;
Ok(())
}
#[hooq]
#[hooq::hook_targets("?")]
#[hooq::method(match $expr {
Ok(v) => v,
Err(e) => return Err(e.into_app_error()),
}!)]
fn run2() -> Result<(), AppError> {
Ok::<(), AppError>(())?;
let int_str = "abc";
let _num: i32 = int_str.parse()?; // .map_err(ConversionError::InvalidIntegerString) が不要!!!
Ok(())
}
fn main() {
run().unwrap_err();
run2().unwrap_err();
}
Cargo.toml
[package]
name = "thiserror_pg"
version = "0.1.0"
edition = "2024"
[dependencies]
hooq = { version = "0.3.1", features = ["consume-question"] }
sqlx = "0.8.6"
thiserror = "2.0.17"
やりたいことに対して装置が大掛かりになっちゃったけど...伝われ!!!
まとめ・所感
? の置換機能は、hooq属性マクロは絶対に使い道があるだろうと信じて開発していく中で取り付けたやりすぎ機能かなと個人的には思っているのですが、こうしてアドカレネタになったから良いかな...
ともかく、ここまで読んでくださりありがとうございました!
-
match式の枝についてはOperator expressions - The Rust Referenceを根拠としています。ちゃんと公式ドキュメントを探しつくしたり実装を見れたりしていませんが、まだ安定化されていないTryトレイトに準拠するとこのように単純に表せるものではないかもしれません。本記事ではこれで意味的にはほぼ等価だろうと判断して脱糖結果としました。...情報求ム! ↩