小ネタです
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を使いたくなる経緯
以下のように普通に書いてみます。(↓一見しただけで、動かないことが分かる人もいると思いますが、しばしお付き合いください)
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())
}
}
見た目はすっきりしましたね。
エラーが消えてコンパイルも通りました
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()
の実装を見ることですでに知っています。しかし、S
とimpl 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
を推論できない問題が解消されます。
最後まで読んでいただきありがとうございました。
-
S
はジェネリックな型であるのに対して、fmt().json().with_writer(stderr).finish()
の型はひとつに定まっています。 ↩ -
戻り値型をきちんと書く場合は、
with_writer()
にstderr
を渡している箇所で、stderr as fn () -> Stderr
のようなキャストが必要です。 ↩ -
記事の読みやすさの都合このように書きましたが、この書き方でも当然後述のエラーが出るので、このように書くなら
impl Logger<FmtSubscriber<JsonFields, Format<Json>, LevelFilter, fn() -> Stderr>> { pub fn json() -> Self {…} }
のように書くべきですね。 ↩