怠惰なエンジニアのためのコード生成

高橋一平氏(以下、高橋):では、怠惰なエンジニアのためのコード生成ということで、リクルートマーケティングパートナーズの高橋が発表させていただきます。

さっそくですが、みなさんコードを書くのは好きでしょうか? 僕はあんまり好きではありません。コードを書いていて、ロジックを書くのはおもしろいですが、ボイラープレートや定型文的なコードなどをどうしても書かなければならないことは多いと思います。

こうしたボイラープレートを何度も書くのはしんどい。共通化、設計とかで解決しようとしても限界があるということで、コード生成をして解決しようというのが今回のお話です。

本発表でお話することですが、直接的なコード生成の話。ソースコードをファイルで書き出すという話と、マクロなどを使ったメタプログラミングによって先ほどの問題を解決しようという2つのお話をさせていただきます。

まず、コード生成をするにあたっての前提条件ですが、きちんと設計がなされている、クラスや関数の役割が明確になっていることが前提となってきます。

ルールがないような煩雑なめちゃくちゃなものに関してはコード生成することはできません。それに加えて、副作用がなかったりだとかDIをしっかり使っていたりだとか、テストのしやすいプログラミングになっていることが、まずコード生成よりも先にやらなければならないことだと考えています。

パーサを書こう

さっそく、コード生成の具体的な方法について話していきます。まずはパーサを書きましょうということなんですが、パーサというのは、構造化されている対象に対してだったら必ず書けます。

例えばXMLであったり、JSONであったり、Protocol Buffersだったり、SQLであったり、はたまた誰かが作ったExcelのフォーマットだったりとか、いろいろあると思いますが、構造化されていれば必ずパースはできる。

開発言語にも依存しない方法なので汎用的にも使えます。例えばSwiftで開発してます、だけどパーサはHaskellで書いてみてSwiftのコードを生成しますとか。そういう使い方でもいいと。けっこう汎用性の効く方法なので、困ったらパーサを書こうということですね。

パーサを使った具体的なコード生成の方法について説明していきます。スキーマ情報を利用したコードの生成の方法ですね。

まずスキーマをまるごとダンプして、それをプログラムで扱いやすいかたちへパースする。

このときにSQLの構文すべてを網羅したようなパースを書かなければならないわけではなくて、ダンプしたものがどういったかたちになるのかに基づいて、ある程度限定したかたちでパースを書いて作るくらいで十分だと思います。

そのパースしたものからDAO関連のソースコードを生成できます。例えばデータモデルやDAO、あとDAOのテストコード、またデータモデルのファクトリなど。そういったものに関してスキーマの情報をもとに自動生成することができて、実際に我々のサービスでもこういったことをやっています。

メタプログラミングを用いる

続いてメタプログラミングについてです。メタプログラミングというのはプログラムを扱うようなプログラミング手法のことで、プログラムを入力したり出力したりするようなプログラミング手法のことです。

メタプログラミングをすることによってなにが嬉しいかと言うと、構文木だとか型の情報にアクセスすることができます。ということはメタプログラミングを利用することで、利用しないようなプログラミングと比べてプログラムの表現力が高まるということが挙げられます。

具体的にメタプログラミングを使ってどういったことができるのかをみていきたいと思います。我々のプロダクトでもこういうことをやっているんですけれども、テストコードのテンプレートの生成です。

ユースケースだとかそういったもののテストのコードの生成だと考えてもらえればいいんですが、例えばテストしたい対象のコードを文字列として読み込む。そこからマクロなどを使って構文木の情報を持ってくる。

そうすると何ができるかと言うと、パースされた構文木の情報がわかるので、モックすべき関数がどんなものかがわかる。モックすべき関数がわかれば何ができるかと言ったら、そこからテストコードのテンプレートを生成することができると。

前提条件としてきちんとクラスの役割が明確であるだとか、テストができる状態になっているというのはここに関連することで、ユースケースとか、書き方が統一されていれば比較的メタプログラミングによってテストコードを作るみたいなことはやりやすいんですけれども。これが人によってバラバラとか、そういった感じで作られていると、なかなかこういう生成は難しくなってきます。

ダミーデータを簡単に作る方法

続きまして、ダミーデータを簡単に作る方法ですね。テストを書いていて1番しんどいことは何かと言うと、たぶんダミーデータを作るのがとてもしんどいことだと思います。

巨大なクラスですと1つのユースケースにいくつものモデルが関連していたりして、何種類ものオブジェクトのダミーデータを作らなければならない。テスト用のダミーのデータを作らなければならないということで、これを手でやろうとするととてもしんどいと。

これまで話していたように、ユースケースのパースをしたりとか、メタプログラミングを使ってクラスごとにダミーデータのファクトリを作るっていう解決方法もあります。僕たちももともとそういうクラスごとのダミーデータを生成するという方法でこれを解決していたんですけれども。

クラスごとにファクトリを作らないといけないということで、手でやるとしんどい。生成するにしても、生成したファイルはけっこう膨大なのでメンテナンスのコストが大きくなってしまう。加えて、モデルの書き方がちょっと変わったりすると全体的に影響が出たりすることもあります。

では、これを楽に解決する方法はないのかということで、辛くない解決方法。どんな型のインスタンスだろうが作れる汎用的なファクトリというものを定義できればこれを解決できると考えました。

具体的にどうやってそれを実現するかと言うと、あらゆる型ということなので、あらゆる型が実際どんなかたちをしているのかを一般化することができれば、どんな型のインスタンスでも作れるファクトリというものが作れます。そこで登場するのが直和型と直積型の考え方です。

直和型というのはA+Bで、AかBのどちらかを取るような型というふうに定義します。直積型というのはA×Bということで、AとBの2つの要素を持つような型。

直和型というのはEitherとか聞いたことあると思いますが、そういったものです。直積型というのはTupleとかですね。そういったデータ構造だと思っていただければと思います。

こういったかたちにいろんな型は書き換えることができます。

例えばAnimalという抽象クラスがあって、それを継承するDog、Catっていうクラスがあったとします。AnimalはDogかCatのどちらかなので、DogとCatの直和であると。

Catというのは、nameというStringの要素とageというIntの要素を持っている直積であると定義して、直和と直積だけで記述することができるます。

汎用ファクトリの実装方法

これを使って、構造に関しては全部直和と直積のかたちで言葉は定義できるので汎用的なファクトリを作ることができる。実際にどのように実装するかと言うと、ジェネリックプログラミングライブラリを使うという方法がたぶん1番一般的だと思っています。

Scalaだったらshapelessっていうライブラリなんですけど、そのあたりを使って作るのが一般的です。もう1つはメタプログラミングを使った方法ですね。自分で自力で実装するという方法があります。

我々のプロダクトではScalaを使っていて、実際にこのようなどんな型のダミーデータでも作れるファクトリというものを実装したんですけれども、それがこの例ですね。

1番上の行で、Dogというクラスを定義しました。その真下でTestObject [Dog] というふうに書いて、型引数を渡しています。これによってDogのダミーデータが生み出されました。

ここで注目してほしいのは、TestObjectというものを書いている段階ではDog型のダミーデータをどのように作るかを、実はTestObjectに与えていません。

これが実際に中では直積と直和のかたちに置き換えて、そこからまた戻してというようなことをやって作っているんですけれども。このようにしてイントロの作り方、Stringの作り方を個別に与えるだけでどんなインスタンスでも作れるようなダミーデータのファクトリを作ることができました。

今回はお話ししませんが、Stateモナドを使うことによって、これは固定のダミーデータなんですけど、ものよって違うダミーデータを作ることも可能になってきます。

ということで、まとめです。コード生成を使って怠惰にプログラミングをしましょう。メタプログラミングとかジェネリックプログラミングみたいなものを使っていくといいんですけれども。

こうしたことは言語に依存するところがあると思うので、メタプログラミングやあまり強くない言語を使っている場合にはパーサを使ってコード生成をすることでなんとかすることができます。

以上になります。ご静聴ありがとうございました。

(会場拍手)

テスト1回で1,000行→数十行に

司会者:高橋さん、ありがとうございました。ご質問がある方は挙手お願いいたします。ないですか? じゃあ僕から。僕、実はAndroidエンジニアなんですが、テストとかも書かなきゃいけなくてすごくしんどい気持ちにはなっています。そこで、実際にこれを導入してどれくらい楽になったのかなというのは気になります。

高橋:とくに効果が大きかったのは、汎用ファクトリです。本当に最初の状態ってファクトリすら定義されていなくて、テストを1個書くごとにダミーデータを上からガーっと書いて。1つのテスト書くのに1,000行書く、みたいなことがあったりして大変でした。

ダミーデータを作って、さらにテンプレートの生成みたいなものを組み合わせることによって正常系のテストを1個書くくらいだったら、自分で手で書かなきゃいけないのは数十行とかそのくらいで済むようになりました。生産性の点に関してはだいぶよくなったのかなと思います。

司会者:ありがとうございます。それでは、これで終了とさせていただきます。高橋さん、改めてありがとうございました。

(会場拍手)