G*なWeb API
Grails3もカウントダウンな感じのおり、イベント紹介です。以下引用。
G*ワークショップZは日本Grails/Groovyユーザーグループの定例イベントです。Java仮想マシン上で動作するGroovy、Grails、Gradle、Spock、vert.xといったG*技術をテーマに、ハンズオンやコードリーディングなど参加型の内容を金曜日に開催しています。
今回のG*ワークショップ"Z" 第19弾は、「G*なWeb API」と題して、昨今話題のWeb APIに関連した以下の内容です。いずれもハンズオン形式ではありません。
Grails 3でWeb APIを簡単に作ろう!
Grails 3の新機能の紹介を交えながら、GrailsでのWeb API構築がいかに容易にできるかをお話します。GrailsのREST機能と、Web Microプロファイルなどを含めてご紹介する予定です。
REAL Objects : 動的なエンタプライズAPIアーキテクチャをどう構築するか
REAL Objectsは、(例えばSmalltalkのような) メッセージ・ベースのオブジェクト指向アーキテクチャを現代の並列/分散環境で実現するものです。 現実の業務システムのような、大規模で、ドメイン・モデルが動的/多様で, 非パブリックなAPIを持つアーキテクチャをどう実現するかについて、 G*技術を (多少) 絡めつつ, お話しします (ワークショップ形式ではありません)。
講演者紹介
- 山本 和樹( @yamkazu )さん: Grails 3でWeb APIを簡単に作ろう!
- 山田 正樹( @yamadamasaki )さん: 「REAL Objects : 動的なエンタプライズAPIアーキテクチャをどう構築するか」
こちらからお申し込みください。
Grails/Groovyのサイトを構築している、名もなき静的サイトジェネレータ
Grails/Groovyのサイトは、静的サイトジェネレータで作成されています。Groovyサイトは去年ぐらいからそうだったのですが、最近Grailsもそうなりました*1。
しかしこの静的サイトジェネレータの名前がわかりません。ソースコード上は、単に「generator/SiteGenerator.groovy」で、独立したgithub projectもありません。groovy-websiteやgrails-static-websiteというプロジェクトの一部としてそれぞれでカスタム化されて機能しているというだけです。
SiteGenerator.groovyは139行ぐらいでやってることもシンプルで自明なので、独立にするまでもない、ということでしょうか。処理の実行はgradleタスクでguild.graldeに書いてあります。
gradle genrateSite
こんな感じでsite/build/site配下にHTMLなどを含むサイト全体の情報が生成されます。gradle webzip
でリンク切れチェックなどが実行された上で、サイト内容をzipで固めたものが生成されます。デプロイ機能とかはないみたいです*2。
この名もなき「サイトジェネレータ」がたいへん気に入ったので紹介します。
Markup Template Engineでページを作成する
Markup Template Engineは、Groovy 2.3で導入された、HTMLを書くためのDSLであり、HTML生成を実行するGroovy標準クラスライブラリです。ドキュメントはこちら。APIリファレンスはこちら。
たとえばGrailsサイトの、pages配下にあるトップページindex.groovyは以下のような感じで記述されます。
import model.Event layout 'layouts/main.groovy', true, pageTitle: 'The Grails Framework', mainContent: contents { div(id: 'band', class: 'band') { } div(id: 'content') { include unescaped: 'html/index.html' int eventsPerRow = 3 def allEventNames = allEvents.keySet() as List int rows=allEventNames.size()/eventsPerRow + (allEventNames.size()%eventsPerRow != 0 ? 1 : 0) for(int rownum = 1; rownum <= rows; rownum++) { int offset = (rownum-1) * eventsPerRow def cssClasses = "row colset-3-article" if(rownum==1) { cssClasses += " first-event-row" } if(rownum==rows) { cssClasses += " last-event-row" } section(class: cssClasses) { if(rownum == 1) { h1 { strong "Groovy and Grails events you shouldn't miss!" } } allEventNames[offset..(Math.min(offset + (eventsPerRow-1), allEventNames.size()-1))].each { String eventName -> Event event = allEvents[eventName] article { div(class: 'content') { // Note that the event image should be 257x180 to look nice div(class: 'event-img', style: "background-image: url(${event.logo})") {} h1 { a(href: event.url) { strong eventName br() em event.location } } time event.date yieldUnescaped event.description } } } } } } include unescaped: 'html/they-use-groovy.html' }
要するに、HTMLをプログラムとして書くわけです。抽象化、モジュール化、共通部分の括りだし、合成、型チェックなどなど、プログラミング言語が持つフル機能を享受してサイトというシステムの提供機能を記述することができます。
Markup Template Engineについてはこちらの資料もどうぞ。
大規模なサイトは、構造をもったソフトウェアなのであり、「プログラムのように」ビルドやチェックができるのが利点です。さらに静的サイトジェネレータ一般の利点として、ソースコード管理システムで管理できるのがうれしいです。データをDBに保存するCMSとかは個人的には捨てるしかないです。バックアップとか面倒すぎるし、履歴を辿れないのもアレだし。かといってGititとか使ってみたこともありますが、結局私には耐えられなかったとです。
生のHTMLを使える
先のindex.groovyでは、include unescaped "html/index.html"
のようにHTML断片を使えます。
これは実際問題としては現実的です。Excelをたとえば XLS2HTMLTABLEアドインなどでテーブルに変換したりしたものをページの一部に貼る、みたいな状況ではHTMLを「そのまま」使いたいときがあり、必須です。半端なwikiとか、「不完全なHTML」だけしかつかえないCMSなどは、動的だろうが静的だろうが捨てるしかないと私は思うわけです。
「 データ」をモデルクラスとして定義、使用する
わたしはこれが一番面白いと思ったのですが、サイトで使用するデータを「モデル」として定義します。たとえば以下が「モデル」です。
- メニューの選択項目(リンクであったり、サイト内のページであったり)
- 「書籍」一覧を含むのであれば、書籍クラス(Book.groovy)
- サイトで配布物jarを列挙しているのであれば、配布物のクラス(Distribution.groovy)
さらにこれらのモデルは、sitemap.groovyというDSLで、その値を簡潔に定義できます。 sitemap.groovyの例はこちら。
Webサイトが大規模でも大規模じゃなくても、静的であっても、それがデータを扱っていることは間違いありません。静的データをHTMLのULやliでベタに書くということは、もしやたいへんに愚かしいことではなかったのか、と考え込まずにはいられません。JSで書くのもアレだし。
こちらのサイトによれば、sitemap.groovyとモデルクラスで設定するモデル群は、「低コストなデータベース」だそうです。 まさしく。コンパイル時に使用したい、データベースか。うーん、この発想はなかったな。
もちろん、既存の静的ジェネレータでも「ブログエントリ」とかはデータなんですが、もっと汎用的なもの。
リンク切れチェック
checkDeadLinks
タスクでサイト全体のリンク切れのチェックが実行されます。
事前インストールフリー
Gradleだから当たり前なんですが、サイトのプロジェクトソースさえ入手すれば、Java以外のインストールなしに、GroovyやGradle、gvmやらの明示的なインストールなしで、即刻ページ生成が可能となります。gradle wrapperってやつです。
その他
font-awesomeとかtwitter-bootstrapとかをデフォルトでだいたい含んでます。
まとめ
Spud-CMSのGrailsプラグインとかも遍歴しましたが、社内のサイトとかは、これからは当面これで作っていこうと思います。apacheだけでいいです。tomcatとかもういいです。
実装から学ぶ型クラス…Groovyで型クラスを実現することを例にして
これは2014年のG*アドベントカレンダーの第23日目の記事のつもりでしたが、12時すぎてしまいましたorz。
HaskellやScalaやRustには型クラスという言語機能があり、個人的感想として、理解が難しかったものの一つです。いわく、インターフェースのようなもの。いわく、オープンクラスのようなもの、など。
わからなければ作ってみるのが一番です。なのでGroovyで型クラスを実装してみました。
ソースはこちら。
ただし実用的なものではなく、学習用です。また実装したのは概念のコア部分のみで、言語によって詳細は異なることに注意ください。
型クラスとは何か
型クラスとは、多相型・ジェネリクス型の型引数(仮型引数)に対して、「ある型に対して可能な操作の集合」を、制約として与え、またそれらの操作が可能であるという保証を、「クラスの継承関係」とは無縁の方法で与えるものです。
別の言い方で言うと、「クラスにデータを従属させる」「メソッドとデータを一体として管理する」「代入可能性を、クラス継承関係で与える」というようなOOPの考えかたの侵襲を拒否し、クラス継承を使用せずに、型引数を制約する方法です。
型クラスが無い場合、たとえばGroovyにおいてメソッドジェネリクスを使用した以下のコード
static<T> void runSomething(T a) { a.run() }
を、正しいコードとして静的型チェックを通すためには、
static<T implements Runnable> void runSomething(T a) { a.run() }
のように「TはRunnableを継承している」と指定します。Runnableという操作の集合による制約を、引数aの型Tに対して行ない、それによってaに対して特定の操作ができるという保証もしています。
これはこれで良いのですが、型クラスが必要になる動機は、ジェネリクス型変数の制約はしたいのだが、このようなクラス継承の使用を避けたいということです。
クラス継承による制約の何が嫌なのか
なぜ避けたいかといえば、
- クラス継承がないから(Haskellの場合)
- 多相型変数Tを「<T extends XXX>」のように制限するためには、TがXXXのサブクラスか実装クラスとして宣言されている必要がある。もし、渡したい引数のクラスがもともとそのように定義されていない場合、
- Tとして渡すクラスのソースを変更してimplements XXXを追加したりメソッドを追加する。しかし使用用途によって都度クラス定義が変化させたり、追記していかなければならないのが嫌
- Tとして渡すクラスを継承し、XXXを実装するクラスを定義して、新規インスタンス作ってメンバをコピーして渡す。でもコピーでは意味が変わってしまうので嫌(ラッパーと同程度、もしくはそれ以上に嫌)。Scalaのnew with Traitも同様。
- Tとして渡すクラスから継承させることがなんらかの理由(プリミティブ型であったり、finalクラスである場合)で不可能だったり、嫌であるなら、以下の何れかの方法でラッパークラスを使う:
- XXXを継承するクラスのメンバーとしてTを定義する。
- Groovyのトレイトの動的proxy。
- →でもラッパーを導入するとアイデンティティが破壊されるのでいやだ。オーバーヘッドが嫌だ。
- 関数型プログラミングスタイルの一流派としてあり得る方針としての「データと関数の分離」に反するので嫌なのだ
- 「継承不要」「ラッパー不要」というのはパラメトリック多相*1で普通にできていた話なのに、制約を付けようとした時点で必要になるというのは嫌だ。なるべく同じようにやりたい
- なんでもクラスにするという基本方針がそもそもの間違いで、どう覆い隠そうとしてもシンプルさが損なわれる(本質的批判)
- オブジェクトが保持するvtableへのリファレンスなどのメモリコスト、vtableを介在したメソッド呼び出しの実行時コストを避けたい*2。また、データ型のメモリイメージ表現のC言語などとの互換性を常に保持したい*3。
- 仮想関数による多態性イラネ。ていうより、原語では同じ単語で多相性とかぶってるかぶってる。
- 操作のレシーバーという概念は非対称でいやだ。レシーバーと他の引数、戻り値の型に非対称性があり、数学的概念の記述などでの一般性を阻害する
など。まるでイヤイヤ期の幼児のような、嫌だ嫌だ、の連発です。まあ嫌なのだからしょうがない。私もこう列挙されると嫌な気になってきます。
型クラスの仕組みの4段階
型クラスの仕組みを以下の4段階で考え、Groovyで実装していきます。
- (1)型クラスを定義する
- (2)型クラスのインスタンス化
- (3)型クラスで制約した多相型引数を含む型の引数をとる関数を定義する
- (4)型クラスで制約した多相型引数を含む型の引数をとる関数を呼び出す
それぞれに付いてモノイド型クラスのコードを例にして解説します。
ちなみに、今回サンプルとしては以下の型クラスを作っています。
型クラス interface Monoid<T> { interface Functor<F> { interface Applicative<A> extends Functor<A> { interface Monad<M> extends Applicative<M> { interface Show<T> { trait Eq<T> { // One of eq or neq, or both should be overriden. trait Ord<T> extends Eq<T> { インスタンス class OptionalMonoid<T> implements Monoid<Optional<T>> { class ListFunctor implements Functor<List> { class OptionalFunctor implements Functor<Optional> { class ListApplicative extends ListFunctor implements Applicative<List> { class ListMonad extends ListApplicative implements Monad<List> { class IntShow implements Show<Integer> { class StringShow implements Show<String> { class ListShow<T> implements Show<List<T>> { class IntEq implements Eq<Integer> { class IntOrd implements Ord<Integer> {
(1)型クラスを定義する
モノイド型クラスを定義します。型引数に適用する制約を定義するものです。
@TypeChecked interface Monoid<T> { def T mappend(T t1, T t2); def T mempty(); }
このようにします。Tではなくdef Tなのは、なぜかそうしないとコンパイルが通らないから。
見るとわかるように普通のインターフェースです。ここでインターフェースにする理由は、継承が使えるからです。例えば、Monad extends Functorのように型クラスを継承することができます。
これは先程の「クラス継承を不要としたい」に反するように思えますが、実際にはthisからのオフセットによるインスタンス変数の参照やアクセス、thisを介した仮想テーブル(vtable)とその呼び出しの機構は完全に不要で、メソッド置き場として使っているにすぎません。なので、ここでクラス継承を使用するのは便宜的なもので、本来なら、staticメソッドにして、staticメソッドを継承階層をたどって探索してくれる仕組みをAST変換で作り込めば良いところです*4が、手をぬきます(Scalaではobjectにするところ)。Monoidはデータメンバを持たない(インターフェースなので持てませんが、classで実装したとしても持たせない)ことに注意ください。
ここでやっていることをまとめると、
(2)型クラスのインスタンス化
型クラスのインスタンス化とは、制約を与えたいデータ型が、ある型クラス制約を満たす、という宣言であり、満たせるようにするための操作を補充します。
以下では、String型がMonoid型クラスの制約を満たすように、「StringのMonoid型クラスのインスタンス」を作成します。
def instance_Monoid_java$lang$String_ = new Monoid<String>() { @Override String mappend(String i1, String i2) { i1+i2 } @Override String mempty() { "" } }
型クラスのインスタンスを保持する変数名を、「instance_Monoid_java$lang$String_ 」という決められた規約に従うものにしておきます。これはスコープの探索をサボるためです。変数名がインスタンスを決め、同名のインスタンスがあれば、内側の物が勝ちます*5。この変数名の変数をstatic importすることも可能であることも期待します。
ここまでは、何の変哲もない標準のGroovyの機能で実現することです。
(3)型クラスで制約した多相型引数を含む型の引数をとる関数を定義する
さて、準備した型クラスMonoidの制約を使って、多相関数を定義してみます。
以下のようにします。
@TypeChecked class Functions { static <T> T mappend(T t1, T t2, Monoid<T> dict=Parameter.IMPLICIT) { dict.mappend(t1,t2) } static <T> T mempty(Monoid<T> dict=Parameter.IMPLICIT) { dict.mempty() } }
引数「Monoid<T> dict=Parameter.IMPLICIT)」が本実装における型クラス制約の宣言であり、意味は、mappendの返り値と、引数で使用する型変数Tは、Monoidという型クラスの制約を満たす必要がある、ということです。
dictは型クラスMonoid型の、型クラスのインスタンスが渡ってくる引数ですが、こちら側から見ると関数の名前空間だと思ってください。引数にあたえられた名前空間に転送します*6。複数の型クラスが引数に与えられたら区別して振り分けます*7。
IMPLICITは以下のように定義してあるただの定数で、AST操作の際のマーカーとして機能します。
class Parameter { static final Object IMPLICIT = null }
ちなみにここで、mappend,memptyが型クラスインスタンスの同名メソッドを呼び出しているだけの関数なのは、無駄に感じるかもしれません。その意味では、ちょっと例がわるくて、Monoid型クラス制約のついた型引数を使った、任意の静的関数が定義できます。ここでmappendやemptyをstatic関数として定義してるのは、利便のため、ユーティティ関数としてこれらをstatic空間にexportしていると思ってください。Haskellだと、インスタンス宣言で、staticな型クラスインスタンスがモジュール内グローバルに存在を開始し、関数呼び出しはそのモジュール内グローバルな空間からマッチする名前が呼ばれることになるんですよね。このGroovy型クラス実装は、Scalaでも同様だと思いますが、名前空間として機能する型クラスインスタンスを明示的に扱うので、1つクッションが入る場合があるということです。善し悪しはあるでしょうが、Haskellの方がシンプルなのは確実です。
(4)型クラスで制約した多相型引数を含む型の引数をとる関数を呼び出す
ここからが、そしてここのみが、山場です。:)
先程定義した関数を呼び出します。以下のようにします。
import static Functions.* : @TypeChecked(extensions='ImplicitParamTransformer.groovy') : String s = mempty() assert s == "" assert mappend("a","b") == "ab"
このコードが静的型チェックのもとでコンパイル・実行できます。
ソースコードの表面上は、関数定義での仮引数Monoid<T> dictに対応する実引数は与えていませんが、暗黙に型クラスのインスタンスが補充されて呼び出されます。これをやってるのが今回作ったImplicitParamTransformerであり、カスタム型チェッカ(型チェッカ拡張)でありながら、型チェッカであることを踏み越えて、以下を実行します。
- ジェネリクス型を使っている静的メソッド呼び出しにおいて、その静的メソッド定義で「IMPLICIT」をパラメータ初期値で使用しているなら、そのパラメータに、ジェネリクス型から定まる変数名を挿入する。呼び出しが、「mappend("a","b")」であるなら、mappendの定義が
static <T> T mappend(T t1, T t2, Monoid<T> dict=Parameter.IMPLICIT) { dict.mappend(t1,t2) }
であるので、実引数の型から型引数の実際の型を推論・解決させて、Tにjava.lang.Stringを束縛して、IMPLICITに対応する引数を、Monoid<String>型のinstand_Monoid_java$lang$Stringという名前の変数の参照に変換します。ついでに、ごく限定されたターゲット型推論を、「String s = mempty()」の形のとき実行し、
static <T> T mempty(Monoid<T> dict=Parameter.IMPLICIT) { dict.mempty() }
では、戻り値が代入される変数の型がStringであることから、Mooid<T>の型がMonoid<String>であると推論させ、変数名を決定します。
最終的には、これらのmappend,memptyは以下の呼び出しと等価になり実行されます。
String s = mempty(instance_Monoid_java$lang$String_ ) assert s == "" assert mappend("a","b",instance_Monoid_java$lang$String_ ) == "ab"
要はScalaのimplicitパラメータの機能を実行していることになります。Scalaの場合は変数名は無意味で型だけで探索するわけですが、特定の型を持った変数定義の探索が面倒なので変数名に型情報をエンコーディングしてスコープ解決させています。
直感的な説明
型クラスのざっくりイメージ的な理解の仕方としては、クラスベースOOPでは、データ型にvtableを組み込み不可分にしていたのを、ひっぺがして、動的な束縛機能を除去して、本当にそれが必要になる最後のタイミング、つまり多相関数呼び出しの時点まで結合を持ち越して、そしてその時点で多相型を解決の枠組みでインスタンスを選定し、別引数で渡し、関数の名前空間として使うということです。
他にやるべきこと
あとはいろんなシンタックスシュガーを導入できると思いますが、学習用としてはここらで打ち止めておきます。
問題点
不都合がありまして、モジュール化ができてません。たとえばEqクラスとかOrdクラスとかそれぞれを別クラスにしたかったのですが、一つの大きな誤算として、多相型のメソッドをstatic importしてもジェネリクス情報が得られません。
class Functions { static <T> T mappend(T t1, T t2, Monoid<T> dict=Parameter.IMPLICIT) { dict.mappend(t1,t2) } :
を、
import static Functions.mappend
すると、引数や返り値のジェネリクス情報が呼び出しコード上の処理段階で不明になります。なので(4)でのジェネリクスの推論ができません。解決策はなんでしょうね。クラスファイルに残っているジェネリクス情報をリフレクションとかでたどるんでしょうかね。たいへんなのでこれ以上の追求は個人的にはあきらめます。
おまけ: Higher-kind Generics
Groovyでは、型引数をとる型構築子を型引数に(いわゆる高階型変数(Higher-kind Generics))したときに期待する動作を行います。たとえば、今回以下のようなコードが書けています。
@TypeChecked interface Applicative<A> extends Functor<A> { public <T> A<T> pure(T t) public <T,R> A<R> ap(A<Function<T,R>> func, A<T> a) // <*> :: f (a -> b) -> f a -> f b }
ここでAは引数を取れる型引数であり、たとえばMaybeやListなどを与えることができます。これはJavaのジェネリクスでは(少なくともJava8では)できないことです 。これができることによって得られる抽象化能力は、非常に大きいものです。モナドはこれがあるから型クラスになり得るのです。
とはいえ、これをもってGroovyでHigher-kind Genericsができる、と言ってよいかどうかは、微妙です。というのも、しなければならないことは「期待する動作を行う」だけではないからです。具体的には、上記を継承するインスタンスで、上記のジェネリクス条件を満たさないオーバーライド定義したときに、型エラーになること、が必要だと思っています。実際試してみると、Groovy 2.4-beta-xでは期待したようにエラーにはならないケースがあるようです。
エラーチェックが正しく出来ないなら、型チェックがなされない=動いてしまう、という結果になるのは明らかです。綿密に調べてもしバグならバグ報告したいところですが、できておりません。
ということで結論は保留です。今のところわかりません。
まとめ
- 型クラスの基本は実装がわかれば理解できる
- ただし言語ごとの実装戦略や、シンタックスの差異は当然あるのでそこは別途。
- 型クラスの構成部品はOOPから流用可能だが、コンセプト的に根本的に相入れないからこそ、型クラスが存在する
- 型クラス=FPではない。その実現に寄与する周辺機能にすぎない。但し静的型および多相型に強く結びついた重要で強力な機能である。FPにおける型クラスは、OOPにおけるクラス継承に比するべき概念である(クラス継承はOOPに必須ではない、ということとも同型)。
- Groovyはコンパイラやランタイムに手を入れずに、カスタム型チェッカやAST変換でかなりのことができる
- 静的型や多相型に関してもある程度のことができる。ただし、静的型チェッカの多相型処理を読み込むのは覚悟がいる。
- Groovy2.xの静的型チェッカの多相型サポートは、激しく進歩している。Javaのそれより強いのは間違いない。
- カスタム型チェッカ(型チェッカ拡張)は、フルのAST変換を書くのにくらべて非常に便利。ただし実行フェイズや、ハンドラを書けるイベントが決まっているので目的とする処理に適合しない可能性がある。
おまけ2:アドホック多相
アドホック多相という言葉は使いませんでしたが、これはこういうことなんじゃないかと思っています(オレオレ理解)。。
- パラメトリック多相: 処理対象のデータ型の特定をしないで処理を記述できるような、処理記述の柔軟性。ただし、処理対象となるデータに対して実施できるのは、ポインタ操作やバイトコピーなど、内容や構造に依存しないような操作のみに限る。典型的にはコンテナへの出し入れ対象として扱うだけ、とか。
- アドホック多相: 目的は同様だが、パラメトリック多相では達成できない、「データの内容に依存した処理」を可能とするために、データと独立した「操作の集合」を事前に定義し、それを関数呼び出し時のデータ引数を渡す時に、引数データの付随情報として与える。(その際に操作の集合の選定を多相型の型推論で元にして自動的に実行する。)
これらの概念は、実装方式とは独立である。たとえばパラメトリック多相についてポインタで実現するケースもあると思うが、型毎に関数実体を適切なタイミングで生成して使っても良いであろう。
*1:パラメトリック多相とは、型引数の制約が無い場合、つまりコンテナに入れる、出す、といった、データ固有の操作を行わない引数を多相にするときに使える。
*2:可能かどうかは実装による。
*3:余談になるが、多言語間呼び出しでは、OOPが長期的には無価値であることが実証されてきたと言うしかない。CORBA/RMIを見よ。SOAPを見よ。WebAPIはメソッドを持たないJSONを返す
*4:@InheritConstructorはすでに似たようなことをしているのでそれを真似すればよい。
*5:Scalaとの相違点:確かScalaでは衝突をコンパイル時エラーにするんじゃなかったかな。
*6:この振り分けは、Haskellなどのイレイジャではないジェネリクス環境で理想的な条件下であれば、静的に決定できるとされている。未確認だが。
*7:Groovyのwith句を使っても良い。
(周知) 予告: JGGUG大忘年会LT大会と、LondonのG*なカンファレンス行ってきた報告!+合宿の報告もあるよ! #jggug
今週末は、JGGUG忘年会です。あしたビアバッシュのピザなどを予約しますんで、参加希望のかたは今日中にぜひどうぞ。
JGGUG大忘年会LT大会と、LondonのG*なカンファレンス行ってきた報告!+合宿の報告もあるよ! - 日本Grails/Groovyユーザーグループ | Doorkeeper
建物とフロアは同じですが、会議室が通常と違いますので注意を。
LTネタ考え中。
Grails/Groovyでのカバレッジ取得に関してのTIPS
プリミティブ最適化を抑制することでブランチカバレッジをましなものに
- Test Code Coverage Plugin(http://grails.org/plugin/code-coverage)
しかし、上記を使用した場合、分岐網羅(ブランチカバレッジ)は多くの場合、期待する値が取得できない。この理由の1つは、Groovy 1.8以降で導入されたプリミティブ最適化によって、型がプリミティブかどうかによっての条件分岐を行うコードがGroovyのコード生成器によってバイトコード上生成されているためである。Test Code Coverage Pluginはバイトコードレベルでカバレッジ情報を収集するので、ソース上に表われない暗黙の分岐をカバレッジ率の分母に計上してしまう。そしてその値は一般には100%にすることが困難である。
本来Groovyにおいて、プリミティブ最適化を抑止するためのコマンドラインオプション「--disableopt int」が存在する。しかし、GrailsではGroovyをコマンドラインから起動するわけではなくオプションが指定できない。
この問題の対処の一つは、この記事のリンク先にある、「renataogarcia/disableOptimizationsTransformation · GitHub」のコンパイル結果jarである
「DisableOptimizationsTransformation-0.1-SNAPSHOT.jar」をダウンロードし、クラスパスに通すことである。こうすればGrails上でのGroovyの最適化が抑制されるため、ブランチカバレッジが正確に測定できる可能性がたかまる(ただし、ブランチカバレッジが期待する値にならない理由はこれだけが原因とは限らないことに注意*1 )。
なお上記jarをクラスパスに通す方法として以下が考えられる。
どの方法でも良いが、「試験では最適化抑制をし、プロダクトコードでは最適化する」ということはリスクになるため、テスト時に常に抑制するのは怖い気がする(プロダクトコードでも最適化抑制するなら別だが)。なので、テストのとき常に、ではなく、カバレッジ判定のときだけ一時的な指定をするという意味で1でも良いかもしれない。
なお、上記の設定前後でgrails cleanを実行した方がよい。
ヒアドキュメントと複数行文字列について
「ヒアドキュメント」をなんで「ヒアドキュメント」っていうかを調べてみた。以下が参考ページ。
- http://programmers.stackexchange.com/questions/143918/why-is-it-called-a-here-document
- http://ja.wikipedia.org/wiki/%E3%83%86%E3%83%AC%E3%82%BF%E3%82%A4%E3%83%97%E7%AB%AF%E6%9C%AB#.22Here_is.22_.E3.82.AD.E3.83.BC
わかったこと
上記の内容が正しいとして、読み取ったことは以下のとおり。
- 「ヒアドキュメント」は「"Here is" document」から来ている。
- "Here is"は何からきているかというと、昔テレタイプ端末にあった「Here is」というキー。
- 「Here is」キーは何かというと、押すとあらかじめ端末ごとに設定できた20文字ぐらいの文字列をホストに送り返すキー。この用途は、例えば、その文字列に端末の識別IDなどを設定しておいて、1キーで「この端末はXXXだよ」とホストに送ること。
- さらに、ホストが送ったENQという制御文字を端末が受けとると、自動的にHere isキー登録文字列をホストに送信するように設定することも可能。ENQの送信は、ホスト側から、ログインしてるあんたの端末どれよ/あんた誰よ(操作者の名前をhere isキー文字列に登録しておいた場合か)、という情報の問合せをするのに用いられた。
ShellスクリプトにおけるHere documentの意味について
上記参考リンクには、それほど詳細には説明されていないので、ここからは推測も交えて、になる。
Shellスクリプトにおけるヒアドキュメントは、Shellが起動する、コマンドのプロセスに対して特定の文字列をパイプ(もしくはシングルタスクOSでは一時ファイル?)を通じて送りつける、という機能である。
cat <<EOT ... EOT
上では、プログラム中に書かれた固定文字列「...」の部分をShellが切り出し、そのデータをパイプ(やひょっとしたら一時ファイル)を通じてcatプロセスに送り込んでいる。これがホストに対してHere isキー登録文字列(=固定文字列)を送付している振舞に似ている。だからこの機能をHere (is) documentと名付けたのではなかろうか。
複数行を含むことができる文字列定数をヒアドキュメントと呼ぶことについて
時はながれて。
これを調べたのは、現代のプログラミング言語のいくつかにおいて、「改行文字を含むことができる」程度の機能をもった文字列定数の表記法を、「ヒアドキュメント」と呼ぶ場合があることにもともと違和感があったからである。
Shellスクリプトにおいては、任意の文字列を処理対象としてコマンドに渡すのにパイプやファイルを経由するしかない場合があるが、その処理を簡易に記述する機能を「ヒアドキュメント」と呼んだのは、当時において合理性があったように思われれる。(まさか、"Here is"キーがその後消滅することなんか、誰にも想像つかないし!)
また、PerlやRubyなどの言語における「ヒアドキュメント」機能は、その背景機構や元々の命名理由とは無関係にShellスクリプトとの表記上の類似性だけでそう呼んでいると推測できる。なぜなら、Shellにあった、起動したプロセスにパイプ繋いで送り込む、という様相が存在しないためである。このことを批判するつもりは別にないが、Shellスクリプトにおいて「ヒアドキュメント」という名称が背後機構をちゃんと説明するものであった、という利点は失うこととなっている。
しかし、PythonやGroovyなどの複数行文字列定数で使用する"""〜"""などには、表記上の類似性すらもないので、ヒアドキュメントと呼ぶ必要が全くないと思う*1。なので自分はそう呼ばないことにしている。なので「プログラミングGroovy」の本にもヒアドキュメントという用語を使うことは意図的に避け、確か複数行文字列定数と呼ぶように徹底したのであるよ。
(2014/12/24)
記憶をたどれば、昔のGroovy(Classic Groovy)には、「本当の(RubyやPerlの意味での)」ヒアドキュメントが実際にあったのですが、JSRに提案される段階で削除されました。ヒアドキュメントの廃止 - どうせ見苦しいですから (smile)。もし使いたい場合はかわりにトリプルクォートを使いましょう
この意味でも、Groovyの複数行文字列定数をヒアドキュメントと呼ぶのはまぎらわしい。
*1:改行を含んでいるとドキュメントっぽいから、ということが理由なら、バックスラッシュ('\')で行末エスケープした通常の文字列定数もヒアドキュメントと呼ぶべき。さらに「ヒア」の意味があるものすべてにヒアをつけるべき。ヒア整数、ヒア引数、ヒア関数…
ペアワイズ法でSpockのテストデータを生成する
(関連記事:http://uehaj.hatenablog.com/entry/2015/10/18/155107 )
JCUnitというすばらしいJUnitの拡張があります。
JCUnitではペアワイズ法もしくはオールペア法という手法でテストデータを生成します。ペアワイズ法というのは、試験対象コードに対するパラメタのバリエーションテストをする際に、より少ないテストデータの件数で、効率良くバグを発見できるというテストデータの選択技法だそうです。(オールペア法の記事)
折角の自動テストならば、テストデータも自動生成してしまおうと。しかし単純に総当たりだと、指数的にケース数が増えて手に負えなくなるので、統計学に基づいて、優れているとされている方法でデータを選択し実用的には十分なようにしよう、ということです。
JCUnitはJUnit4のRunner(@RunWithアノテーション)を使って実現されていますが、本記事では、そのエンジンだけを呼び出すことで、JCUnitをSpockのデータドリブンテストで使う方法を紹介します。
方法
Spockのテストケースで以下のように「genPairwiseTestData」を使用してデータを生成します。genPairwiseTestDataはJCUnitの機能を呼び出すためのラッパーとして機能するstaticメソッドで定義は後述のテストコード全体に含まれています。
@Unroll def "分配法則(a=#a,b=#b,c=#c)"() { expect: c*(a+b) == c*a + c*b where: [a,b,c] << genPairwiseTestData(new Object(){ @FactorField public int a,b,c } ) }
上記中、「[a,b,c] <<は」Spockのデータパイプという機能で、a,b,cというテストコードで使用する変数に対して、3要素のリストの集合を流し込むことで、パラメータのバリエーションに対してテストコードが複数回呼び出されるというものです。@Unrollはさらにそれをテストレポート上で複数のテストメソッドがあるかのように表示するアノテーションです。これらはいずれもSpockの機能に乗っとっていて、データパイプに流し込むデータをJCUnitの機能を使って生成することで、機能連携しています。
@FactorFieldはJCUnitが提供するアノテーションで、JCUnitで指定できる機能を利用できます*1。
上記では、[a,b,c]がすべてintの場合ですが、型がもし違っていれば、
[a,b,c] << genPairwiseTestData(new Object(){ @FactorField public int a @FactorField public String a @FactorField public double c } )
のようにそれぞれ指定します。<<の左辺に表われる変数(上記ではa,b,c)は、右辺の@FactorFieldで同じ順序で一回ずつ指定される必要があります(冗長だが、現状の仕組みでは仕方がない)。
長いテストコード例
以下は単独で実行できるように@Grabで指定したSpockテストコードです。Grails中のSpockテストコードやGradleからの使用では適切な依存関係の指定で置き換えることになるでしょう。
groovy JCUnit.groovy
で実行できます。
@Grab('com.github.dakusui:jcunit:0.4.10') @Grab('org.spockframework:spock-core:0.7-groovy-2.0') import com.github.dakusui.jcunit.core.FactorField; import com.github.dakusui.jcunit.generators.TupleGeneratorFactory; import com.github.dakusui.jcunit.core.tuples.Tuple; import com.github.dakusui.jcunit.generators.ipo2.IPO2; import com.github.dakusui.jcunit.generators.ipo2.optimizers.GreedyIPO2Optimizer; import com.github.dakusui.jcunit.constraint.constraintmanagers.ConstraintManagerBase; import com.github.dakusui.jcunit.constraint.ConstraintManager; import com.github.dakusui.jcunit.exceptions.UndefinedSymbol; import spock.lang.* class HelloSpec extends Specification { static ConstraintManager closureConstraintManager(List<String> names, Closure c) { return new ConstraintManagerBase() { @Override boolean check(Tuple tuple) throws UndefinedSymbol { Map map = [:] for (name in names) { if (!tuple.containsKey(name)) { throw new UndefinedSymbol(); } map[name] = tuple.get(name) } c.delegate = map c.call() } } } static Collection genPairwiseTestData(Object object, Closure constraint = null) { def tg = TupleGeneratorFactory.INSTANCE.createTupleGeneratorForClass(object.class) def names = tg.factors.collect{it.name} def cm if (constraint == null) { cm = tg.constraintManager } else { cm = closureConstraintManager(names, constraint) } IPO2 ipo2 = new IPO2(tg.factors, 2, cm, new GreedyIPO2Optimizer()) ipo2.ipo() return ipo2.result.collect{ testData -> names.collect{ testData[it] } } } @Unroll def "分配法則(a=#a,b=#b,c=#c)"() { expect: c*(a+b) == c*a + c*b where: [a,b,c] << genPairwiseTestData(new Object(){ @FactorField public int a,b,c } ) } @Unroll def test1() { expect: c*(a+b) == c*a + c*b where: [a,b,c] << genPairwiseTestData(new Object(){ @FactorField public int a,b,c }, { a > 0 && b != c }) } @Unroll def test2() { expect: (a+b).size() == a.size() + b.size() where: [a,b] << genPairwiseTestData(new Object(){ @FactorField public String a,b }) } @Unroll def test3() { expect: a+b == b+a where: [a,b] << genPairwiseTestData(new Object(){ @FactorField(intLevels=[1,2,3]) public int a @FactorField public int b }) } @Unroll def test4() { expect: a == b || a != b where: [a,b] << genPairwiseTestData(new Object(){ @FactorField public MyBoolean a @FactorField public MyBoolean b }) } } enum MyBoolean { True, False }
データの生成のされかた
3つのintデータは、デフォルトでは以下のデータがペアワイズ法で生成されます。1, 0, -1, 100, -100, Integer.MAX_VALUE, Integer.MIN_VALUEの7種類の値が、a,b,cのパラメータに流し込まれるので、全組み合せだと7^3=343ケースが実行されるところを、53テストケースに絞り込まれていますね。
[[a:1, b:1, c:0], [a:1, b:0, c:100], [a:1, b:-1, c:-1], [a:1, b:100, c:-100], [a:1, b:-100, c:2147483647], [a:1, b:2147483647, c:-2147483648], [a:1, b:-2147483648, c:1], [a:0, b:1, c:100], [a:0, b:0, c:2147483647], [a:0, b:-1, c:-100], [a:0, b:100, c:0], [a:0, b:-100, c:1], [a:0, b:2147483647, c:-1], [a:0, b:-2147483648, c:-2147483648], [a:-1, b:1, c:2147483647], [a:-1, b:0, c:-2147483648], [a:-1, b:-1, c:1], [a:-1, b:100, c:-1], [a:-1, b:-100, c:100], [a:-1, b:2147483647, c:-100], [a:-1, b:-2147483648, c:0], [a:100, b:1, c:-2147483648], [a:100, b:0, c:-1], [a:100, b:-1, c:0], [a:100, b:100, c:1], [a:100, b:-100, c:-100], [a:100, b:2147483647, c:2147483647], [a:100, b:-2147483648, c:100], [a:-100, b:1, c:1], [a:-100, b:0, c:0], [a:-100, b:-1, c:-2147483648], [a:-100, b:100, c:100], [a:-100, b:-100, c:-1], [a:-100, b:2147483647, c:100], [a:-100, b:-2147483648, c:-100], [a:2147483647, b:1, c:-100], [a:2147483647, b:0, c:1], [a:2147483647, b:-1, c:100], [a:2147483647, b:100, c:2147483647], [a:2147483647, b:-100, c:0], [a:2147483647, b:2147483647, c:-2147483648], [a:2147483647, b:-2147483648, c:-1], [a:-2147483648, b:1, c:-1], [a:-2147483648, b:0, c:-100], [a:-2147483648, b:-1, c:2147483647], [a:-2147483648, b:100, c:-2147483648], [a:-2147483648, b:-100, c:-2147483648], [a:-2147483648, b:2147483647, c:1], [a:-2147483648, b:-2147483648, c:2147483647], [a:-100, b:2147483647, c:2147483647], [a:-2147483648, b:1, c:0], [a:-2147483648, b:-100, c:100], [a:1, b:2147483647, c:0]]
他のデータ型について、デフォルトではどのようなデータの集合(レベル)をつかってデータが生成されるかはこちら。
Enumは勝手に全要素を組合せてくれます。
データ集合の指定
元になるデータの集合(レベル)を指定することもできます。たとえば、intについて1,2,3の3通りのデータからの組合せを生成するには、以下のようにします。
@FactorField(intLevels=[1,2,3]) public int a
制約
データが満たすべき制約條件を与えて、テストケース数をふるいにかけて減らすこともできます。
以下では、変数間の関係がa > 0 && b != cを満たすテストケースに絞り込みます。
[a,b,c] << genPairwiseTestData(new Object(){ @FactorField public int a,b,c }, { a > 0 && b != c })
Groovyなので、クロージャで制約式を与えられるようにしてみました。
おまけ
生成されるテストデータは毎回かわるわけではないので、テストデータをファイルにキャッシュする仕組みがあると良い気もする。
まとめ
JCUnitすばらしい!Spock便利! Groovy超便利!!
追記(2014/10/27)
JCUnitの更新情報として作者の方からコメントを頂いています。記事中では使用していませんが、'levelsFactory'メソッドが0.4.11か0.4.12で'levelsProvider'に改名されていますので注意。