LoginSignup
1
1

Rust小技: コンストラクタからジェネリクスにimpl Traitを含んだ型を返す方法

Last updated at Posted at 2024-06-17

小ネタです

tl;dr
use std::io::stderr;
use tracing::Subscriber;
use tracing_subscriber::{
    fmt::fmt,
    util::SubscriberInitExt,
};

pub struct Logger<S>(S);

impl Logger<()> {
    /// JSON形式のログをstderrに出力するロガーを作成する
    pub fn json() -> Logger<impl Subscriber> {
        Logger(fmt().json().with_writer(stderr).finish())
    }
}

impl<S> Logger<S>
where
    S: Subscriber + Send + Sync + 'static,
{
    /// `self` をプログラム全体で使われるロガーに設定する
    pub fn set_as_global(self) {
        self.0.init();
    }
}

やりたいこと

以下のようなシグネチャのコンストラクタを作る

fn new() -> Logger<impl Subscriber>

/// JSON形式のログをstderrに出力するロガーを作成する
fn json() -> Logger<impl Subscriber>

詳しい状況

以下のようにtracing::SubscriberをラップしたLogger型があったとします。

use tracing::Subscriber;

pub struct Logger<S: Subscriber + Send + Sync + 'static>(S);
(クリックして展開)さらに以下のようなimplが続く…
use tracing_subscriber::util::SubscriberInitExt;

impl <S> Logger<S>
where
    S: Subscriber + Send + Sync + 'static,
{
    /// `self` をプログラム全体で使われるロガーに設定する
    pub fn set_as_global(self) {
        self.0.init();
    }
}

ここで、このLogger型に対して、1行目のように呼び出せるコンストラクタを実装したいです。
どうすれば良いでしょうか?

let logger = Logger::json();
logger.set_as_global();

問題

impl Traitを使いたくなる経緯

以下のように普通に書いてみます。(↓一見しただけで、動かないことが分かる人もいると思いますが、しばしお付き合いください:bow:

use tracing_subscriber::fmt::fmt;

impl <S> Logger<S>
where
    S: Subscriber + Send + Sync + 'static
{
    /// JSON形式のログをstderrに出力するロガーを作成する
    pub fn json() -> Logger<S> {
        Logger(fmt().json().with_writer(stderr).finish())
    }
}

コンパイルエラーが出てしまいました。

error[E0308]: mismatched types
  --> src/lib.rs:13:16
   |
7  | impl<S> Logger<S>
   |      - this type parameter
...
13 |         Logger(fmt().json().with_writer(stderr).finish())
   |         ------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected type parameter `S`, found `FmtSubscriber<JsonFields, Format<Json>, ..., ...>`
   |         |
   |         arguments to this struct are incorrect
   |
   = note: expected type parameter `S`
                      found struct `FmtSubscriber<JsonFields, Format<Json>, LevelFilter, fn() -> Stderr {stderr}>`

fmt().json().with_writer(stderr).finish() の部分の型がSと一致しないので怒られています。1

以下のように、戻り値型をきちんと書くことでこのエラーは回避できそうにみえます。23
しかしこの戻り値型、なんだか長いうえにfmt()SubscriberBuilderのメソッドをチェーンするとさらにどんどん複雑な型になっていきます。subscriberの細かな挙動を変更する度に長い戻り値型がどう変わるかを調べて修正するというのは、できなくはないですがあまりスマートではないような気がします。

pub fn json() -> Logger<FmtSubscriber<JsonFields, Format<Json>, LevelFilter, fn() -> Stderr>>

戻り値型を手書きできない/手書きするのが大変だけど、それが特定のトレイト(今回はSubscriber)を実装していることがわかっている……

……といえば、impl Traitですね。

impl Traitを使ってみるが……

使ってみましょう。

impl <S> Logger<S>
where
  S: Subscriber + Send + Sync + 'static
{
    /// JSON形式のログをstderrに出力するロガーを作成する
    pub fn json() -> Logger<impl Subscriber> {
        Logger(fmt().json().with_writer(stderr).finish())
    }
}

見た目はすっきりしましたね。
エラーが消えてコンパイルも通りました:raised_hands:

    Checking example v0.1.0 (…)
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s

しかし

呼び出し側も書くと、今度は呼び出し側でエラーが出ました。

let logger = Logger::json();
logger.set_as_global();
error[E0282]: type annotations needed
 --> src/main.rs:4:18
  |
4 |     let logger = Logger::json();
  |                  ^^^^^^^^^^^^ cannot infer type of the type parameter `S` declared on the struct `Logger`
  |
help: consider specifying the generic argument
  |
4 |     let logger = Logger::<S>::json();
  |                        +++++

LoggerのジェネリックパラメータSを推論できなかったらしいです。

Logger<S>json()関数を呼び出すと、Logger<impl Subscriber>が返ります。このimpl Subscriberがどのような型かは、コンパイラはjson()の実装を見ることですでに知っています。しかし、Simpl Subscriberは、コンパイラにとってはまったく無関係な2つの型なので、コンパイラはimpl Subscriberがどんな型か知っていてもSについてはノーヒントです。そのためSの推論に失敗しているのですね。

もし先述のように戻り値型を具体的に書いていたなら①のように書くことができましたが、impl Traitは使える場所が限られているので②のような書き方はできません。

impl Logger<FmtSubscriber<JsonFields, Format<Json>, LevelFilter, fn() -> Stderr>> {
    /// JSON形式のログをstderrに出力するロガーを作成する
    pub fn json() -> Self {
        Logger(fmt().json().with_writer(stderr as fn () -> Stderr).finish())
    }
}
impl Logger<impl Subscriber> {
    /// JSON形式のログをstderrに出力するロガーを作成する
    pub fn json() -> Self {
        Logger(fmt().json().with_writer(stderr).finish())
    }
}

どうしましょう。

解決策

まずは、Loggerの宣言時のSのトレイト境界を削除します。このように、構造体の宣言時にはトレイト境界を設けずに、implでだけトレイト境界を設けているのはOSSのライブラリでも時々見かけますね。

- pub struct Logger<S: Subscriber + Send + Sync + 'static>(S);
+ pub struct Logger<S>(S);

impl<S> Logger<S>
where
    S: Subscriber + Send + Sync + 'static,
{
    /// JSON形式のログをstderrに出力するロガーを作成する
    pub fn json() -> Logger<impl Subscriber> {
        Logger(fmt().json().with_writer(stderr).finish())
    }

    /// `self` をプログラム全体で使われるロガーに設定する
    pub fn set_as_global(self) {
        self.0.init();
    }
}

最後に、Logger<()>のimplを用意し、impl Traitを含んだ型を返すコンストラクタをその中に移動します。

pub struct Logger<S>(S);

impl Logger<()>
{
    /// JSON形式のログをstderrに出力するロガーを作成する
    pub fn json() -> Logger<impl Subscriber> {
        Logger(fmt().json().with_writer(stderr).finish())
    }
}

impl<S> Logger<S>
where
    S: Subscriber + Send + Sync + 'static,
{
    /// `self` をプログラム全体で使われるロガーに設定する
    pub fn set_as_global(self) {
        self.0.init();
    }
}

これによって、json()関数を持つLogger<S>Logger<()>しか存在しなくなるので、呼び出し側でSを推論できない問題が解消されます。

最後まで読んでいただきありがとうございました。

  1. Sはジェネリックな型であるのに対して、fmt().json().with_writer(stderr).finish()の型はひとつに定まっています。

  2. 戻り値型をきちんと書く場合は、with_writer()stderrを渡している箇所で、stderr as fn () -> Stderrのようなキャストが必要です。

  3. 記事の読みやすさの都合このように書きましたが、この書き方でも当然後述のエラーが出るので、このように書くならimpl Logger<FmtSubscriber<JsonFields, Format<Json>, LevelFilter, fn() -> Stderr>> { pub fn json() -> Self {…} }のように書くべきですね。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1