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

小ネタです

<details>
  <summary>tl;dr</summary>

  ```rust
  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();
      }
  }
  ```
</details>


## やりたいこと

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

```rust
fn new() -> Logger<impl Subscriber>
```

や

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

### 詳しい状況

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

```rust
use tracing::Subscriber;

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

<details>
  <summary>（クリックして展開）さらに以下のようなimplが続く…</summary>

  ```rust
  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();
      }
  }
  ```
</details>

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

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


## 問題

### impl Traitを使いたくなる経緯

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

```rust
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`と一致しないので怒られています。[^2]

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

以下のように、戻り値型をきちんと書くことでこのエラーは回避できそうにみえます。[^1][^3]
しかしこの戻り値型、なんだか長いうえに`fmt()`に[`SubscriberBuilder`](https://docs.rs/tracing-subscriber/0.3.18/tracing_subscriber/fmt/struct.SubscriberBuilder.html)のメソッドをチェーンするとさらにどんどん複雑な型になっていきます。subscriberの細かな挙動を変更する度に長い戻り値型がどう変わるかを調べて修正するというのは、できなくはないですがあまりスマートではないような気がします。
[^1]: 戻り値型をきちんと書く場合は、`with_writer()`に`stderr`を渡している箇所で、`stderr as fn () -> Stderr`のようなキャストが必要です。
[^3]: 記事の読みやすさの都合このように書きましたが、この書き方でも当然後述のエラーが出るので、このように書くなら`impl Logger<FmtSubscriber<JsonFields, Format<Json>, LevelFilter, fn() -> Stderr>> { pub fn json() -> Self {…} }`のように書くべきですね。
```rust
pub fn json() -> Logger<FmtSubscriber<JsonFields, Format<Json>, LevelFilter, fn() -> Stderr>>
```


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


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

### impl Traitを使ってみるが……

使ってみましょう。

```rust
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
```

しかし

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

```rust
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()`の実装を見ることですでに知っています。しかし、`S`と`impl Subscriber`は、コンパイラにとってはまったく無関係な2つの型なので、コンパイラは`impl Subscriber`がどんな型か知っていても`S`についてはノーヒントです。そのため`S`の推論に失敗しているのですね。

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



```rust:①
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())
    }
}
```

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

どうしましょう。

## 解決策

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

```diff
- 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を含んだ型を返すコンストラクタをその中に移動します。

```rust
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`を推論できない問題が解消されます。

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