Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

フレーバーでプリセットを作成/利用する

前のレッスン の最後に、 Option 型のメソッド ok_or_elseResult 型にはないためにフックをスキップするという話をしました。

そもそも ResultOption の両方にあるメソッドをフックすることはできないでしょうか…? anyhowクレートが提供するメソッド Context::with_context がまさにうってつけです。 Option 型に対して .with_context(...) を呼ぶと、 None の時は anyhow::ResultErr バリアントへと変換を行ってくれます。

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, eyretracingといったクレートに対してもフレーバーを用意しています。

今まで #[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 = フレーバー名] という記法が出てきました。こちらはフレーバーの設定項目を部分適用するための機能です。その他の設定項目に関しても同様に部分適用が可能です。詳しくは属性の方を参照ください。

とても地味な注意なのですが、 `.$so_far` メタ変数による上書きは(フレーバーによって)最初に設定されたメソッドからの上書きを設定するものであるため、途中のアトリビュートからのみ可能になります。

言い換えると、 #[hooq(フレーバー名)] で指定したフレーバーのmethodには $so_far を含めることはできず、 #[hooq(my_flavor::ok_or_else)] はフレーバーの指定方法としては正しいのですがコンパイルエラーになってしまいます。

#[hooq::method = my_flavor::ok_or_else] は上書きに当たるため .$so_far の利用が可能です。