[Rust] anyhow::Context を use したいが名前が被ってしまうときの解決策 -> impl-only-use

公開:2021/02/11
更新:2021/02/11
4 min読了の目安(約3600字TECH技術記事

anyhow::Context は便利

2021年2月現在、Rust でエラーを扱う上でのデファクトスタンダードは anyhow (と thiserror)です。これらの使い方などは以下の記事によくまとめられているので、必要に応じてご参照ください。

https://cha-shu00.hatenablog.com/entry/2020/12/08/060000

anyhow は anyhow::Resultanyhow::Error のような便利な型に加えて、 anyhow::Context というトレイト も提供しています。このトレイトは、Option から anyhow::Result へと変換したい場合に特に役に立ちます。例えば、以下のコードを見てみましょう:

use anyhow::Context;

fn index_of(values: &[i32], target: i32) -> anyhow::Result<usize> {
    values
        .iter()
        .position(|&v| v == target)
        .with_context(|| format!("{} is not found", target))
}

この関数 index_of は、引数で &[i32]i32 を受け取って、目的の値がスライスの何番目に位置しているのかを求めます。目的の値が存在する場合には Ok、存在しなかった場合には Err で返すような実装にしたいとします。
「目的の値を見つける」部分は、上記のようにイテレータに生えている position というメソッドを使うのが便利です。ただし、このメソッドは Option<usize> を返してくるので、これをどうにかして anyhow::Result に変換する必要があります。
ここで使えるのが anyhow::Context トレイトです。このトレイトは任意の Option 型に対して実装されているので、use anyhow::Context をインポートするだけで使えるようになります。上記の例の中に登場している with_context というメソッドは anyhow::Context が提供しているもので、これによって Optionanyhow::Result に変換することができます。

Context って名前、汎用的すぎない?

さて、このように anyhow::Context は脇役ポジションではあるものの割と使いどころがある便利トレイトなのですが、この記事ではトレイト自体の紹介をしたいわけではありません。この記事で解決したいのは、

Context って名前が被ったときにはどうすればいいの?

という問題です。Context という名前はそこかしこに出てきます。例えば、Rust で書かれている TypeScript リンターである deno_lint では、各リントルールが以下のようなトレイトで表現されています:

pub trait LintRule {
    // ルールのインスタンスを作成
    fn new() -> Box<Self> where Self: Sized;

    // リントを実行する
    fn lint_program(&self, context: &mut Context, program: &Program);

    // ルールごとに定まっているコードを返す
    fn code(&self) -> &'static str;

    // ルールの種別を返す
    fn tags(&self) -> &'static [&'static str] {
        &[]
    }

    // ルールのドキュメント文言を返す
    fn docs(&self) -> &'static str {
        ""
    }
}

リントの実際の処理内容は lint_program メソッドに書かれるのですが、このメソッドの引数に Context というのがあります。これはリントを実行する際に役立つデータやメソッドがまとめられているもので、例えばリント対象のソースコードの変数スコープを解析したデータが格納されていたり、リントエラーを発行したい場合に context.add_diagnostic(..) というようにメソッドを呼び出したり、といった具合です。

Context が名前被りしてしまったときの(素朴な)回避策

では、この lint_program の実装で anyhow::Context を使いたくなくなったらどうすればよいでしょうか? Context という型名はすでに使われてしまっているので、use anyhow::Context をすることはできません。

とりあえず、Rust の useas XXX というように as で別名をつけることができるので、それを使えば回避することができます。

use anyhow::Context as AnyhowContext;

これでとりあえずは大丈夫ですが、冷静になって考えてみると、我々が anyhow::Context をインポートしたいのは、「anyhow::Context が提供しているメソッドを Option 型に対して利用したいから」であって、「anyhow::Context トレイトを自前の型に実装するため」ではありません。つまり、上記のように use anyhow::Context as AnyhowContext; というふうにしたとして、わざわざ名前を付けた AnyhowContext が自分たちのコードの中で登場することはないのです。なんかもったいない感じがします。

Context が名前被りしてしまったときの華麗な回避策

そこで Rust では華麗な方法が用意されています。以下のように書くことができます!

use anyhow::Context as _;

これは "impl-only-use" とか "underscore imports" と呼ばれているもので、「インポートはするけど、名前を付ける必要はないです」ということを表明できます。これで anyhow::Context をスコープに持ってきつつ、名前空間は汚さない、ということが両立できるのです。

"impl-only-use" の RFC は以下です(RFC 2166)。

https://github.com/rust-lang/rfcs/blob/master/text/2166-impl-only-use.md

"underscore imports" の説明は The Rust Reference にあります(RFC の要約のような感じです):

https://doc.rust-lang.org/reference/items/use-declarations.html#underscore-imports

というわけで、「トレイトに生えているメソッドを使うためにトレイトを use する必要があるけど、名前空間を汚したくない」というケースでは use SomeTrait as _; とすれば良い、ということの紹介でした。