タイプセーフプリキュア!を支える技術 その2

~定義を自動でまとめる問題にGHCのANNプラグマで挑む~

Posted by Yuji Yamamoto(@igrep) on December 24, 2017

このエントリーはHaskell Advent Calendar 2017 24日目の記事兼プリキュア Advent Calendar 2017 24日目の記事です。
毎度の手口ですが、二つのAdvent Calendarに同時に投稿しています。

HaskellとプリキュアのAdvent Calendarということで、去年に引き続き「タイプセーフプリキュア!」について、開発する上で見つかった問題と、その解決方法について紹介します 1
なお、「タイプセーフプリキュア!」そのものの日本語の紹介については、私の去年のHaskell Advent Calendarの記事同じく去年のプリキュア Advent Calendarの記事をご覧ください。

Link to
here
問題提起

例えば、あなたはたくさんの仲間と、たくさんのサブコマンドがあるCLIアプリを作っていたとします。
コードの規約上、サブコマンド一つにつき一つのモジュールで、決まった関数Haskellであれば[String] -> IO ()みたいな型の関数でしょうか)を定義するものとします。
そうした場合、必ずどこかのモジュールで、各モジュールで定義したサブコマンドを表す関数を列挙する必要があるでしょう。
その場合、次のような問題が生じることがあります。

  • サブコマンド(を表す関数)を追加したとき、サブコマンドを列挙しているモジュールに、追加し忘れる。
  • 複数の開発者がそれぞれのブランチで、新たに作成したサブコマンドを列挙しているモジュールに追加すると、マージする際にコンフリクトがしばしば発生する。

また、DRY原則を徹底するならば「サブコマンドの名前を、サブコマンド自身の定義と列挙しているモジュールとで繰り返さない」というアイディアに基づき、こうした関数の列挙を避ける、という考え方もあるでしょう。
そのように作ることで、モジュールに関わる情報(どのような定義で、どのように使用されるのか)をなるべくモジュールのファイルのみに集約させることができ、モジュールに関する情報が分散してしまうのを軽減することができます。

つまり、今回実現したいことは、複数のファイルに散らばった特定の関数やデータ型の定義を、自動で一カ所にまとめて再利用する、ということです。
この記事で何度も使うことになるので「定義を自動でまとめる問題」と呼ぶことにします。
これをGHCの各種機能を利用して、Haskellで実現させる方法を考えましょう。

Link to
here
ほかの言語での例

こうした処理をHaskell以外のプログラミング言語で行う場合、例えば下記のような機能を使うことになるでしょう。
参考のために、私がこれまでに出会ったものを紹介します。

Link to
here
Rubyでの場合

前職時代、私は実際にこの「定義を自動でまとめる問題」に出くわしたのですが、Rubyを使っていたため、下記のようにModule#includedという、対象のモジュールをinclude(モジュールが提供する機能の継承)したときに呼ばれる、特別なメタプログラミング用のメソッドを使って解決しておりました。

このように書くことで、ListedAsSubCommand.listedというプロパティから、ListedAsSubCommandincludeしたClassオブジェクトのリストが取得できます。
実際に使用するときは、下記のように、対象のクラスが定義されているファイルを含んだディレクトリーからまとめてrequireした上で、ListedAsSubCommand.listedにアクセスする事になるでしょう。

Link to
here
Javaでの場合

Javaで「定義を自動でまとめる問題」を解決する場合も、Rubyと同様に、何らかの形でメタプログラミング用の仕組みを利用することになるかと思います。
とりわけ、Javaにおいては、この問題の解決に特化しているライブラリーの機能が存在している点が興味深いでしょう。Springの「コンポーネントスキャン」です。

SpringをはじめとするDIフレームワークでは、各クラスにおいて依存するオブジェクト(正確にはそのインターフェース)を宣言した際、必ず何らかの形で、「どのインターフェースにどのオブジェクトを紐付けるか」を宣言することになります。いわゆるApplication Contextを書いたXMLであったり、@Configurationアノテーションが着いたクラスがそれに当たります。
結果、モジュール(実際にはJavaなのでクラス)に関する情報、すなわちどのクラスのどのフィールドに、どのオブジェクトを注入するか、といった情報はすべてモジュールのファイルとは独立して管理することになり、DRYではなくなってしまいます。 まさに「定義を自動でまとめる問題」の典型と言えますね。

それに対してSpringの「コンポーネントスキャン」では、下記のように設定することで、「どのインターフェースにどのオブジェクトを紐付けるか」といった情報を、すべて自動で設定してしまうことができます。
下記はコンポーネントスキャンを@Configurationアノテーションが着いたJavaのクラスで設定した場合のサンプルコードです。

@Configurationアノテーションを付与したJavaのクラスに、更に@ComponentScanというアノテーションを付与すると、Springは、@ComponentScanアノテーションの引数として渡した名前空間以下に存在する、すべての@Componentというアノテーションが着いたクラスのオブジェクトを、自動的にほかの@Componentが着いたクラスのフィールドとして設定できるようにします2

このようにコンポーネントスキャンを使うことで、@ComponentScanされたクラスのオブジェクトは自動で依存するオブジェクトとして紐付けられるようになります。
従来foo-context.xmlみたいな名前のファイルに、どのオブジェクトのどのフィールドにどのオブジェクトを紐付けるか、といった情報を一つ一つ書いていたのを、ほとんど書かなくて済むようになりました。

Link to
here
解決に必要なもの

さて、私が経験した二つの言語における「定義を自動でまとめる問題」の解決方法を見てきたところで、この問題を解決するのに共通して必要なことを列挙しましょう。

(1) 対象となる「まとめたい定義(モジュールや関数、型など)」が書かれているファイルが、どのディレクトリー以下にあるか設定する

「定義を自動でまとめる問題」に取り組むに当たり、最低限必要となるのが、この設定です。
まさかファイルシステムにあるすべてのソースコードから「まとめたい定義」を探すわけにも行きませんし、プロジェクトのディレクトリーすべてを処理するのも、柔軟性に欠けた解決方法でしょう。そこで通例「定義を自動でまとめる問題」に対応する際は、「まとめたい定義(モジュールや関数、型など)」が書かれているファイルがどのディレクトリー以下にあるか、を何らかの形で書くことになります。

前述のRubyによる例の場合、この情報は下記のDir.globメソッドに渡した引数に当たります。
'path/to/commands/**/*.rb' という文字列のうち、 path/to/commands/ の部分ですね。

JavaにおけるSpringのコンポーネントスキャンの場合、@ComponentScanアノテーションに渡した引数が該当します。
厳密には、@ComponentScanアノテーションに渡す引数はディレクトリーのパスではなくJavaのパッケージの名前ですが、Javaではパッケージはクラスパス以下のディレクトリーと一対一で対応するよう作る必要があるので、事実上ディレクトリーのパスを渡していると言えるでしょう。

(2) 「まとめたい定義(モジュールや関数、型など)」が書かれたファイルに、なんらかの印をつける

「定義を自動でまとめる問題」では、「どの定義を自動でまとめるか」さえ指定できればよいので、理屈の上では前述の「(1) 対象となる『まとめたい定義(モジュールや関数、型など)』が書かれているファイルが、どのディレクトリー以下にあるか設定する」さえできれば、後はディレクトリー以下のファイルをすべて自動でまとめられるはずです。 しかし、それだけでは次の問題が生じてしまう恐れがあります。

  1. 「自動でまとめられるファイル」がどのように使用されるか理解しにくくなる。
    • 「自動でまとめられるファイル」に書かれた定義は、多くの場合、明確に使用される箇所で言及されなくなってしまいます。結果、そのファイルを読んだだけでは、書かれている定義がどこでどう使われているのか、そもそも本当に使われているのかどうかすら分からなくってしまいます。プロジェクトに新しく参加する人は、相応の学習が必要になってしまうでしょう。
  2. 細かい例外を設定しにくい。
    • 「まとめたい定義が書かれたファイル」を含むディレクトリーの中に、まとめる対象としたくないファイルを作る、ということがやりにくくなってしまいます。
    • 例えばサブコマンドの例で言えば、Commandsというディレクトリー以下に複数のサブコマンド(まとめられる対象)を置いたとき、各サブコマンドで共有されるユーティリティー関数もCommandsディレクトリー以下に置きたくなるかも知れません。もちろん状況に応じてほかのディレクトリーに置く手段も検討すべきですが、そうしたユーティリティー関数の入ったファイルは自動でまとめて欲しくないでしょう。

そうした問題を軽減するために、「定義を自動でまとめる問題」に対応する際は、必ず「『まとめたい定義(モジュールや関数、型など)』が書かれたファイルに、なんらかの印をつける」ことを検討した方がいいと思います。

前述のRubyによる例で言えば、これはinclude ListedAsSubCommandという、includedメソッドを実装したListedAsSubCommandモジュールをincludeすることが該当します。
JavaSpringのコンポーネントスキャンの場合、まさしく@Componentアノテーションがそれに当たるでしょう。

これらの印が着いたファイルを読む場合、この「印」を手がかりにして、コードベースを検索したり定義ジャンプしたり、Springの場合はインターネットを検索したりすることで、「印」の役割を知り、そのファイルがどう使われるのか調べることができるのです。

Link to
here
注意点

いよいよ次の節で「定義を自動でまとめる問題」をHaskellで解決した例を紹介いたしますが、その前にこの問題を解決することによって生じる、副作用について強調しておきましょう。 私の観測範囲内でですが、今までこの問題に対応した例を見たことがないのは、そうした副作用による悪影響が大きいと感じている人が多数派だからなのかも知れません。

それは、前節でも触れましたが、「『自動でまとめられるファイル』がどのように使用されるか理解しにくくなる」ということです。
この問題は、確かに「『まとめたい定義(モジュールや関数、型など)』が書かれたファイルに、なんらかの印をつける」ことである程度緩和可能な問題ではありますが、それでも強く意識するべきでしょう。
「自動でまとめられるファイル」を初めて読んだ人が、include ListedAsSubCommand@Componentという印に気づければよいのですが、そうでない場合、使用箇所を求めてコードベースをさまようことになってしまいます。
事前に「印」の存在を知らせておくに越したことはありません。

それから、「『まとめたい定義(モジュールや関数、型など)』が書かれたファイルに、なんらかの印をつける」ことを選択した場合、「まとめたい定義が書かれたファイル」を新しく追加したいとき、ファイルにその「印」を書き忘れてしまうことがある点も、覚えておくべきでしょう。
当初この「定義を自動でまとめる問題」を提起した際、自動でまとめなかった場合のデメリットしてあげた、

  • サブコマンド(を表す関数)を追加したとき、サブコマンドを列挙しているモジュールに、追加し忘れる。

という問題と本質的に同じです。
自動でまとめずに手で定義を列挙した場合と比べて、編集するファイルが少ない分、忘れる可能性は低いかもしれません。
ひな形に「印」を含めれば、さらに忘れる確率を下げることができるでしょう。手で一つのファイルに定義を列挙していた場合、そうした工夫はできません。
ですが、いずれにしても忘れてしまうリスクがあることは変わらないでしょう。

以上の通り、結局のところ、「定義を自動でまとめる」よう設定するか、単純にまとめたい定義を手で列挙するかどうかは、そうしたトレードオフを考慮しつつ落ち着いて考えるのを推奨します。
これから紹介する方法を採用する際も、ここであげた注意点については忘れないでください。

Link to
here
Haskellでの解決事例 - 「タイプセーフプリキュア!」におけるcure-index.jsonの実装

タイプセーフプリキュア!(パッケージとしての名前はtypesafe-precureなので、以下「typesafe-precure」と呼びます)では、最近の更新により、コンパイル時に「cure-index.json」と、「pretty-cure-index.json」いうファイルを生成するようになりました。
次のような内容のファイルです。

これは、変身アイテムからプリキュア、プリキュアに変身する前の女の子、それから浄化技や変身時の台詞まで、typesafe-precureで定義されているあらゆる情報をまとめたJSONです。
まさしく、プリキュアの定義を自動でまとめた「インデックス」となっております 3
ただし、残念ながら現時点では「キラキラ☆プリキュアアラモード」に収録されたプリキュアの情報しか、cure-index.jsonには記録されていません(理由は後で説明します)

名前の通り、pretty-cure-index.jsonにはcure-index.jsonをプリティープリントしたJSONが記録されています。
下記のようにcurlして確かめてみましょう。

さて、このcure-index.json、繰り返しになりますが、typesafe-precureで定義されている、すべてのプリキュアの情報をまとめたJSONとなっております。
ライブラリーとしてのtypesafe-precureでは、これらの情報は一つ一つがHaskellの型として定義[^detail-typesafe-precure]されており、cure-index.jsonは、それらの情報をコンパイル時に「自動でまとめる」ことで作成されます。決して、JSONからHaskellの型を作っているわけではありません。
詳細は冒頭にも挙げましたが、私の去年のHaskell Advent Calendarの記事同年のプリキュア Advent Calendarの記事をご覧ください。
ここではそれを実現するために使用した、Haskellで「定義を自動でまとめる」方法を紹介しましょう。

Link to
here
使用したGHCについて

…と、その前に、今回typesafe-precureのビルドに使用したGHCのバージョンを述べておきましょう。

typesafe-precureは現在(ver. 0.5.0.1)の時点において、通常GHC 8.0.2でビルドされています。
特にCIでの確認はしていませんが、GHC 7.10でもビルドできるはずです。
従って、使用しているtemplate-haskellパッケージは2.10.0.0から2.11.1.0となっています。

この記事で紹介する機能は、GHC(と、GHCに標準添付されるtemplate-haskellパッケージ)のバージョンによって、大きく変わる場合があります。
今回は「できない」としたことも、将来のGHCではできるようになっている(あるいは運悪くその逆もある)かもしれません。
あらかじめご了承ください。

なお、各バージョンのGHCに標準添付されているパッケージのバージョンについては、Commentary/Libraries/VersionHistoryGHCをご覧ください。

Link to
here
ANNで「まとめたい型」が書かれたモジュールに「印」を着ける

まず、「『まとめたい定義(モジュールや関数、型など)』が書かれたファイルに、なんらかの印をつける」方法を考えましょう。
実はHaskell(GHC)にもアノテーションがありますJavaのアノテーションと使い勝手が異なりますが)
ANNというGHCのプラグマ{-# ... #-} という形式で表される、特別なコメント)を使用すると、下記のように、モジュールや型、名前が付いた値に対して、アノテーションを加えることができます(例はアンッ!!!アンッ!!!! - Qiitaから拝借しました)

上記の通り、GHCANNは、Javaのアノテーションと異なり、アノテーション専用のインターフェースを作って引数を補足情報として渡す、というような形式ではありません(そもそもHaskellにはインターフェースなんてありませんしね)
Data型クラスのインスタンスである型の値であれば、なんでもアノテーションとして設定できます。

そのData型クラスのインスタンスですが、baseパッケージに含まれている多くの型に加え、DeriveDataTypeableというGHCの言語拡張を使えば、オリジナルの型も簡単にそのインスタンスにすることができます。

この、Data型クラスを使えば、実行時に型の構造を取得したりすることができます。
とはいえ、ここでは単純に{-# LANGUAGE DeriveDataTypeable #-}deriving Dataを「おまじない」として使うだけで差し支えありません。
詳しく知りたい方はWhat I Wish I Knew When Learning Haskell 日本語訳」の「ジェネリクス」の章をご覧ください。

さてtypesafe-precureでは、このData型クラスとANNプラグマを利用した次のようなアプローチで、各モジュールに対し、プリキュアやプリキュアに関する情報を「印」として付与しました。

  1. ACME.PreCure.Index.Typesというモジュールに、型の名前やインスタンスの定義を自動生成したり、それをJSONに変換したりするのに使う、中間データのための型を作る。

    • この、各種中間データ用の型をData型クラスのインスタンスとすることで、「まとめたい定義」が含まれたモジュールに、その中間データ用の値をANNプラグマで付与できるようにする。
  2. 名前がACME.PreCure.Textbook.*.Profilesという形式のモジュール4「キラキラ☆プリキュアアラモード」での例で、中間データの値(つまり各プリキュアや変身アイテムなどについての情報)を定義する。

    1. ACME.PreCure.Textbook.*.Profilesで定義した中間データを、ACME.PreCure.Textbook.KirakiraALaMode.Typesという形式のモジュールに対してANNプラグマで付与する(同じく「キラキラ☆プリキュアアラモード」での例

Link to
here
Stage Restrictionを避けるためにモジュールを分ける

先の手順で引用したコードをご覧になった方は、こんなことを疑問に思ったかも知れません。
中間データの値を定義するモジュールと、ANNで中間データの値を付与するモジュールとを分ける必要があるのか、と。
上記の例で言えば、一つのモジュール(ACME.PreCure.Textbook.KirakiraALaMode.Types)girlsを定義しつつANNで付与すればよいのではないか、ということです。
あるいはgirlsという名前をつけずに、

というような書き方はできないのか、ということです。

中間データの値をANNで使うだけならそれで問題ないのですが、typesafe-precureの場合、中間データの値からプリキュアや変身アイテムを表す型と、その型クラスのインスタンスを定義する必要があります。
なので、先ほどのコード例にあったACME.PreCure.Textbook.KirakiraALaMode.Profilesというモジュールでは、実際には{-# ANN module girls #-}の行の後に、girlsから、プリキュアに変身する女の子(を表す型)や、それに対して型クラスのインスタンスを宣言するTemplate Haskellのコードが続いています。
下記の$(declareGirls girls)という行がそれです。

詳細は冒頭でも挙げた私の去年のHaskell Advent Calendarの記事などをご覧いただきたいのですが、typesafe-precureでは、それぞれのプリキュアや、プリキュアに変身する女の子、変身に必要なアイテムなどを、すべて個別の型として定義しています。
そのため、中間データの値はJSONとしてまとめるだけでなく、個別の型として定義する必要もあったのです。
その結果、中間データの値は必ず名前をつけて使い回さないといけなくなるのです。

そして、ANNTemplate Haskellにおいて値に名前をつけて使い回す場合、「Stage Restriction」というやっかいな制限が顔を出してきます。
これは、「ANNで値を付与する式や、トップレベルの宣言などを生成するTemplate Haskellのコードでは、ほかのモジュールからimportされた名前しか参照できない」という制限です(詳しくは「できる!Template Haskell ()」をご覧ください)
これがあるために、中間データの値を含めた名前(上記のコードの場合girl)は、ANNTemplate Haskellで参照するモジュールとは一旦別のモジュールとして定義して、importして再利用するしかありません。

本来、「定義を自動でまとめる問題」に対応する目的の中には「モジュールに関わる情報(どのような定義で、どのように使用されるのか)をなるべくモジュールのファイルのみに集約させる」というものがありましたが、外部のファイルに書くボイラープレートが増えてしまい、この観点ではイマイチな実装になってしまいました。
この点については、後の節でよりよい方法を検討しましょう。

Link to
here
autoexporterで「まとめたい型」が書かれているモジュールが、どのディレクトリー以下にあるか設定する

前節までで紹介した方法により、ANNプラグマを使うことでプリキュアの情報が書かれたモジュールに、プリキュアの情報を「自動でまとめる」ための「印」を着けることができました。
続いて、ANNプラグマで「印」を着けたモジュールがどこにあるかを指定して、GHCに自動で回収させる方法を述べましょう。 「解決に必要なもの」の節で説明した、「対象となる『まとめたい定義(モジュールや関数、型など)』が書かれているファイルが、どのディレクトリー以下にあるか設定する」部分に当たります。

次の節で説明しますが、ANNプラグマで付与した情報は、「アンッ!!!アンッ!!!!」でも説明されているとおりreifyAnnotationsというTemplate Haskellの関数を使えば取得することができますが、該当のモジュールを何らかの方法で集めなくてはなりません。
私が調べた限り、少なくともTemplate Haskellを使う限りは、importしているモジュールから収集する方法しか見つかりませんでした。
Template Haskellのライブラリーのドキュメントでは、reifyAnnotationsするのに必要な、Module型の値を取得する方法として、thisModuleを使ってTemplate Haskellのコードを実行しているモジュールから取得するか、reifyModule関数を使ってthisModuleからthisModuleimportしているモジュールから取得するしか紹介されていないためです。

しかし、現状のGHCではTemplate Haskellをもってしても、指定したディレクトリー以下のモジュールを自動でimportするということはできません。
あまりユーザーに自由を与えてしまうと、却って混乱が生じる恐れがあるので敢えて実装していないのでしょう。
とは言え、だからといって「印」を着けたモジュールを一つずつ手でimportして列挙してしまっては、「定義を自動でまとめる問題」を解決できたとは言えなくなってしまいます。
そこで、今回は実践でもよく使われる、さらなる「裏技」を用いることにしました。
本節の見出しでネタバレしてしまっていますが、autoexporterというプログラムと、GHCのカスタムプリプロセッサーのためのオプションを使います。

autoexporterは、ドキュメントに書いてあるとおり、GHCのカスタムプリプロセッサーのためのオプション(-F -pgmF)、さらにはOPTIONS_GHCプラグマ組み合わせて、次のように使うことを想定して作られています。
以下は、typesafe-precureACME/PreCure/Textbook.hsというファイルからの抜粋です。

と、いっても1行だけですが😅

一つずつ解説しましょう。
まずOPTIONS_GHCプラグマですが、文字通りこれはghcコマンドに渡すオプションを、ファイル単位で指定するためのものです(もちろんすべてのオプションをファイル単位で指定できるわけではありません)
つまり、上記の場合-F -pgmF autoexporterというオプションが、ACME/PreCure/Textbook.hsというファイルでのみ有効になります。

続いて-Fオプションですが、これは「カスタムプリプロセッサー」という機能を有効にするためのものです。
これを有効にすると、有効にしたファイルを、続く-pgmFオプションで指定したプログラムで変換するようになります。
具体的には、-pgmFオプションで指定したプログラムに、

  1. 変換前のファイル名、
  2. 変換前のソースコードを含むファイルの名前(恐らく、一時ディレクトリーにコピーした、変換前のファイル名とは異なる名前と思われます)
  3. 変換後のソースコードを書き込むファイル名(これも一時ディレクトリーにあるファイル名なのでしょう)

という3つのコマンドライン引数を渡して、-pgmFオプションで指定したプログラムを実行します。
-pgmFで指定したプログラムが、3つめの引数として渡した名前のファイルに変換後のソースコードを書き込むことで、-Fを有効にしたファイルを、変換後のソースコードでそっくりそのまま差し替えます。
結果、-pgmFオプションで指定したプログラムは、自由に任意のHaskellのソースを生成できるようになります。まさにソースコードの自動生成にぴったりな機能と言えるでしょう。

ちなみにこの機能、hspec-discoverなどのパッケージでも使用されています。テストコードを複数のファイルに分けて書く場合はほぼ必ず使われるものなので、みなさんも「おまじない」として使用したことがあるでしょう-F -pgmFなんて文字列、ググラビリティーも低いですしね。)
そういえばこれもテストコードの「定義を自動でまとめる問題」を解決したものでしたね!

話がそれましたが、autoexporterはこのカスタムプリプロセッサーを利用することで、次のようなソースコードを自動生成します。
autoexporterのドキュメントにも同じことが書かれていますが、ここでもACME/PreCure/Textbook.hsを例に説明しましょう。

そう、(プリキュアが好きで)賢明なHaskellerのみなさんならお気づきでしょう。typesafe-precureACME/PreCure/Textbook/ディレクトリーに含まれている、(プリキュアの各シリーズを表す)すべてのモジュールをimportして、再エクスポートしているのです!

つまり、autoexporterはこのような、「責務を分割するためにモジュールを細かく分けたい、でもユーザーには一つのモジュールをimportしただけで使えるようにしたい」というライブラリー開発者のニーズに応えるため、よく行われているモジュールの書き方を自動で行うための便利コマンドなのです。

紹介が長くなりましたが、typesafe-precureではこのautoexporterを次のように使うことで、「まとめたい型(プリキュアや変身アイテムなどの情報)」が書かれているモジュールを集めています。

  1. 前述のACME.PreCure.Textbookモジュールでautoexporterを使うことで、ACME.PreCure.Textbook以下にある、「まとめたい型(プリキュアや変身アイテムなどの情報)」が書かれているモジュールをすべて自動的に再エクスポートする。
  2. ACME.PreCure.IndexモジュールがACME.PreCure.Textbookモジュールをimportすることで、実際にcure-index.jsonなどの書き出しを行うACME.PreCure.Indexモジュールが、ACME.PreCure.Textbookが再エクスポートしたすべてのモジュールを利用できるようになる。

実際のところOPTIONS_GHC -Fをもっとうまく使えば、ACME.PreCure.Textbook以下にあるモジュールを自動ですべてimportするモジュールと、それを利用してcure-index.jsonなどの書き出しを行うモジュールを、分けずに一つのモジュールで済ますこともできたでしょう。
今回は敢えてautoexporterを再利用することで、ACME.PreCure.Textbook以下にあるモジュールをすべて回収する処理を書かずに任せることにしました。
この件については後ほど再検討しましょう。

Link to
here
ANNプラグマで付与した定義情報から、JSONを書き出す

いよいよ、autoexporterを駆使して集めたモジュールから、ANNで付与したプリキュアの情報を取り出し、JSONに変換して書き出しましょう。
詳細はACME.PreCure.Indexモジュールや、ACME.PreCure.Index.Libモジュールのソースコードをご覧いただきたいのですが、ここでは簡単にアルゴリズムを解説します。

  1. 「現在のモジュール(ACME.PreCure.Index)」を取得する。
  2. 「現在のモジュール」がimportしているモジュールから、ACME.PreCure.Textbookモジュールを見つけて、取り出す(具体的には38行目から39行目)。
  3. 取得したACME.PreCure.Textbookモジュールがimportしている、プリキュアの情報を集めたモジュール(ANNプラグマでプリキュアの情報を付与したモジュール)をすべて取り出す(具体的には42行目から45行目)。
  4. 「プリキュアの情報を集めたモジュール」すべてから、ANNプラグマで付与されているプリキュアや変身アイテムなどの情報を集めて、種類ごとに一つのリストとしてまとめる(具体的には48行目から60行目)。
  5. 収集してできたIndexという型の値を、それぞれJSONに変換して書き込む(具体的には48行目から60行目)。

上記のアルゴリズムにおいても、Template Haskellの「Stage Restriction」と戦わなければならないということは注記しておきましょう。
つまり、ACME.PreCure.IndexにおけるTemplate Haskellのコードで繰り返し使う便利な関数は、ACME.PreCure.Indexとは別のモジュールで定義して、importして使わなければならないのです。
ACME.PreCure.Index.Libモジュールは、その制限を回避するためのモジュールです。

ともあれこうして、typesafe-precureではACME.PreCure.Indexモジュールをコンパイルする度に、各モジュールに定義されたすべてのプリキュアに関する情報を集めて、genディレクトリーにあるcure-index.jsonpretty-cure-index.jsonというファイルに書き出すことができました。
「定義を自動でまとめる問題」、これにて一件落着です!🎉
なお、自動生成されるファイルをGitで管理することはなるべく避けた方がよいことですが、cure-index.jsonの配布を簡単に行うため方策として用いることにしています。

Link to
here
うまくいかなかった方法 (+ 来年の「タイプセーフプリキュア!」についてちょっとだけ)

typesafe-precureにおける「定義を自動でまとめる問題」の解決方法はここまで述べたとおりですが、今後同じような問題に対応したくなったときのために、最初に思いついたけどうまくいかなかった方法や、後で思いついた別の解決方法をこの先の二つの節ででまとめておきます。
私や読者のみなさんがお仕事など、より重要なプロジェクトでこれらのアイディアを活かすことができれば幸いです。

Link to
here
型クラスのインスタンスから

当初(実は今も大部分は)、typesafe-precureには、ACME.PreCure.Textbook.KirakiraALaMode.Profilesで定義しているような中間データはなく、各プリキュア(や、変身アイテムなど諸々)に対しては、直接型を宣言したり型クラスのインスタンスを実装したりしていました。
例えば下記のようなコードです5👇

今回作ったcure-index.jsonを最初に思いついたとき、「型クラスから各型のインスタンス宣言を自動で収集して、そこからcure-index.jsonを作れないだろうか」と、漠然と考えていました。
typesafe-precureを作り始める以前、私はRubyで「定義を自動でまとめる問題」に対応した際、Rubyでの場合の節で紹介したような方法を用いていたため、「Haskellにおける、Rubyで言うところのmix-inされるモジュールは型クラスだ」なんて類推をしていたからかも知れません。
いずれにしても、そんな方法で実現できれば、既存のtypesafe-precureのモジュールの構造をそのまま使ってcure-indexが作れるので、大変都合がよかったのです。

しかし、残念ながらその方法は、少なくとも単純にTemplate Haskellを使うだけでは不可能であるとすぐ気づきました。
なぜなら、Template Haskellのライブラリーが提供するreifyInstancesという関数は、インスタンス宣言を取り出したい型を、自前で持ってきて引数として渡さなければならないからです。
したがって、Rubyでやっていたように、型クラスのインスタンスを自動でリストアップする、といったことはできません(もちろん、Rubyでやった時も完全に自動ではなく、includeしたクラスが自分でグローバルなリストに追加していたわけですが)
それならば、自前でimportしているモジュールから定義されている型を収集することはできないだろうか、と思って、指定したモジュールで定義されている型を取り出すAPIを探ってみましたが、それも見つかりませんでした。
最もそれらしいことができそうなreifyModuleという関数が返すModuleInfoも、保持しているのはあくまでもimportしている別のモジュールだけであり、いくらreifyしてもモジュールの中で定義されている型の情報はとれないのです。

やむなく、私はtypesafe-precureの構造を改め、現在のような、JSONとして書き出すデータ構造を元に型と型クラスのインスタンスを自動で定義するような実装にすることとしました。
この変更は依然として続いています。具体的には、今年新しく追加された「キラキラ☆プリキュアアラモード」に登場するプリキュア以外は、まだ従来の構造のままで、中間データの値は定義されていません。
「キラキラ☆プリキュアアラモード」に収録されたプリキュアの情報しか、cure-index.jsonに記録されていないのはそのためです。

来年のプリキュアハッカソンやプリキュアAdvent Calendarでは、haskell-src-extsという、HaskellHaskellのソースコードをパースするライブラリーを使って、この大きな移行プロジェクトに取り組むことになるかと思います。
typesafe-precureには技術的なネタが尽きませんね。

Link to
here
ほかにやればよかったかも知れない方法

同じことを繰り返しますが、これから紹介する方法も含めて「定義を自動でまとめる」問題の解決は、どんな方法を使うにしても、多かれ少なかれ凝ったメタプログラミングのテクニックを使わなければならなくなります。
注意点の節で強調したとおり、そのコードベースを初めて読んだ人が迷子にならないよう配慮することは忘れないでください。

Link to
here
モジュールが持っている特定の名前の関数・型を処理する

その方法は、先の節でも紹介したhspec-discoverでも実際に行われている方法です。
hspec-discoverは、GHCのカスタムプリプロセッサーを利用して実行することで、テストが書かれたディレクトリーからSpecという名前で終わるすべてのテスト用モジュールを自動でまとめて、それらをすべて実行するSpec.hsを、自動で生成します。
hspec-discoverの場合、ANNのようなアノテーションは一切使用せず、モジュールの名前やモジュールがエクスポートする名前に規約を設けることで「定義をまとめる対象」を検出しています。
このように、ANNのような特別な「印」を着けずに純粋に名前だけで「定義をまとめる対象」を決めることもできます。
実績もあり、同じような方法をとることは非常に簡単そうです。

しかし、個人的には注意点の節でも述べたとおり、「定義をまとめる」対象であることを表す「印」は、「定義をまとめる」対象のファイルの中にあった方が、わかりやすくていいと思います。
確かにhspec-discoverのように、公開されていて広く使用されているものであれば、使用したプロジェクトのコードを初めて読む人でも、すぐに理解できる場合が多いでしょう。「何がまとめられるのか」も比較的直感的ですしね。
とはいえ、私が想定している、例えばアプリケーションのプラグインみたいな、もう少しローカルなコードベースである場合、「印」はより「印」らしいものであった方が、手がかりとして気づきやすいのではないかと思います。

😕初めて「まとめられる」コードを含むファイルを目にして、どのように使用されるのか分からず戸惑う
⬇️
🤔{-# ANN MarkedAsFoo #-}という見慣れないコメントを見つけて、それでコードベースを検索してみる(プラグマは多くのsyntax highlighterで普通のコメントより目立って見えるはずです)
⬇️
💡MarkedAsFooが着いたモジュールを実際に収集してまとめているコードを見つけて、理解する

という流れで「定義を自動でまとめる」機構の存在に気づくのではないでしょうか。

あるいはいっそANNも使わずに、こんな内容のhuman-readableなコメントを「印」とするのもよいかも知れません。
プログラムで検出するのもそう難しくはないでしょう。

これなら、Foo.Commandsモジュールにヒントがあることが、すぐに分かります。
hspec-discoverのように、Template Haskellを使わず直接ファイルシステムにあるファイルを開く方法とも、相性がいいはずです。

ほかにもいろいろな方法を考えましたが、これ以上に有効でもなさそうだし、そろそろ時間もなくなってきたので、この辺でまとめたいと思います。

Link to
here
まとめ

  • 「定義を自動でまとめる」問題を解決することにより、モジュールに関わる情報(どのような定義で、どのように使用されるのか)をなるべくモジュールのファイルのみに集約させることができる
  • 「定義を自動でまとめる」問題を解決するには、下記のことをする
    • 「まとめたい定義」が書かれているファイルが、どのディレクトリー以下にあるか設定する
    • 「まとめたい定義」が書かれたファイルに、なんらかの印をつける
  • Haskellで「定義を自動でまとめる」問題を解決する場合、Template HaskellGHCANNプラグマや、GHCのカスタムプリプロセッサー(-F -pgmF)を組み合わせて使うことによって解決できるが、実際にはGHCのカスタムプリプロセッサーのみで十分可能
    • まとめる対象や状況に応じて、柔軟にやり方を考えよう
  • どのような方法であれ、「定義を自動でまとめる」問題を解決すると、「『「自動でまとめられるファイル』がどのように使用されるか理解しにくくなる」という別の問題が発生するので、気をつけよう

それでは2018年もTemplate HaskellとプリキュアでHappy Hacking!! ❤️❤️❤️

Link to
here
参考にしたページ

(記事中で直接リンクを張っていないもののみ)


  1. 実際には「タイプセーフプリキュアそのものを開発する上で見つかった問題」というよりタイプセーフプリキュアの開発をすることで問題解決の実験をしている、といった方が正しいのは内緒。

  2. もう少し正確に言うと、自動的に設定したいフィールド(あるいはコンストラクターの引数)に@Autowiredというアノテーションが必要ですが、今回の話では本質的ではないので割愛しています。

  3. もちろん、数年前流行ったあのライトノベルのパロディーではありません。

  4. 誰にも聞かれてはいませんが勝手にお話ししますと、ACME.PreCure.Textbookという名前は、プリキュアの教科書から来ています。

  5. 現在もそうですが、実際にはTemplate Haskellで定義されているので、typesafe-precureのリポジトリーにはこれと全く同じコードはありません。