uehaj's blog

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

ペアワイズ法で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'に改名されていますので注意。

*1:ただし、githubのJCUnitのドキュメントは2014/10/13時点で、'com.github.dakusui:jcunit:0.4.10'で得られるjarと比べると、おそらく古い。本記事はソースコードを解析して書いています。 左記は勘違いでした。お詫びして訂正させて頂きます。古くありませんでした。たいへん申し訳ありません。

Elmでやってみるシリーズ13:あらためてシダを描く

間が少しあいちゃいましたが、実は続いていたこのシリーズ、「あらためてシダを描く」です。

f:id:uehaj:20140904230303p:plain

この図形は「バーンズリー(バーンズレイ)のシダ」と呼ばれる有名な図形で、以前各種の言語で実装するのが少し前に流行ったのが記憶に新しいところです。アルゴリズムなどについては詳しくはこちらをどうぞ。

実は、これについては、前にも「「プログラムでシダを描画する」をelmで描画する - uehaj's blog」で作ったのですが、当時はElmを良く知らずに試行錯誤で作ったものなので、現時点での知見をもとにして再度とりくんでみました。

このデモは計算量が大きく、さらにiframeでブログ内にインライン表示すると、なんらかの理由で非常に負荷が高くなる*1ので、今回はインラインデモは割愛します。全画面表示はこちら

ソースはこちら
ドラッグすると、パラメータが変更されて新たなシダ図形が描画さます。

ポイント

  • Main.elm(メイン), Leaf.elm(葉の表示)、Gauge.elm(ゲージの表示)の3モジュールに分割しました。Elmアプリケーションをモジュールに分割するにあたっては、Signalや状態更新の記述を独立性高く分割する方法について考えさせられました。
  • 結論から言うと、
    • モジュールに含まれるのが、純粋関数だけの場合には、思考すべきことはあんまりない。何を公開し何を隠蔽するかを吟味しておけば良い*2
    • モジュールが状態を保持する場合(そのコードにfoldpを含む場合)、モジュールごとにぞれぞれ以下のようにパートを分けて、単独実行可能なアプリケーションにする(参考:GameSkeleton.elm)。
      • Define constants
      • User input
      • State
      • Update
      • Display
      • puts it all together and shows it on screen.
    • その上で、メインプログラムからモジュールの状態更新(nextStateのような関数)、および現在状態を履歴から計算するfoldpの呼び出し等をどう扱うかについては以下から選択する
      • 定時間隔タイマ(fps,..)を使った状態更新が、機能上不要なモジュールであれば、モジュール側がliftしてSignal Elementを返しても良い。つまりイベントハンドラとステート管理をモジュール側で実装しても良い。
      • あるいは、定時間隔タイマを使うモジュールであっても、それを使うMain側が定時間隔タイマを使わないのであれば、ステート管理をモジュール側で実装してもよい。
      • しかし、モジュール側もMain側も定時間隔タイマを使う、もしくはMain側が定時間隔タイマを使うかどうか限定できない(一般ライブラリにする場合はそうなる)のであれば、両方でステート管理を実装すると、少なくとも現行Elmコンパイラが生成するJSコードにおいては性能劣化が激しい。なので、以下のようにする。
        • モジュール側では、
          • そのモジュールに関するStateレコードを定義し公開する
          • nextState, initStateに相当する処理を公開
          • 呼び出す側のMainでは、MainのStateレコードの1つのフィールドとしてモジュールのstateを含める。
        • 呼び出す側のMain側では、
          • nextStateの処理で、モジュール側のnextStateを呼び出し、MainのStateを更新する。
          • foldpを実行するのはMain側のみ。
      • その上で、モジュール側は、自分のfoldpを呼び出すmainを定義しても良い(Mainからは呼ばれない)。こうすることで、それぞれのモジュールを単独でも実行可能なElmアプリにできる。これはモジュールのデモや試験に有用。たとえば今回、赤のゲージはゲージ表示モジュール単独で実行できます(→Gauge.html)し、葉の表示モジュールでは、アニメーションや操作に反応しない葉っぱを表示させています(→Leaf.html)。
  • リアクティブっつーぐらいで、反応性重要
    • このための工夫として、ドラッグ動作中は描画・計算する点の数を減らすようにしました。
    • しかし、本来はこれは2モードにならなくても良いはずなのです。しかし残念ながら、現在のElmでは「計算中のマウスカーソル移動イベント」の検出の追随性が問題になります。(後述)
  • 前回同様以下の問題がありました。
    • 描画する点列の数nは実行を継続すればするほど増えていくのですが、Elmの標準ライブラリのGraphics.CollageおよびGraphics.Elementなどが提供するグラフィックスモデルは、ペイント系というよりドロー系で、描画されるデータが保持されるというものです。Canvasに書き残る、ということがないです(純粋だというわけですね)。
    • この結果、nに比例して描画時間が増えていきます。計算結果の点列([(Int,Int)])は、foldpで累積的に追加していけるので、O(1)なのですが、累積的に伸びた結果であるpoints:[Form]の描画時間はO(1)ではなく、O(n)なのです*3
    • この結果、何も考えないと、放置しておくとだんだん重くなってマウスクリックに反応しなくなります。回避策として以下を実施しています。
      • 1フレームの描画に要した時間を計測し、しきい値(例1秒)を越えるようなら、点列の追加ペースを落す(半減させる)。最終的に追加ペースは0になるので、そうなったら表示内容は収束し変化しなくなる。
      • 他の方針としては、まにあわなくなってきたら解像度とか計算精度落して、とかの戦略もありそうだったが、今回はできなかった。
      • 本来なら、Elm開発者Evanczの論文「Elm: Concurrent FRP for Functional GUI」によれば、Elmには「async」というプリミティブがあり、長い計算処理は適当な粒度で非同期イベントとしてイベントキューに再キューイングを行うことで、「ジャイアントUIスレッドロック」のようなことが起きなくなり、レスポンスを損なわずに長い計算ができるはずでした。しかし現状のElm実装にはasyncプリミティブはまだ実装されていません(そのことや理由も上記論文に書いてある*4し、解決されるべき課題として議論されている。)。(追記)asyncの代用として、Time.delay 0が使用可能だそうですが試してない。
    • 問題視してみましたが、例えば10000個とかの固定数で計算を打ち切れば良い話です。これをしなかった理由は、高速なCPUをもったマシンでは多数の点列を表示してゴージャスな表示、遅いマシンでは点数が少なくて、ちょっぴりみすぼらしいが表示はされる、というようなことを実現しようとしたかったからです。点数が多いと綺麗な表示になりますからね。
  • 諸事情によりElm 0.13のスナップショット版を使用。これはリリース版ではないので、自前でコンパイルしたい方は、googlegroupsのelm-discussionを見て適当にダウンロードしてください。

関連エントリ


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

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

プログラミングHaskell

プログラミングHaskell

Haskellによる並列・並行プログラミング

Haskellによる並列・並行プログラミング

*1:Chromeで発生、他のブラウザでは未確認

*2:ただし、Elmの現バージョンでは、エラーメッセージが不適切・もしくは全く出ないことがある(mainをexportしてない、拡張レコード型で暗黙に定義されるデータコンストラクタ関数が、個別export( (..)ではなく個々の関数名指定するケースで)未exportになる…)ので注意。

*3:virtual domのように、FormやElementから構成されるツリーの差分を検出し、差分だけをcanvas反映する機構があれば高速化できるかもしれませんね。でもいかにも難しそうですな。

*4:本質的には、JSにスレッドがないせいなんですが、将来的にはHTML5のWebWorkerでスレッドプーリングも併用するか、クロージャ本体を文字列にしてevalしてしかし引数をキャプチャ不要にしてなんとかとか。CPSに書き換えたりしてもできるんじゃないかな。

Elmでやってみるシリーズ12:遅延ストリームで多段階選抜

Elmでやってみるシリーズ12:遅延ストリームで多段階選抜

http://elm-lang.org/logo.png

横浜へなちょこプログラミング勉強会の過去問より、「多段階選抜 2014.8.2 問題というのをやってみます。以下が方針。

  • 無限長データ構造を使えといわんばかりの問題である。Haskellならそのまんまである。しかし「いわゆる遅延評価」ではない正格評価のElmではそのままでは無限長データは使えない。なのでmaxsnew/Lazyというのを使用します(後述)。
  • せっかくだからインタラクティブにしよう。

実行例

全画面はこちらからどうぞ。
以下はiframeでブログ記事内でインライン実行。なんかずれとりますが、全画面ではずれてません。
「記号列を入力」のところに例えば"ccc"と入力してみてください。
やってる内容は、多段階選抜 2014.8.2 問題を見てください。

ソースは以下もしくはこちら

filter.elm

-- http://nabetani.sakura.ne.jp/hena/ord24eliseq/
module Filter where
import Lazy.Stream as S
import String (toList, fromList)
import Graphics.Input.Field as F
import Graphics.Input (Input,input,dropDown)
import Char (toCode)
import Maybe
import Debug (log)

-- 無限長の自然数ストリーム
naturals : S.Stream Int
naturals = S.iterate (\it -> it+1) 1

-- 初期リスト
init : Maybe (S.Stream Int)
init = Just naturals

-- 入力文字に対応する各段階のフィルタとなる関数群
filter_n : Int -> S.Stream a -> S.Stream a
filter_n n stream = S.map snd (S.filter (\(a,b)->(a `mod` n) /= 0) (S.zip naturals stream)) -- 2〜9 の倍数番目を撤去(先頭が1番目であることに注意)

isSquare : Int -> Bool
isSquare n=any (\x->n==x*x) [1..n `div` 2+1]

filter_S : S.Stream Int -> S.Stream Int
filter_S x = S.zip x (S.cons 0 (\_->x)) |> S.filter (\(a,b)->not (isSquare b)) |> S.map fst -- 平方数の次を撤去

filter_s : S.Stream Int -> S.Stream Int
filter_s x = S.zip x (S.tail x) |> S.filter (\(a,b)->not (isSquare b)) |> S.map fst -- 平方数の直前を撤去

isCubed : Int -> Bool
isCubed n=any (\x->n==x*x*x) [1..n `div` 2+1]

filter_C : S.Stream Int -> S.Stream Int
filter_C x = S.zip x (S.cons 0 (\_->x)) |> S.filter (\(a,b)->not (isCubed b)) |> S.map fst -- 立方数の直後を撤去

filter_c : S.Stream Int -> S.Stream Int
filter_c x = S.zip x (S.tail x) |> S.filter (\(a,b)->not (isCubed b)) |> S.map fst -- 立方数の直前を撤去

filter_h : S.Stream a -> S.Stream a
filter_h = S.drop 100 -- 先頭の100件を撤去

-- 入力文字に対応するフィルタ関数を返す。その関数について:入力文字が不正な文字(2-9,cCsSh以外)であったり、フィルタの入力がすでにNothingであった場合Nothingが返る。
char2func : Char -> Maybe (S.Stream Int) -> Maybe (S.Stream Int)
char2func ch maybeStream =
    case maybeStream of
      Just stream -> if | '2'<=ch && ch<='9' -> Just (filter_n (toCode(ch)-toCode('0')) stream)
                        | ch == 'c' -> Just (filter_c stream)
                        | ch == 'C' -> Just (filter_C stream)
                        | ch == 's' -> Just (filter_s stream)
                        | ch == 'S' -> Just (filter_S stream)
                        | ch == 'h' -> Just (filter_h stream)
                        | otherwise -> Nothing
      Nothing -> Nothing

-- 入力文字列に対応するフィルタ関数群を取得し、そのすべてをfoldlで関数合成したものに初期リストを適用して結果を得る
solve : String -> Maybe (S.Stream Int)
solve s = foldl (\ch acc -> char2func ch acc) init (toList s)

-- フィルタ適用の各段階を表示する
dispResultStep : Int -> (a, String) -> Element
dispResultStep siz (ch, str) = flow down [flow right [asText ch, plainText "↓"]
                                         , solve str |> maybe (plainText "undefined") (asText . S.take siz) ]

-- フィルタ適用の全段階を表示する
dispResultSteps : Int -> String -> [Element]
dispResultSteps siz xs = zip (toList xs) (allSteps xs) |> map (dispResultStep siz)

-- フィルタ適用の途中段階用の入力文字列を生成
-- allSteps ["abc"] == ["a","ab","abc"]
allSteps : String -> [String]
allSteps x = let steps i x = map (\it -> fromList(i::(toList it))) x
             in foldr (\i acc -> [(fromList [i])] ++ (steps i acc))
                      []
                      (toList x)

-- 入力文字列
filterString : Input F.Content
filterString = input F.noContent

-- 入力文字列フィールド
filterField : F.Content -> Element
filterField fldCont = F.field F.defaultStyle filterString.handle id "記号列を入力" fldCont

-- 結果の幅
resultLength : Input Int
resultLength = input 10

-- 結果の幅の選択入力フィールド
resultLengthField : Element
resultLengthField = dropDown resultLength.handle [ ("10", 10), ("20", 20) ]

desc : Element
desc = [markdown|
[オフラインどう書く過去問題](http://yhpg.doorkeeper.jp/)[#24 多段階選抜](http://nabetani.sakura.ne.jp/hena/ord24eliseq/)
<table border>
<tr><th>記号<th>意味</tr>
<tr><td>29<td>29 の倍数番目を撤去(先頭が1番目であることに注意)</tr>
<tr><td>S<td>平方数の次を撤去</tr>
<tr><td>s<td>平方数の直前を撤去</tr>
<tr><td>C<td>立方数の直後を撤去</tr>
<tr><td>c<td>立方数の直前を撤去</tr>
<tr><td>h<td>先頭の100件を撤去</tr>
</table>
<br>
|]

-- 画面を構築
-- see:https://github.com/elm-lang/Elm/issues/523
main = let disp xs siz = flow down [ desc
                                   , (filterField xs `beside` plainText "長さ" `beside` resultLengthField)
                                   , (naturals |> S.take siz |> asText)
                                   , flow down (dispResultSteps siz xs.string) ]
       in disp <~ filterString.signal ~ resultLength.signal

気付いたことなど

  • Haskellはデフォルト非正格評価だが、Elmは正格評価の言語である。このセマンティクス上の違いはいずれも純粋関数型であるが故に「おおむね見えない」のだが、実用的には以下のように表われてくる。
    • (A) Haskellの場合、遅延評価に用いられるデータ構造と評価のタイミングにより、スペースリークの問題が生じ得ること。
    • (B)Elmでは無限長データ構造がデフォルトでは扱えない
  • (A)について、FRPの実装として、スペースリークが生じないことは、Elmが、Haskellのライブラリでもなく内部DSLでもなく、まさに今の形のように別言語であることの根本的な理由とされている。
  • (B)について、無限データ構造は、Elmの非標準(コミュニティ)ライブラリの、遅延ストリームライブラリ「maxsnew/Lazy」を明示的に使用することで実現できる。しかし、
    • Lazyバージョン0.3がリポジトリに公開されていない。0.2にはたぶんバグがあって、今回の使途では止まってしまい結果が出ない。
    • Lazyバージョン0.3を自前でコピーして使用すれば上記問題は解決するが、1点、Elm 0.12の文法変更にともなう修正が必要。
    • 上記2つの理由により、現時点(2014/08/14)ではelm-getでLazyのパッケージを取ってくるだけでの利用はできないと思われる。なので手動でやりました。
  • Elmのロゴは「タングラム」を表わしていて、用途によってバリエーションを作るのが良いそうです。
  • ElmのMaybeはモナドではない。なにしろ型クラスがないからね。それどころか(それゆえに?)、<~,liftなどは多相的に定義されていないので、アプリカティブ的にも使えない。困るかと思ったけどあまり困らない。Maybe.maybe便利。
  • Lazy.Streamのconsのシグネチャは以下。なんで()->なの? なんで a->Stream a -> Stream aじゃないのだろうか? ご存知の方教えてください。
    • cons : a -> (() -> Stream a) -> Stream a

関連エントリ


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

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

プログラミングHaskell

プログラミングHaskell

Haskellによる並列・並行プログラミング

Haskellによる並列・並行プログラミング

Elmでやってみるシリーズ11:お絵描きツール

Elmでやってみるシリーズ11:お絵描きツール

今回は、マウスボタンをクリックすると、半径4の赤いドットが描画され、押下したままドラッグすると線が描けるというもの。以下が実行イメージ。

f:id:uehaj:20140805235544p:plain

しかし今回は、いきなり完成させるのではなく、2ステップでやってみましょう。

ステップ1 マウスカーソルにドットが追随する


とりあえず、マウスカーソルにドットが追随するというもの。

上記を編集しての実行はこちらから。
ソースは以下のとおり。

import Mouse
import Window

-- マウスカーソル位置をElmのcollageの座標に変換する。collageの座標系は中心が原点でx軸は右が大きくy軸は上が大きい。マウスカーソル位置は左上が原点で、x軸は右に大きくy軸は下に大きい。
mousePosition : Int -> Int -> Int -> Int -> (Float, Float)
mousePosition x y w h = (toFloat x-(toFloat w/2), -(toFloat y)+(toFloat h/2))

-- ウィンドウサイズと同じ大きさのcollageの中に次のようなFormを表示: circleで作ったShapeをfilledしたFormをさらにmoveでマウスカーソル位置に移動したもの。
main : Signal Element
main = let disp x y w h = collage w h [move (mousePosition x y w h) <| filled red (circle 4) ]
       in disp <~ Mouse.x ~ Mouse.y ~ Window.width ~ Window.height

解説

これは本質的に、「マウスカーソルのx座標を表示しつづける」プログラム(→編集)

import Mouse

main : Signal Element
main = let disp x = asText x
       in disp <~ Mouse.x    -- この2行は main = asText <~ Mouse.x と等価

と同型のコードです。Signal引数が増えて、あとElementの構成が複雑になっただけ。Elementの構成は、純粋なコード呼び出しと組立ての地道な問題です。

ポイントは「記憶すべき状態がないプログラム」の例だということです。マウスカーソル位置を保持する主体はElmコードの外にあります。Elmコードはシグナルの変化に対して受動的にその場でリアクションだけすればいい。

さらに、暗黙のループがあると想像するのも正しいです。Elmプログラム一般に、mainはシグナルが更新されるたび、実際に何回も呼ばれます*1

ステップ2 軌跡を残す

さてステップ2として、マウスボタンが押下されたとき、画面にドットが残るようにします。
Elmのライブラリには「書いたらその情報がそのまま残るCanvas」という描画命令やデータ型は存在しません。ドット列全体の描画命令を再実行する必要があります。ドット列の情報は、マウスドラック操作によって追加されていくわけですから、プログラムは状態を保持しなくてはいけません。

そして、Elmでプログラムに過去からの状態を保持させる方法は、foldp、これ一択です*2。ドット列の情報は、座標位置x,yのタプルのリストとして保持するようにしましょう。

能書きは置いといて、とりあえず以下はshare-elmで動作中のコード。左ボタンを押してドラッグしてみてください。ソースや編集しての実行はこちらから。

コメントつきソースは以下です。

import Mouse
import Window

-- プログラムへの入力となる「イベント」を表現する代数的データ型Eventの定義。代数的データ型はとりあえずはenumのようなものと思っておけばよろし。
data Event = MouseUp | MouseDown | MouseMove Int Int

-- プログラムが保持するすべての「状態」を表現するレコード型に型名Statusをつける。Haskellでは型シノニムと呼ぶが気にしなくていい。mouseDownはボタンが押されているときTrue。pointsは座標列。
type Status = { mouseDown:Bool, points:[(Int, Int)] }

-- 初期状態(ボタンは押されてない、点は描画されてない)
initialState : Status
initialState = Status False []

-- 現状態stateからeventを受けて次状態を計算する。foldpの第一引数になることを想定。入力であるEventの中身を見て、対応する処理を行い、返り値のStatusを作ってリターンする。
nextState : Event -> Status -> Status
nextState event state = case event of
                          MouseUp -> { state | mouseDown <- False }
                          MouseDown -> { state | mouseDown <- True }
                          MouseMove x y -> if state.mouseDown
                                           then { state | points <- (x,y)::state.points }
                                           else state
-- 入力列をシグナル化。mergeはホモジニアスなリスト、ここでは「EventのSignalのリスト」を要求するので、リストの要素の型をSignal Eventにそろえる。MouseMove、MouseDown, MouseUpは代数的データ型のデータ構成子だが、これは関数のようなものなので、直接リフト(<~)ができる。
inputSignal : Signal Event
inputSignal = merges [ MouseMove <~ Mouse.x ~ Mouse.y
                     , (\b -> if b then MouseDown else MouseUp) <~ Mouse.isDown ]

-- foldpで過去を現在まで折り畳む。っていうか実質的にはイベントのシグナルの更新1回ごとにnextStateが呼ばれるように仕掛ける。この場合、nextStateはイベントハンドラだと思えばよろしい。状態はinitStateから始まり、nextStateの第二引数として、そしてそのnextStateのリターン値として持ち回る形で保持されていく。
currentState : Signal Status
currentState = foldp nextState initialState inputSignal

-- 先行のコードと同じ
mousePosition : Int -> Int -> Int -> Int -> (Float, Float)
mousePosition x y w h = (toFloat x-(toFloat w/2), -(toFloat y)+(toFloat h/2))

-- currentStateが返す「Signal Status」のStatus部分をとりだし、pointsを取り出してプロット。
main : Signal Element
main = let disp state w h =
            collage w h (map (\(x,y) -> move (mousePosition x y w h) <| filled red (circle 4)) state.points )
       in disp <~ currentState ~ Window.width ~ Window.height

気付いたことなど

  • 結構長いが、「currentState = foldp nextState initialState inputSignal」は、ほぼ完全に定型コード。あとはここを起点として呼ばれている、nextState,initialState,inputSignal、そして使われている型Event,Statusをしかるべく定義すれば良い。
  • MVCへの対応付けは以下のとおり。
    • Statusが全てのモデルに相当する。nextStateは全モデルの更新(純粋関数型なので、実際には更新せずに新規にデータを作ってる)を賄う。
    • currentStateが返す、シグナルでラップされたモデル(Status)をビューで表示するのは、main中のdispの役割り
    • コントローラにはInput,Field,Buttonなどの入力部品(今回はない)と、シグナルの依存関係の構築(上ではinputSignal)が対応するであろう。
  • Signal更新のたびに全ドット書いてるはずだが、点が多くなったときの速度は大丈夫だろうか*3。ビューポート指定して部分書き換えとかはできてないし、それがうまくできるかはわからない。
  • UI構築のために、Graphics.Collage,Graphics.Elementまわりの型をひととおりは覚えておく必要がある。最終的にはmainの型であるElementを作らないといけないのだが、以下の流れ:
    • 基本図形(丸とか四角とか多角形とか)
      • Shapeを(rect/circle/..)で作って、filledでFormにして、(move/rotate/..)で変形させて、collageでElementへ
    • 点列から
      • (segment/path)でPathを作って、LineStyleを与えてtraceでFormにして、(同上)
    • 文字列から
      • (plainText/asText)でStringから直接Element作る
      • Textを(toText)でStringから作り、(leftAligned/rightAligned/centered/..)でElement作る
      • Textを(link/monospace/..)で変換してTextを作り、同上
      • Textを(style)でStyleを与えて変換してTextを作り、同上
    • Elementから
      • Element同士をflow (right/down)..や`beside`などで繋げてElementにする。
    • Markdownから
      • [markdown|..|]でElement作る
    • :

関連エントリ


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


すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

プログラミングHaskell

プログラミングHaskell

*1:正確に言うと、mainが返すシグナルが保持するリフトされた純粋関数(ここではdisp)が何回も呼ばれる。どのタイミングで呼ばれるか? それは論理的には「すべてのシグナルのうちいずれか一つが更新したとき」なのだが、ある種の最適化のおかげで「常に呼ばれる」わけではなく、引数が変化しなければ引数としてキャッシュされた値が使用され、関数呼び出しはスキップされます。これはElmの関数が純粋だからできることです。純粋関数型万歳!!

*2:Singal.countもあるが、foldpで実現できる

*3:点の「追加」なので、差分更新が賢ければ速いはず。そしてelm-htmlのVirtual DOMはまさにそれをやってるはず。しかし、collageにそれができるか??

elmでやってみるシリーズ10: ボールの衝突回数で円周率を計算する

Elmでやってみるシリーズ10:ボールの衝突回数で円周率を計算する

id:wamanさんからひっそりと提案もらいましたので、やってみました。
今回は記事自身もElmで書きましたので、github-pages上の全画面(github上のソース)からどうぞ。

以下にも一応hatena blog記事中にiframeで表示しましたが、スクロールや文字の配置が読みにくい・もしくはJSの実行速度が遅いです。hatena blogではiframeにseamless属性つけられば良いのだが。iPhoneSafariでもiframe中だとうまくいきませんが、全画面なら動くようです。


ソースはこちら。

PiByBall.elm

module PiByBall where

import Window
import Debug (log)
import Graphics.Input (input, Input, button, dropDown)
import Keyboard

data Status = Pause | Running
type State = { stat:Status, x1:Float, x2: Float, v1: Float, v2: Float, count: Int, ratio: Float }
data Event = Start | Stop | TimeTick | ChangeN Int

frameRate = 320 -- 画面更新頻度

initialState : State
initialState = { stat=Pause, x1=-200, x2=-100, v1=1, v2=0, count=0, ratio=1 }

inputSignal : Signal Event
inputSignal = let f running = if | running -> Start
                                 | otherwise -> Stop
              in merges [(f <~ inpRunning.signal), (ChangeN <~ inpN.signal), (sampleOn (fps frameRate) (constant TimeTick))]

colide v1 v2 r =  (((r-1)*v1 + 2*v2)/(r+1), (2*r*v1 - (r-1)*v2)/(r+1), 1)

nextState : Event -> State -> State
nextState = \event ({stat, x1, x2, v1, v2, count, ratio} as state) ->
                        case event of
                          Start -> (log "Start" {initialState|stat<-Running, ratio<-ratio})
                          Stop -> (log "Stop" {state|stat<-Pause})
                          ChangeN n ->
                               (log "ChangeN" {initialState|ratio<-100^toFloat n})
                          TimeTick ->
                               if stat == Running then
                                   let (new_v1, new_v2, countIncl)
                                        = if | x1+v1>= x2+v2 -> colide v1 v2 ratio
                                             | x2+v2 >= 0    -> (v1, -v2, 1)
                                             | otherwise     -> (v1, v2, 0)
                                   in (log "timetick" State Running (x1+new_v1) (x2+new_v2) new_v1 new_v2 (count+countIncl) ratio)
                               else
                                  (log "Pause" {initialState|ratio<-ratio})

currentState : Signal State
currentState = foldp nextState initialState inputSignal

description1 = [markdown|
## ボールをぶつけるだけで円周率がわかる?
### シミュレーションの舞台

以下のように2つの質点M1,M2と壁を考えます。<br/>
|]

description2 = [markdown|
表示上の判り易さのために、質点の大きさに差を付けていま<br/>
すが、表示されている大きさは質点の質量の比率には対応し<br/>
ていません。

### 質量について

M1とM2の質量をそれぞれm1,m2としたとき、m1とm2の比率<br/>
を以下とします。

              m1:m2 = 100^N : 1

ここでNは0以上の整数値です。Nに応じて上記の比率は具体<br/>
的には以下のようになります。

<table border="1">
<tr><th>N</th><th>M1の質量(100^N) : M2の質量</th></tr>
<tr><td>0</td><td>1 : 1</td></tr>
<tr><td>1</td><td>100 : 1</td></tr>
<tr><td>2</td><td>10000 : 1</td></tr>
<tr><td>3</td><td>1000000 : 1</td></tr>
<tr><td>:</td><td>:</td></tr>
</table>

### シミュレーション

前提として、質点および壁は完全弾性衝突するとします。<br />
そして質点M1に右向きの適当な初速を与え、M2のM1および<br />
壁に対する衝突回数をカウントします。</p>

実際にやってみましょう。まず以下でNは変更せずに(N=0<br />
まま)「開始」ボタンを押してみて下さい。
|]

description3 = [markdown|
N=0のとき、衝突回数は最終的に3になったはずです。<br />
「最終的」といっても計算の打ち切り処理はしてませんので、<br />
永久に衝突しなくなるだろう時点を適当に判断してください。<br />
さらにNを1,2..と変えてみると、以下の結果になるでしょう。<br />

<table border="1">
<tr><th>N</th><th>衝突回数</th></tr>
<tr><td>0</td><td>3</td></tr>
<tr><td>1</td><td>31</td></tr>
<tr><td>2</td><td>314</td></tr>
<tr><td>3</td><td>3141</td></tr>
<tr><td>4</td><td>31415</td></tr>
</table>

注意深い読者は気づいたでしょうが、この回数が円周率に対応します。

<table border="1">
<tr><th>N</th><th>衝突回数c</th><th>c/10^N</th></tr>
<tr><td>0</td><td>3</td><td>3.0</td></tr>
<tr><td>1</td><td>31</td><td>3.1</td></tr>
<tr><td>2</td><td>314</td><td>3.14</td></tr>
<tr><td>3</td><td>3141</td><td>3.141</td></tr>
<tr><td>4</td><td>31415</td><td>3.1415</td></tr>
</table>

Nを増やせば増やすほど、精度が上っていきます。

### 留意点など

- 初速は結果には関係ありません。
- 質点と壁の具体的な初期位置は結果には関係ありません(M1,M2,壁の順序で並んでい<br />て、M1の初速が右向きである必要はあります)
- 衝突による速度の変化だけが結果を決めます。
- 正しい表示のためには、一定の離散時間でプロットするのではなく、時間精度を適<br />宜細かくとかしていく必要がありますが、このシミュレーションでは時間間隔一定<br />でプロットしています。動きが変なのは、そのせいです。

### 参考その他

この記事はElmを使って書いています。この記事を紹介している記事は[こちら](http://uehaj.hatenablog.com/entry/2014/08/03/234120)。
以下を参考に(計算式は丸パクリ)させて頂きました。

- [「2つのボールをぶつけると円周率がわかる」らしいのでシミュレーションしてみた](http://wasan.hatenablog.com/entry/2014/04/10/073638)
- [「2つのボールをぶつけると円周率がわかる」のをしつこく確かめてみた・・・解析的に](http://wasan.hatenablog.com/entry/2014/04/15/045611)

|]

bkcolor = rgb 200 200 256

inpN : Input Int
inpN = input 0

-- Nを選択。
selectN : Element
selectN = plainText "N=" `beside` dropDown inpN.handle
        [ ("0", 0)
        , ("1", 1)
        , ("2", 2)
        , ("3", 3)
        , ("4", 4)
        , ("5", 5)]

inpRunning = input False

startButton : Element
startButton = button inpRunning.handle True "開始"

stopButton : Element
stopButton = button inpRunning.handle False "停止"

-- シミュレーションを表示
simulation : Int -> Int -> State -> Element
simulation w h state = layers
                        [ collage w (h `div` 2)  <| [move (-(toFloat w / 4), 0) <| filled bkcolor (rect ((toFloat w)/2) ((toFloat h)/2))]
                          , flow down
                           [ flow right [ collage w (h `div` 2)  <|
                                          [ traced {defaultLine|width<-4} (segment (0, 200) (0, -200))
                                          , move (min 0 state.x1, 0) (filled red <|circle 5)
                                          , move (min 0 state.x2, 0) (filled red <|circle 2) ]
                           ] ] ]
-- 画面を表示
main : Signal Element
main=let disp w h state = spacer 10 10 `beside` flow down
          [ description1
          , image 610 362 "fig1.png"
          , description2
          , selectN
          , if state.stat == Running then stopButton else startButton
          , flow down [ "M1,M2の質量の比率(m1:m2)= 100^N:1 = "++show state.ratio++":1" |> plainText
                      , "M1の位置="++show state.x1 |> plainText
                      , "M2の位置="++show state.x2 |> plainText
                      , "衝突回数:"++show state.count |> plainText ]
          , simulation w h state
          , description3
          ]
     in disp <~ Window.width ~ constant 400 ~ currentState

補足

  • Elmの次回リリースでは、Markdown interpolationというのができるようになるので、この手のはもっとみやすく書けるようになるでしょう。
  • asパターン無いと思ってたらあった。キーワードasを使用します。上ではnextStateの引数「{stat, x1, x2, v1, v2, count, ratio} as state)」で使用。
  • Signalを1つのEventストリームにマージして、Event->State->Stateという関数を作ってfoldpに与えて…という基本構造は、いろいろ考えても、おちつくところにおちつく。あんまりバリエーションが生じない気がする。そのための、ある種のフレームワークがいくつか提案されているようだが、今後調査してみたい。(→Playground, automaton)
  • 古典的FRPでは、離散イベントを扱うEvent、連続的な変化を抽象化したBehaviorの2つで整理するようだが、Elmの採用するArrowizedFRPにおけるSignalは離散的であるという意味で古典的FRPのEventに対応している。SignalはEventのようにタイムスタンプを保持しているわけではないが、Time.timeStampでタイムスタンプを持ったSIgnalを生成することができる。Signal.sampleOnする先も離散的でよい(シグナルは最後の値1個を常に保持しているので値がとれないということはない)。Elmでは本質的に連続的に変化する値を扱うことはない。
    • 古典的FRPの「連続的時間」の意義は、こちらを読んで、時間解像度に独立した値を扱えることと理解。とすると、Elmのシグナルもまさにそういう風に扱える。Mouse.xは実質連続変化であり、任意の解像度でサンプリングできる。まあElmのSIgnalはBehaviorとEventの両方を表わしている、と思えばいいように感じられる。実際問題、コンピュータ上の時間粒度は無限小まで分解できるわけではないし、解釈の違い、ぐらいか。

関連エントリ


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


すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

プログラミングHaskell

プログラミングHaskell

elmでやってみるシリーズ9: JavaScript連携(JSのevalを呼ぶ)

Elmでやってみるシリーズ9: JavaScript連携(JSのevalを呼ぶ)

Elmではportというものを使用することで、ElmからJSの機能を呼ぶことができます(→Ports: Communicate with JS)。
portは名前がJS側に公開されるSignalです。
portには入力portと出力portがあります。

  • 入力port: JS→Elm
    • Elm側では予約語portを指定して、型宣言のみをする。
      • port evalIn : Signal String
    • JS側から以下のいずれかの方法で値を送り込む。
      • 初期値を Elm.embed()などでマップで与える。
      • <Elm.embed()などが返すelmモジュール>ports.ポート名.send()で送る。
  • 出力port: Elm→JS
    • Elm側では予約語portを指定して、Signalを定義する。
      • port evalOut : Signal String
      • port evalOut = btnInp.signal
    • そのSignalをUI操作などで間接的に更新することでJS側で登録したハンドラが呼ばれる
      • 登録方法は、
        • <Elm.embed()などが返すelmモジュール>.ports.ポート名.subscribe(ハンドラ関数)
      • ハンドラ関数の引数にSignalがラッピングしている値が渡ってくる

JS側ではJSの機能は何でも使えるのですが、以下ではevalを呼んでみました。

以下は画面キャプチャ。
f:id:uehaj:20140730060010p:plain

コードは以下のとおり。

module PortTest where

import Graphics.Input (button, input, Input)
import Graphics.Input.Field as F

inp : Input F.Content
inp = input F.noContent

btnInp : Input String
btnInp = input "S"

fld fldCont = F.field F.defaultStyle inp.handle id "JSの式を入力して下さい" fldCont
btn fldCont = button btnInp.handle fldCont.string "Eval"

port evalIn : Signal String

port evalOut : Signal String
port evalOut = btnInp.signal

main : Signal Element
main = let disp cont bname = flow down [fld cont, btn cont, plainText bname]
       in disp <~ inp.signal ~ evalIn

上記を呼び出すHTMLは以下。

<!DOCTYPE HTML>
<html>

<head>
  <meta charset="UTF-8">
  <title>Call JS from Elm</title>
  <script type="text/javascript" src="http://elm-lang.org/elm-runtime.js"></script>
  <script type="text/javascript" src="build/PortTest.js"></script>
</head>

<body>
  <div id="portTest" style="width:50%; height:400px;" ></div>
</body>

<script type="text/javascript">
var div = document.getElementById('portTest');

// embed our Elm program in that <div>
elmModule = Elm.embed(Elm.PortTest, div, {"evalIn": "initial"});
elmModule.ports.evalOut.subscribe(jsEval);
function jsEval(exp) { elmModule.ports.evalIn.send(eval(exp).toString()); }

</script>

</html>

実行画面(操作可能)

全画面はこちらから。

気づいたこと・解説

  • JSからElmに渡される値の型は厳しくチェックされる。自動的にtoString()を呼んでくれたりはしない。
  • portは「値」ベースの情報交換である。直接相互の関数を呼んだりはできない。
    • Fregeと比べると興味深い。JSのpureな関数があってもそれをElmから直接を呼ぶことはできない(今のところ)
  • 他のaltJSに比べれば、一手間かかるわけだが、evalは万能インターフェース(文字列限定)
  • Elmにおいて煩雑に思われるJsonからの値のとりだしはJS側でやるという手もあるかもね。
  • シグナル間の依存関係は以下のとおり。
                                                                           evalOut:Signal String
                                                                                 ^
"Eval"Button                              btnInp:Input String                    /
btn:Element ==================================> handle:Handle String            /btnInp.signal
                                                signal:Signal String ----------/
                                                   ^
                                                   :inp.signal.string
                                                   :(fldCont = inp.signal;
JS Exp Field    inp:Input F.Content                : btnInp.signal = Signal (fldCont.string)
fld:Element ========> handle:Handle F.Content     /
                      signal:Signal F.Content ../


                             Result Text
evalIn:Singal String ------> plainText bname:Element

(凡例)
signalA -------> signalB
signalBはsignalAを参照している
signalAの値が変更されるとsignalBの値が再計算される
(情報の流れの向きと参照関係は逆)


ElementE =======> HandleH
ElementEはHandleHを参照している。
ElementEのフィールド入力値が変更されるとhandleHを保持するInputに所属するSignalで更新イベントが発生する
(情報の流れの向きと参照関係は同じ)


valueA ........> valueB
valueAがvalueBとして使われている。純粋な値のコピー。
  • btnを押したときに、evalOutへの出力はされるのに、fldを変更したときに、evalOutへ出力されないのはなぜか?それは、全体構造を良く見るとわかるように、fldが変更されたとき、inp.signalが変化し、その値をもとにbtnが再構築されるから。btnInp.signalはinp.signalに依存していない。inp.signal.stringの(純粋な)値を使って構築されている。

関連エントリ


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


すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

プログラミングHaskell

プログラミングHaskell

Groovy 2.4のgroovyshでは、defが効くようになった。

https://jira.codehaus.org/browse/GROOVY-6623
で対処されました。

% groovysh
Groovy Shell (2.4.0-beta-1, JVM: 1.8.0_11)
Type ':help' or ':h' for help.
-------------------------------------------------------------------------------------------------------------------------------
groovy:000> def x = 3
===> 3
groovy:000> x
===> 3
groovy:000> :set
Preferences:
    interpreterMode=true
groovy:000> x.class
===> class java.lang.Integer
groovy:000> String x = 3
===> 3
groovy:000> x.class
===> class java.lang.String
groovy:000>

こうです。
(2014/07/31追記)
もちろん、defに限らず

groovy:000> String t = "abc"
 000> String t = "abc"
===> abc
groovy:000> t
 000> t
===> abc

のように型を指定してもOKです。ローカル変数のスコープが、1行単位ではなく、セッションを通じて有効になる、という話。今までは一行ごとだった、つまり

groovy:000> String s="a"; s
 000> String s="a"; s
===> a

はエラーにならなかった。
(2014/07/31追記終り)

前の動作に戻すには、

groovy:000> :set interpreterMode false
groovy:000> def y = 3
===> 3
groovy:000> y
Unknown property: y

こうします。なんで今まで…、っていう話はあるんでしょうが、行ごとにnew GroovyShell()してたから、という実装の都合で、まあ、あれだ。

また、別件ですが、
https://jira.codehaus.org/browse/GROOVY-6754
によれば、
groovyshに、groovyコマンドと同様に-eオプションが指定でき、groovysh起動のタイミングで固有の初期設定コードが実行できるようになりました。