uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

実装から学ぶ型クラス…Groovyで型クラスを実現することを例にして

これは2014年のG*アドベントカレンダーの第23日目の記事のつもりでしたが、12時すぎてしまいましたorz。
f:id:uehaj:20141224031845p:plain:right:w200

HaskellScalaや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で実装したとしても持たせない)ことに注意ください。

ここでやっていることをまとめると、

  • 操作の集合をクラスのインスタンスメソッドとして定義する。このクラスをジェネリクス型引数の制約として使用する。これが型クラスである。このクラスから継承したいかなるクラスにおいても、データをインスタンス変数として定義することはない(本質的)
  • ここで定義したメソッドの間では相互に呼び出しあうことができる
  • メソッドの本体を別に定義しても良い。Groovyのtraitが便利に使えるだろう。

(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句を使っても良い。

Elmでやってみるシリーズ14:ライブラリを公開する

この記事は「Elm Advent Calendar 2014」の23日目として書きました。


f:id:uehaj:20141223193216j:plain:right:h320

今回は、作成したElmのライブラリをElmコミュニティライブラリに公開してみます。公開するブツは以前こっそりと作成してすでに登録していた「IntRange」というもので、たいしたものじゃございません*1。今回Elm 0.14に対応させた上で、elm-packageコマンドの登録手順を整理してみます。

プロジェクトを作る

何はともあれ、公開したいプロジェクトを作ります。
ディレクトリを作ってそこにcdしてelm-packageを実行。

$ mkdir IntRange
$ cd IntRange
$ elm-package install
Some new packages are needed. Here is the upgrade plan.

  Install:
    elm-lang/core 1.0.0

Do you approve of this plan? (y/n) y
Downloading elm-lang/core
Packages configured successfully!

これで以下のファイルが作られているはずです。

$ ls -la
total 8
drwxr-xr-x  4 uehaj  staff  136 12 23 09:34 ./
drwxr-xr-x  4 uehaj  staff  136 12 23 09:34 ../
-rw-r--r--  1 uehaj  staff  330 12 23 09:34 elm-package.json
drwxr-xr-x  4 uehaj  staff  136 12 23 09:34 elm-stuff/

Elmソースコードを作る

ライブラリ設計ガイド」を参考にしてがんばって作ります。このガイドで個人的に気になった内容を箇条書きにすると、

  • モジュールに定義した関数において、主要な処理対象のデータは関数の最後の引数にせよ。合成や部分適用で便利だからですね。
  • 中置演算子を避けよ。誰を非難するわけではないですが、おいそこらの言語! 聞いとけ。演算子乱用すんでね。検索できないし読めねー、プレフィックスで出自モジュールを明示することもできない。やめろ。もしやりたければ慣れた人向けのものとしてAPIの最後に置くか、別モジュール(ホゲ.Infix)に分けろ。
  • 抽象的すぎるAPIはよしとけ。汎用性が重要なのは認めるが、それは道具であって、設計上のゴールではなく、実際のユーザ便益が本当にあるのかどうか考え。その抽象性が有用な具体例を示せ。

ちなみにgitの使用が大前提です。バージョニングもgitを元にしています。githubが必須かどうかは不明。

elm-package.jsonを編集

JSONファイルを編集します。以下のような内容です。

version セマンティックバージョニングに基づいたバージョン番号が、ソース変更から自動的に設定されるので手動で修正する必要がない。セマンティックバージョニングについては後述。
summary 概要。修正してもサイトが更新されないケースがあるので、周到に決める必要がある。
repository このソースを格納しているgithubリポジトリURL。
license BSD3が推奨のようでデフォルトである。
souce-directory ソースコードの格納フォルダ。デフォルトは"."だがsrcとかを作っていればそこに。
exposed-modules 公開するモジュール名のリスト。手動設定が必要。
dependencies 依存ライブラリ。elm-package installで自動的に追加されるが手動で編集しても良い。

ドキュメントコメント

APIリファレンスの内容になるのでこちらのガイドに従って作ります。

ドキュメントコメントは、elm-docコマンドでJSONファイルが生成されます。このJSONは、package.elm-lang.org上でHTMLにレンダリングされるのですが、現状ではローカルで容易にプレビューする方法がないようです*2

$ elm-doc IntRange.elm
{
  "name": "IntRange",
  "comment": "The library provides fold*/map* to the range of numbers without consuming memory.\n\nIntRange.fold*/map* can be used as a replacement of List.fold*/map* on a list of consecutive Int values.\n\nUsing those method reduces memory when iterate on a list which have vast numbers of element and don't use the list after iterate.\n\nFor example,\n\n          import IntRange (to)\n          import IntRange\n          Import List\n\n          IntRange.foldl (+) 0 (0 `to` 100000000) -- Can be calculated without consuming less memory.\n          List.foldl (+) 0 [0..100000000] -- Require memory for the list which length is 100000000.\n\nBoth of List.foldl and IntRange.foldl don't consume call stack, but List.foldl allocate memory for the list whose length is 100000000. In contrast, IntRange.fold requires relatively less memory. It can be used like counter variable of loop.\n\n# Create IntRange\n@docs to\n\n# Iteration\n@docs foldl, foldr, map, map2\n\n# Convert\n@docs toList",
  "aliases": [],

  略

タグを切る

gitのタグを切っておきます。もしタグをつけておかないとelm-publishの際にエラーになり、タグを切れというメッセージが表示されます。

    git tag -a 1.0.0 -m "release version 1.0.0"
    git push origin 1.0.0

publish!

以下を実行することで、http://package.elm-lang.org/サイトにelm-package.jsonおよびelm-stuff/documentation.jsonの内容がPOSTされ、コミュニティライブラリとして登録されます。ちなみに、削除等の自動的な方法はおそらくない(個別に頼むしかない)と思うので、慎重にどうぞ。

$ elm-package publish
Verifying uehaj/IntRange 1.0.0 ...
This package has never been published before. Here's how things work:

  * Versions all have exactly three parts: MAJOR.MINOR.PATCH

  * All packages start with initial version 1.0.0

  * Versions are incremented based on how the API changes:

        PATCH - the API is the same, no risk of breaking code
        MINOR - values have been added, existing values are unchanged
        MAJOR - existing values have been changed or removed

  * I will bump versions for you, automatically enforcing these rules

The version number in elm-package.json is correct so you are all set!
Success!

バージョンアップ

さて、なんらかの理由でソースコードなどを編集した後で、バージョンアップしたものを登録する際には、「elm-package bump」を実行します。

$ elm-package bump
Based on your new API, this should be a PATCH change (1.0.0 => 1.0.1)
Bail out of this command and run 'elm-package diff' for a full explanation.

Should I perform the update (1.0.0 => 1.0.1) in elm-package.json? (y/n) y
Version changed to 1.0.1.

elm-packageは、ソースコードの公開APIを解析して、適切なセマンティックバージョニングに基づいたバージョン番号を付与をしてくれるのです。セマンティックバージョニングとは、詳しくはしりませんが、そのモジュールを利用する他のコード側から見て、バージョンアップしてよいかわるいかなどが判断できるような基準でのバージョン番号の付与ルールのようです。

セマンティクバージョンは「MAJOR.MINOR.PATCH」の3つの番号の組であり、1.0.0から始まり、APIの変更の種別によって

  • PATCH - APIとしては同一であり、互換性を破壊するリスクは無い
  • MINOR - 追加的な修正であり既存部には変更がない
  • MAJOR - 既存部分が修正されているか削除されている

という基準で増加させていきます。elm-packageでは、もちろん意味的な修正内容までを自動的に判断しているわけではないので、コンパイルが通るかどうか、という基準で考えれば良いと思われます。メジャーバージョンアップすると既存コードがコンパイルできる保証がなくなるということです*3

上記では、package.jsonをいじったぐらいなので、APIの追加も無いとみなされて、マイナーバージョンのみ増加しています。

ためしに一個、モジュールからの公開関数(toList)を削除してみると、

elm-package bump
Based on your new API, this should be a MAJOR change (1.0.1 => 2.0.0)
Bail out of this command and run 'elm-package diff' for a full explanation.

Should I perform the update (1.0.1 => 2.0.0) in elm-package.json? (y/n)

のようにメジャーバージョンアップとみなされます。
elm-package diffで以下のように理由を知ることができます。

$ elm-package diff
Comparing uehaj/IntRange 1.0.0 to local changes...
This is a MAJOR change.

ーーーー Changes to module IntRange - MAJOR ーーーー

    Removed:
        toList : IntRange -> List Int

公開するシンボルを一個増やしてみると、

$ elm-package bump
Based on your new API, this should be a MINOR change (1.0.1 => 1.1.0)
Bail out of this command and run 'elm-package diff' for a full explanation.

Should I perform the update (1.0.1 => 1.1.0) in elm-package.json? (y/n)

$ elm-package diff
Comparing uehaj/IntRange 1.0.0 to local changes...
This is a MINOR change.

ーーーー Changes to module IntRange - MINOR ーーーー

    Added:
        add : Int

たしかにマイナーバージョンアップになります。

まとめ、もしくは本題

Elmは、言語の存在目的/ミッションが非常にしっかりとした言語です。以下に私の考えるところのElmの存在意義を書いてみます。

一つは、「フロントエンド・プログラミングについてゼロの状態から考え直す」です。開発者である Evan Czaplicki氏へのインタビュー記事「【翻訳】フロントエンドのプログラミング言語をゼロから設計した理由 | POSTDを参照ください。

もう一つは、純粋関数型言語の産業界への普及です。Evan氏もしくはコミュニティの目的意識が非常にはっきりしていて、Elmに関する言語設計上の選択肢においては、「一般のプログラマが業務で使用できるようになる」ことを常に優先しています。

たとえば用語法について。ADTはADTと呼ばずUnion Typeと呼びます、elmのメーリングリストではHaksellでそうだから、という理由に流されず、常にゼロベースで議論がなされています。Elmにモナドの導入がなされるとしても、Elmにおいてモナドと呼ばれることはないでしょう。

Haskell文化圏は、「歴史的経緯」もしくは「背景となる数学理論」に基づいた名前が導入され永遠に使い続けられる傾向がある気がします。その種の用語に、誰もが最初は苦労したはずです。しかし、いったん慣れると、よしあしや合理性は別にしてそのわかりにくい名前を使い続けるのが通例です。ADT? Algebraic(代数的)って、どこがどういう側面について代数的なんだ? 直和型、直積型が、加算や乗算といった「代数的操作」に対応するからだとぉ! なんでとりたてて加算と減算だけを問題にする?減算・除算はどうなってるんだ??? とかみな思いますよね(2015/1/3削除)(「F-始代数」に由来するそうです(始代数 - Wikipedia)。型スキーマの都度具現化を意味するキーワードがなぜforallでなければならないかを納得できる理由に出あったことも私はありません。

もちろんHaskellはそれで良いです。目的が普及ではないからです。間接的に機能が他の「実用」言語に波及していけばそれが言語ミッションの成功です。あるいは、Haskellがそうであることは「Haskellが成功しすぎることの罠」に落ちないために必要だったのかもしれません。

Elmは別のレベルに目標を置きました。それが成功するかどうかはわかりませんが、自分個人も共感するので、それに寄与したいなあと思っています。プログラマと人類の幸福に寄与するために!
良いクリスマスを!

タングラムエディタ

間に合いませんでしたっ!

参考

関連エントリ


「Elmでやってみる」シリーズのまとめエントリ - uehaj's blog

*1:Elm 0.13でList.indexedMapが追加されたので、明らかに存在意義が減じゃ。意味あるとしたらfoldl/foldrぐらいか。

*2:もしやるなら、elm-packageサイトをローカルに立てて、elm-package送信先を書き変えてコンパイルしなおして実行すればできるかもしれない。あるいはelm-docコマンドで生成したjsonを適当にレンダリングした方が早いかも。

*3:でもこの基準だとバージョン番号のインフレがおきそうですね。公開するシンボルは極小にするなど、API設計は慎重にやれ、ということか。

XML形式のlog4j設定(log4j.xml)をGrailsで使用する

Grailsではロギングの設定がConfig.groovyで設定できますが、なんらかの理由でXML形式の設定(log4j.xml)で行ないたいことがあるかもしれません。しかし、その場合log4j.xmlをどこに置くか、という問題があり得ます。

この問題を説明します。まずGrailslog4jXML設定を使用するには、springの設定ファイルresouces.groovyで、以下のようにlog4jConfigurerを設定します。

GRAILS_PROJ/grails-app/conf/spring/resources.groovy

beans = {
    log4jConfigurer(org.springframework.beans.factory.config.MethodInvokingFactoryBean){
        targetClass = "org.springframework.util.Log4jConfigurer"
        targetMethod = "initLogging"
        // arguments = "grails-app/conf/log4j.xml"  // ★1
        arguments = "classpath:log4j.xml"  // ★2
    }
}

問題は、★1のようにカレントディレクトリ相対で指定すると、warにしてtomcatなどにデプロイした状態では、tomcatが走行しているときのカレントディレクトリ($CATARINA_BASE)からの相対になり、読み取ることができないということです。だからといって、絶対指定も一般にはできないでしょう。

この解決策ですが、まずlog4j.xmlgrails-app/conf配下に置いた上で、(★2)のようにxmlclasspath配下にあると指定します。run-appの際には、grails-app/confはクラスパスに入っていますのでこれで動作します。

ただし、warにしたときには、conf配下は特にはクラスパスにはいっているわけではないので(Config.groovyなどはコンパイルされてclasses配下に移動している)、warの作成時にgrails-app/conf/log4j.xmlを適切なclasspathの通っているところ、たとえば以下ではWAR中の$WEB-INF/classes/配下にコピーするように、BuildConfigに以下の設定を追加します。

GRAILS_PROJ/grails-app/conf/BuildConfig.groovyに追加。

grails.war.resources = { stagingDir, args ->
    println "$stagingDir, $args"
    copy(file: "grails-app/conf/log4j.xml",
         todir: "${stagingDir}/WEB-INF/classes/")
}

なおログファイルをそもそもどこに置くか、という設定はlog4j.xmlに記入しておく必要があります。XML設定の場合、grailsのConfig.groovyで設定するように、developmentのときとprod/testのときに場所を切り替えたりということはたぶんできないので、適当に絶対パスで指定することになるでしょう。

ちなみに、Wiki - Log4j XML Pluginというプラグインもあり、XMLでの設定に対応する、markupbuilderっぽい記法で設定することができるようです。これを使えば、もっとうまくいくのかもしれませんが試してません。

(周知) 予告: JGGUG大忘年会LT大会と、LondonのG*なカンファレンス行ってきた報告!+合宿の報告もあるよ! #jggug

今週末は、JGGUG忘年会です。あしたビアバッシュのピザなどを予約しますんで、参加希望のかたは今日中にぜひどうぞ。

JGGUG大忘年会LT大会と、LondonのG*なカンファレンス行ってきた報告!+合宿の報告もあるよ! - 日本Grails/Groovyユーザーグループ | Doorkeeper
建物とフロアは同じですが、会議室が通常と違いますので注意を。

LTネタ考え中。

Grailsで、tomcatを起動するというだけの目的でファンクショナルテストを設定する方法

Grailsでコントローラの試験を、モックを使わず実際の通信を使って試験したいケースがある。たとえば、Rest APIを開発する場合、Rest APIはブラウザを使用しなくても簡単に呼び出せるものなので、ユニットテストがしたいです。もちろんGrailsにはコントローラをモックで試験する機能があるが、rest APIを呼び出せば済むものを、偽物ですます必然性があまりないと個人的には思う(もちろん試験実施時のtomcat立ち上げ下げのための実行時間を短縮したい、などの関心はありうるが)。

このためにはファンクショナルテストを実行する必要がある(unit/integrationではtomcatはたちあがらない設定になっている)。しかし、Grailsでは、ファンクショナルテストは単にtest/functional配下にテストコードを置くだけでは実行できず、一つなんでもいいからファンクショナルテスト用プラグインを導入しておく必要がある。

Gebの機能は特に使いたくないとしても、たとえばHttpClientなどでRESTの試験だけしたいという場合に、以下の手順でGebを形だけでも導入することで、ファンクショナルテストを行うことができるようになる。

(1) BuildConfig.groovyのdependenciesセクションに以下を追加

      dependencies {
               :
          test "org.gebish:geb-spock:0.10.0"
      }

(2) BuildConfig.groovyのpluginsセクションに以下を追加

      plugins {
                :
        test ":geb:0.10.0"
      }

(3) BuildConfig.groovyでfolkedモードをfalseに設定する

     grails.project.fork = [
          :
         test: false, // ←
         run: [maxMemory: 768, minMemory: 64, debug: false, maxPerm: 256, forkReserve:false],
         war: [maxMemory: 768, minMemory: 64, debug: false, maxPerm: 256, forkReserve:false],
         console: [maxMemory: 768, minMemory: 64, debug: false, maxPerm: 256]
     ]

(4) テストコードをGRAILS_PROJECT/test/functional配下に作成。

ここでのテストコードは、Gebの機能を使う必要はなく、HttpClientなどを使用した通常のSpockコードで良い(もちろんGeb使っても良い)。

(5) テスト実行

     $ grails test-app functional:

上記はbook of gebより。

Grails/Groovyでのカバレッジ取得に関してのTIPS

プリミティブ最適化を抑制することでブランチカバレッジをましなものに

Grailsでは、コードカバレッジは以下で取得できる。

しかし、上記を使用した場合、分岐網羅(ブランチカバレッジ)は多くの場合、期待する値が取得できない。この理由の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. 環境変数CLASSPATHに設定
  2. GRAILS_PROJECT/libs配下に置く
  3. jarをpom化してローカル/どこまのmaven repoに置き、testディペンデンシーに記述する

どの方法でも良いが、「試験では最適化抑制をし、プロダクトコードでは最適化する」ということはリスクになるため、テスト時に常に抑制するのは怖い気がする(プロダクトコードでも最適化抑制するなら別だが)。なので、テストのとき常に、ではなく、カバレッジ判定のときだけ一時的な指定をするという意味で1でも良いかもしれない。

なお、上記の設定前後でgrails cleanを実行した方がよい。

*1:具体的にはコントローラのためのAST変換などの適用結果として、分岐文が追加されているときがある。しかし、本ノウハウを適用してノイズを除去した状態にすれば、「ここには分岐はないはず」「ここにラインカバレッジ上は到達しているのに、ここに来てないはずはない」といった類推によって、ブランチカバレッジを目視確認で把握可能である場合も多くなる。経験上。

Dockerを使ってGrails開発

Grails開発でdockerを使用するためのDockerfileを、備忘録兼ねて晒します。開発中に使うものです。運用用は、別のものになるでしょうかね。
onesysadmin/docker-grailsを元にして、Proxy設定を行い、いくつかの工夫をしております(プロキシ設定については、Dockerコンテナに透過的プロキシ設定とかをすれば不要なのかも)。
https://blog.linode.com/wp-content/uploads/2014/01/docker.pnghttp://www.simplicityitself.com/wp-content/uploads/2013/04/grails.png

利点

Grails開発でDockerを使う利点は以下の通り。

  • 準備が簡単。Grailsのインストールはもとより、gvmやJDKのインストールすらいらない。grails wrapperなどもいらないわけだ。
  • 環境を汚さない。JDKのインストールをしなくてすむ、Postgresなどをコンテナに封入して連携する、などによる。
  • 設定含めた開発時実行環境を開発メンバー間でシェアできる
  • Dockerはlinuxなので、開発環境windows、実行環境linuxによる差異をなくせる。windowsの場合、boot2dockerなどを使って仮想マシン上でlinuxを動かすことになる(MacOSXではDocker 1.3のboot2dockerでホストのフォルダの共有とVOLUME対応(-v指定による共有)がされているそう]だが、まだ試していない。)。

Dockerfileの準備

まず、onesysadmin/docker-grailsをcloneするなりダウンロードするなりして、使用したいGrailsのバージョンのDockerfileを、Grailsプロジェクトのトップディレクトリにコピーします。

Dockerfileの修正

以下に修正点を示します。修正したDockerfile全体はこちらのgistに

Proxyの設定

開発環境がファイアウォール背後でproxyの設定が必要になる場合は以下のように設定します。今回の場合、プロキシは以下において必要になります。

ENV PROXY_HOST 10.2.3.4
ENV PROXY_PORT 18080
ENV http_proxy http://${PROXY_HOST}:${PROXY_PORT}/
ENV https_proxy http://${PROXY_HOST}:${PROXY_PORT}/

ENTRYPOINT, EXPOSEの設定

ENTRYPOINT [ "/usr/local/bin/gvm-exec.sh", "grails", "-Dgrails.work.dir=/app/.grails" ]
EXPOSE 8080

gvm-exec.shや後で出てくるgvm-wrapper.shは「onesysadmin/docker-grails」の親であるイメージ「onesysadmin/gvm」が入れてくれているスクリプトです。

RUN関係

RUN gvm-wrapper.sh install grails 2.4.4 && \
    gvm-wrapper.sh flush archives && \
    cd /app && \
    gvm-exec.sh grails -Dgrails.work.dir=/app/.grails add-proxy client --host="${PROXY_HOST}" --port="${PROXY_PORT}" && \
    gvm-exec.sh grails -Dgrails.work.dir=/app/.grails set-proxy client
RUN mv /root/.grails/ProxySettings.groovy /root/.grails/ProxySettings.groovy.bak && \
    sed -e "s/'http.nonProxyHosts':''/'http.nonProxyHosts':'localhost'/" /root/.grails/ProxySettings.groovy.bak > /root/.grails/ProxySettings.groovy

proxyの設定がポイント。interactiveにしたときでrun-appしてstop-appして再起動したときにportが所有されつづけてしまう問題(address already in useとなる)の回避のため、nonProxyにlocalhostを設定します。

GRAILS_PROJECT/grails-app/conf/BuildConfigの修正

grails.project.dependency.resolutionの「mavenLocal()」を以下のように修正します。

        mavenLocal("/app/.m2")

これで依存jarやプラグインが毎回ダウンロードされるのを防ぐことができます*1。これがポイント。

コンテナのビルド

$ cd /path/to/grails/project
$ docker build -t you/project .

Grailsコンテナ実行(例)

$ docker run -it -p 8080:8080 -v /path/to/grails/project:/app you/project:latest # grailsインタラクティブ起動
$ docker run -d -p 8080:8080 -v /path/to/grails/project:/app you/project:latest run-app # grails run-app実行

ホストの8080ポートにアクセスするとコンテナの8080(Grailsのデフォルトポート)にアクセスできます。

DBコンテナとの連携

必要に応じて。以下はPostgresを前提とします。
Postgresのコンテナは、以下を参考に「orchardup/postgresql/」を使用します。

以降、通常のDB接続の設定(ドライバ追加のためにBuildConfig.groovyのdependenciesに「 runtime 'postgresql:postgresql:9.1-901.jdbc4'」を追加するなど)を行なった上での設定です。

DataSource.groovyの修正

driverClassNameやDB認証などの修正
dataSource {
    pooled = true
    // jmxExport = true
    driverClassName = "org.postgresql.Driver"
    username = System.getenv("DB_ENV_POSTGRESQL_USER")
    password = System.getenv("DB_ENV_POSTGRESQL_PASS")
}
JDBC URLの設定

必要な環境について、URLを設定します。

            url = "jdbc:postgresql://${System.getenv('DB_PORT_5432_TCP_ADDR')}:${System.getenv('DB_PORT_5432_TCP_PORT')}/docker"

「docker」はorchardup/postgresqlが初期的にCREATEしてくれるデータベース名です。別の名前にしたいときは、orchardup/postgresqlの起動時にDB名を指定できるので、それにあわせたものを使えば良いでしょう。

DB連携したGarilsコンテナ実行(例)

$ docker run -d -p 5432:5432 -e POSTGRESQL_USER=docker -e POSTGRESQL_PASS=docker --name pg orchardup/postgresql:latest
$ docker run -d -p 8080:8080 -v /path/to/grails/project:/app --link pg:db  you/project:latest run-app

おまけ

ENTRYPOINTを指定しているので、bashなどを起動したい場合は、

$ docker run -it -v /path/to/grails/project:/app --link pg:db  --entrypoint "bash" you/project:latest

こうかな。

まとめ

上記は、Grailsに限らず、開発時にdockerを使用する場合のパターンの一つになると思うが、ホストのディレクトリのマッピング(-v)を駆使してソース含めほとんどのファイルを外に置くコンセプトなので、単独実行するのと使用感も制約もあまりかわらない。しかし、このパターンだと、リモート利用、つまりdockerクライアントを実行するマシンとdockerデーモンが動作するマシンが別の場合には利用できない。つまりdockerデーモンを動かしているホストのlinuxマシンにログインできるアカウントがないと利用できない。dockerは単体でNFSや高速rsyncみたいなものを提供しないのが残念である。できればもっと開発用PaaSっぽく使えるのに!(ADDじゃ遅そうだし動的更新ができない)*2

このパターンであれば、Eclipse/GGTSなどをdockerで実行するのもそんなに難しくはない。

*1:なお、grails.project.dependency.resolver = "maven"にしているのにもかかわらず、/root/.grails/ivy-cacheが作成され何かが入っている。理由不明

*2:将来的にはFUSEで対応されるかもしれないんだって。やったー。