公開APIのインターフェースで利用している外部クレートはRe-exportする（と良さそう）

この記事は[Rust Advent Calendar 2020](https://qiita.com/advent-calendar/2020/rust)の 10 日目の記事です。

- 9 日目はこちら → [no_std の Rust on Linux で Hello, world!する - Qiita](https://qiita.com/0yoyoyo/items/9a7c96e1250b50c6b339)
- 11 日目はこちら → [Rustの型システムの恩恵：言語処理系としての観点から - Qiita](https://qiita.com/moratorium08/items/0196afe8cb7e225e1e9d)

こんにちは、@tasshi です。
同日にうっかりもう 1 つアドベントカレンダーを登録していて[^1]、
慌てて書いているうちにこちらは 1 日遅れとなってしまいました。

[^1]: [kintoneで他のユーザの予定を確認するJSカスタマイズを作る - Qiita](https://qiita.com/tasshi/items/e35c4e877d9d5a00da9b)

まだ 11 日未明なのでセーフだと信じて強い気持ちで投稿します。

# 概要

Rust の Re-export の使い方について調べていたところ、気になるトピックがありました。

[Guidance around reexporting · Issue #176 · rust-lang/api-guidelines](https://github.com/rust-lang/api-guidelines/issues/176)

ざっくりと説明すると、
__「公開している API のインターフェースで外部クレートを利用している場合は、
その外部クレート全体を Re-export(再公開)するのが良い」__
という内容です。

現在も Open issue のため今後方針が変わる可能性もありますが、現時点ではこれが一番良さそうということで記事に書き起こしてみました。

# Re-exporting(再公開)について

`pub`キーワードと`use`キーワードを組み合わせると、スコープに持ち込んだ名前を外に対して公開することができます。
これを**Re-exporting(再公開)**と呼びます。

```rust
pub mod public_module {
    pub mod submodule_in_public {
        pub fn my_function() {}
    }
}

mod private_module {
    pub mod submodule_in_private {
        pub fn my_function() {}
    }
}

pub use crate::private_module::submodule_in_private; //非公開サブモジュールからRe-export
pub use crate::public_module::submodule_in_public; //公開サブモジュールからRe-export
```

TRPL (en): [Bringing Paths Into Scope with the use Keyword - The Rust Programming Language](https://doc.rust-lang.org/book/ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#re-exporting-names-with-pub-use)
TRPL (ja): [use キーワードでパスをスコープに持ち込む - The Rust Programming Language 日本語版](https://doc.rust-jp.rs/book-ja/ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#pub-use%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E5%90%8D%E5%89%8D%E3%82%92%E5%86%8D%E5%85%AC%E9%96%8B%E3%81%99%E3%82%8B)

Re-export は、以下のような場面で効果を発揮します。

- 階層の深いサブモジュールから外部公開するものを浅い階層へ取り出す
- 非公開サブモジュールで実装を隠蔽しつつ公開用インターフェースのみ公開する
- 外部モジュールをクレートの一部として公開する

Rust のモジュールシステムについては κeen さんの記事を読むのがおすすめです。
[Rust のモジュールの使い方 2018 Edition 版 | κeen の Happy Hacκing Blog](https://keens.github.io/blog/2018/12/08/rustnomoju_runotsukaikata_2018_editionhan/)

# 公開 API のインターフェースで他のクレートの型を受け取る/返す

さて、本題に入ります。
公開 API が外部モジュールの提供する型を受け取ったり、返したりする場合を考えます。

ここでは架空のクレート`imgconverter`を例として進めていきます。
`imgconverter`は`image::DynamicImage`を受け取って何かしらの変換を行う関数`convert_image`を提供しています。

```rust:imgconverter/lib.rs
use image::DynamicImage;

pub fn convert_image(image: DynamicImage) -> DynamicImage {
    // なんらかの処理
}
```

これをアプリケーション側で利用してみましょう。
`main.rs`では`image::DynamicImage`を生成し、`convert_image`関数で変換を行います。
Cargo.toml には crates.io から`image`クレートを探して最新バージョンを持ってきます。[^2]

[^2]: [image - crates.io: Rust Package Registry](https://crates.io/crates/image)

```rust:app/main.rs
use image::DynamicImage;
use imgconverter::convert_image;

fn main() {
    let src_image = DynamicImage::new_rgb8(800, 600);
    let dst_image = convert_image(src_image);
}
```

```toml:app/Cargo.toml
# 一部抜粋
[dependencies]
image = "0.23.12"
```

それではコンパイルしてみましょう。

```
❯ cargo build
   Compiling app v0.1.0
error[E0308]: mismatched types
 --> src/bin/main.rs:6:35
  |
6 |     let dst_image = convert_image(src_image);
  |                                   ^^^^^^^^^ expected enum `image::dynimage::DynamicImage`, found enum `image::DynamicImage`
  |
  = note: perhaps two different versions of crate `image` are being used?

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
error: could not compile `app`.
```

コンパイルエラーとなってしまいました。

### 原因

実は`imgconverter`は少し古いバージョンの`image`クレートに依存していました。

```toml:imgconverter/Cargo.toml
# 一部抜粋
[dependencies]
image = "0.19.0"
```

引数として与えられた`image::DynamicImage`のバージョンが`0.23.12`であるのに対して、
`convert_image`の要求する`image::DynamicImage`のバージョンは`0.19.0`です。

要求するバージョンと与えられたバージョンとが違うためコンパイルエラーとなっています。

## 依存する型を Re-export する

先ほどのエラーは、ライブラリクレートの依存関係を確認して適切なバージョンの`image`クレートを使うことで防ぐことができます。
しかし、__クレートの利用者に毎回適切なバージョンの`image`クレートを見つけさせるのは開発体験が良いとは言えません。__

利用者をバージョン違いのストレスから開放するために依存している型を Re-export してあげましょう。

新しい`imgconverter`では`image::DynamicImage`をそのまま再公開しています。

```rust:imgconverter/lib.rs
pub use image::DynamicImage;

pub fn convert_image(image: DynamicImage) -> DynamicImage {
    // なんらかの処理
    return image;
}
```

アプリケーション側では`imgconverter::DynamicImage`を利用することで適切なバージョンの`DynamicImage`を利用できます。

```rust:app/main.rs
use imgconverter::{convert_image, DynamicImage};

fn main() {
    let src_image = DynamicImage::new_rgb8(800, 600);
    let dst_image = convert_image(src_image);
}
```

これで`main.rs`が正常にビルドできるようになりました。

## 返り値にさらに操作を加える

まだ十分ではありません。

変換された`dst_image`にさらに画像処理を加えてみましょう。
`image::imageops`には`DynamicImage`に対する様々な画像処理関数が用意されています。[^3][^4]

[^3]: [image::imageops - Rust](https://docs.rs/image/0.23.12/image/imageops/index.html)
[^4]: 正確には`image`クレートの提供する画像系の型全般に対する画像処理関数群です

今回は`image::imageops::flip_horizontal`を利用して画像を左右反転します。
さてどうなるでしょうか。

```rust:app/main.rs
use image::imageops;
use imgconverter::{convert_image, DynamicImage};

fn main() {
    let src_image = DynamicImage::new_rgb8(800, 600);
    let dst_image = convert_image(src_image);
    imageops::flip_horizontal(&mut dst_image);  // コンパイルエラー
}
```

これもバージョン違いでエラーになります。

このように直接インターフェースで利用している型だけを Re-export しても、そのクレートの他の機能を使うことができません。

## 依存する型を含むクレート全体を Re-export する

これを防ぐためにタイトルで出てきたクレート全体の Re-export を行います。

`imgconverter`では`image`を再公開します。

```rust:imgconverter/lib.rs
pub use image; //imageクレート全体をRe-export
use image::DynamicImage;

pub fn convert_image(image: DynamicImage) -> DynamicImage {
    // なんらかの処理
    return image;
}

```

アプリケーション側では再公開された`imgconverter::image`を利用します。

```rust:app/main.rs
use imgconverter::convert_image;
use imgconverter::image::{imageops, DynamicImage};

fn main() {
    let src_image = DynamicImage::new_rgb8(800, 600);
    let mut dst_image = convert_image(src_image);
    imageops::flip_horizontal(&mut dst_image);
}
```

これで利用者はインターフェースで利用されているクレートのバージョンを意識しなくて良くなりました。

## 検証コード

上記のバージョン違いのコードも含まれているのでビルドはできません。
[tasshi-playground/rust-re-export-dependencies: Investigation for usage of "Re-exporting"](https://github.com/tasshi-playground/rust-re-export-dependencies)

## 参考ページ

- [RFC API guideline - re-export a whole crate if exporting any - The Rust Programming Language Forum](https://users.rust-lang.org/t/rfc-api-guideline-re-export-a-whole-crate-if-exporting-any/17820)
  - `quickcheck`クレート（ランダムテストフレームワーク）が`rand::Rng`だけを再公開していて、利用者がバージョンを意識しないといけないことについての投稿
  - 本筋とは関係ないけど、バージョン 1.0 以上のクレートなんかほとんどないだろって言われてちょっと笑った
- [Crates which re-export types from other crates should also re-export the entire crate · Issue #170 · rust-lang/api-guidelines](https://github.com/rust-lang/api-guidelines/issues/170)
  - 上の議論の API guideline 本体への issue
  - > This has the problem that all users of quickcheck need to depend on the same version of rand as quickcheck if they want to use the rest of rand's API.
- [Guidance around reexporting · Issue #176 · rust-lang/api-guidelines](https://github.com/rust-lang/api-guidelines/issues/176)
  - おそらく現時点(2020/12/11)で最新の議論
  - > If your crate has a public dependency, then the corresponding crate should be reexported by your crate (via pub extern).

## 余談

__これって依存ライブラリ同士で Re-export しているクレートのバージョンが違った場合どうするんですかね。__