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

はじめに

hooq は、 ? 演算子1return 、末尾の式などに対し、必要に応じてメソッドを挿入できる属性マクロです。

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 マクロが何をしたのか、それは 後ほど 解説していきます。


  1. 旧tryマクロ

  2. この規模なら unwrap を使えばよいだろうって…?君のような勘のいいガキは嫌いだよ…。そうは言っても Result 型が使えないのは不便なはずなので、hooqマクロの意義は依然としてあるでしょう。