Rustコトハジメ

プログラミング言語Rustに関する情報をお届けします。

impl T for Box<T>パターン

GitHub - akiradeveloper/ijk: A real editor for real programmers

現在、私はエディタを開発しています。その中で知った設計パターンについて紹介します。

問題の定義

まず、私のエディタは、

ijk design doc - Google スライド

のデザインドックに書いたように、描画するデータ(View)を作る時に、Viewをオーバーレイしたり、横に並べてマージしたりします。それはまさに、ReactでいうViewのようなものだと思ってもらっていいです。マージの一つは、V1: View -> V2: View -> VerticallyMergedView<V1,V2>のような型だとします。

このViewはtraitです。オーバーレイしたりマージしたりというクラスは、ちょうどIteratorのMapとかZipのように、遅延計算をするラッパーとして実装されています。これらは静的ディスパッチされます。従って、ラップしまくってる間は、なんとかView<なんとかView, なんとかView<なんとかView>>のような巨大な型が生成されていくことになります。

何かしらのモデルからViewを生成する人をViewGenと呼んでいます。ViewGenはArea -> Box<View>のような計算が出来る人だと思えばいいです。ここで、Boxを返すのはViewがtraitだからです。Sizedしか返り値に出来ません。静的ディスパッチしまくったあとにBoxにして返すというのは一つの設計パターンですが、この記事で紹介したいのはその先の話です。

さて、こうすると困ったことが起きます。例えば、以下のようなコードを考えます。

let view_a = HorizontallyMergedView::new(なんとかView, なんとかView); // これは静的ディスパッチ。Box<View>ではない
let view_b: Box<View> = view_gen_b.gen(area_b);
let view_c = VerticallyMergedView::new(view_a, view_b); // こいつが問題

問題は、最後の行に起きます。Box<View>Viewではないので、コンパイルが通りません。

これを解決しましょう。

解決編

STEP1: BoxをViewにしてみる

これが私が最初に書いたコードです。このようなblanket implementationを作るのが解法であるというのは、Rustのライブラリコードを今まで色々と読んできた経験からわかります。

impl <V: View> View for Box<V> {
    fn get(&self, col: usize, row: usize) -> ViewElem {
        self.get(col, row)
    }
}

しかしこのコードは、「Boxコンパイル時にサイズがわからんから静的ディスパッチ出来ない」と怒られます。

STEP2: Vに?Sizedを足す

次に私が書いたのは、「じゃあVがdynでも良いようにするか」という直接的な対応です。こうするとコンパイルが通るようになります。

impl <V: View + ?Sized> View for Box<V> {

しかしこのコードを実行すると、無限再帰してスタックオーバーフローします。

STEP3: selfをderefしてあげる

なぜ無限再帰したかというと、&Box<V>.getを呼ぶと、V.getを呼ばずに、自分自身を呼び続けていたからです。従って、V.getを呼ぶようにderefしてあげる必要があります。

impl <V: View> View for Box<V> {
    fn get(&self, col: usize, row: usize) -> ViewElem {
        (**self).get(col, row)
    }
}

最終的にこうすると、ちゃんと動くようになりました。

Iteratorも同じ実装をしている

私のViewは、Iteratorのような設計をしていると言いましたが、実はIteratorも同じく

impl<I: Iterator + ?Sized> Iterator for Box<I> {

を実装しています。

まとめ

何かしらtrait Tを定義して、ラッパーによって静的ディスパッチしていくという設計を考えた場合、Box<T>を最終的に返すコードが生まれるということはRustにとって必然で、その場合に対応するために、このようにBox<T>型に対する実装impl T for Box<T>を書くというのがRustでは自然な設計パターンなのだと思いました。

ちなみにこうやってViewを合成して、今はこんなものが表示されています。色をつけるのはオーバーレイ、ライン番号や下のバーなどは垂直方向・水平方向にマージしています。

f:id:akiradeveloper529:20190314170148j:plain