uehaj's blog

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

Dockerを使ってGrails開発

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

利点

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  # proxy address/hostname
ENV PROXY_PORT 18080    # proxy port
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で対応されるかもしれないんだって。やったー。

PHPのようにGrailsを使う方法

GrailsアプリケーションではGSPのビューは以下の方法で表示できます(他にもあるかも)。

  • コントローラーアクションの結果として表示
    • renderで指定したビュー
    • コントローラ名とアクション名から定まるデフォルトのビュー
  • ビューを直接表示する
    • URLMappingでURLパターンからビューを指定する

しかし、「ビューだけ」の、GSPの集合だけで機能するサイトは作れません。JSPで言うと、JSPだけのServlet無しのWebサイト、あるいはPHPの埋め込みページだけで構成するようなサイトが作れないのです。

そうなのはおそらく意図的であり、それがWeb開発の黎明期にアンチパターンとしてみなされてきたので排除されているのでしょう。上記のような制約「GSPはコントローラからの画面遷移を経て表示される」があることのメリットは、GSP内で使用できるデータの由来を明確化できること、そして、画面遷移のリンク情報をページの中にばらまくのではなく、コントローラのコードで指定することでしょう。

ただ、そうだとしても、以下のような開発手順をとりたいときがあります。

  • 開発時に、まずHTMLでのモックアップがある(デザイナーや企画者が作成するなど)
  • それをGSPに一括変換する
  • 適宜以下を実施する
    • FORMの部分をGSPタグ<g:form>でおきかえる
    • レイアウトをSiteMeshでおきかえる
    • 静的リソースはアセットにして…

現状のGrailsでは、上記の手順にはなりません。ページの持つ機能をコントローラ・アクションの階層に分割し、そしてコントローラアクションの結果としてのページ群に対応させ…、というトップダウンの機能設計をせざるを得ず、直接画面確認に行けないのです。

本記事では、この問題を解決するために、ページ間遷移について個別のコントローラ無しGSPの集合としてアプリを作る、つまり「PHPのようにGrailsを使う」方法を紹介します。

DispatchController

showPageというアクションを1つだけ持った以下のコントローラDispatchControllerを用意します。

package common

class DispatchController {

    /** grails-app/view/pages配下に配置された、コントローラに所属しない
     * (コントローラのアクション名と一致していることを期待せず、リンク
     * をたどることやメニュー選択の結果として表示されることを想定して作
     * 成されている)ページを表示する。
     */

    def showPage() {
        render view:"/pages/${params.pageName}"
    }
   
}

URLMappingに以下を追加します。

        "/pages/$pageName"(controller: "dispatch", action: "showPage")
        "/"{
            controller = "dispatch"
            action = "showPage"
            pageName = "index"
        }


これで準備は終わり。あとは

配下に、たとえば

というGSP(htmlの拡張子をgspにリネーム)を置けば、

      <a href="${request.contextPath}/pages/a">..</a>

というようなリンクで相互にGSPビューを表示できるようになります。views/pages/index.gspがトップページになります。
あもちろん、<g:link>とか使っても良いです。

まとめ

コントローラ・アクションの編成の方法論というのは、Grails初学者にとっては難しいところです。

Scaffoldではドメイン/アクションをベースにしたビューを生成してくれますが、Webサイトというものが一般には「DBテーブルエディタ」ではない以上、この構造は必ずしも一般的ではなく、参考にならないときもあります。

テーブル単位のCRUDに限らない、コントローラ・アクションの階層をどう編成すべきか。この問いに、なんとか解を出さないと、index.gsp以外の1ページを表示することすらままならないかもしれないわけです。デフォルトのindex.gspのようにページごとにURLMapping追加していってもいいんですが、面倒だし不毛ですよね。

多数のページから呼ばれ得る多数のコントローラの関係があるとき、むしろコントローラ/アクションの階層をページ集合とは独立した機能群として設計する方がやりやすいときもあるでしょう。

このPHPぽいアプローチならば、動くサイトを拡張しながら、コントローラを都度考えていくことができます。また、それとは別に、Grailsの初心者にとってのとっつきは、はるかに良くなると思います。

もちろん、この技法と通常のコントローラベースのアプローチを組合せても全く問題ありません。

問題もありえます。この方法だけで、大規模サイトとか業務システムのサイトの全体をつくるべきではないでしょう。単機能の1ページサイトでも不要でしょう。でも、ヘルプページとか、社長のご挨拶ページとか、サイトマップとか、そういうページを含むサイト全体を作るために、この技法を織り交ぜて適用するのが有用なことがあるでしょう。

ということで、選択肢の一つとして心にとめておいておくと便利なのではないかと思い、紹介させてもらいました。

ヒアドキュメントと複数行文字列について

「ヒアドキュメント」をなんで「ヒアドキュメント」っていうかを調べてみた。以下が参考ページ。

わかったこと

上記の内容が正しいとして、読み取ったことは以下のとおり。

  • 「ヒアドキュメント」は「"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"キーがその後消滅することなんか、誰にも想像つかないし!)

また、PerlRubyなどの言語における「ヒアドキュメント」機能は、その背景機構や元々の命名理由とは無関係にShellスクリプトとの表記上の類似性だけでそう呼んでいると推測できる。なぜなら、Shellにあった、起動したプロセスにパイプ繋いで送り込む、という様相が存在しないためである。このことを批判するつもりは別にないが、Shellスクリプトにおいて「ヒアドキュメント」という名称が背後機構をちゃんと説明するものであった、という利点は失うこととなっている。

しかし、PythonやGroovyなどの複数行文字列定数で使用する"""〜"""などには、表記上の類似性すらもないので、ヒアドキュメントと呼ぶ必要が全くないと思う*1。なので自分はそう呼ばないことにしている。なので「プログラミングGroovy」の本にもヒアドキュメントという用語を使うことは意図的に避け、確か複数行文字列定数と呼ぶように徹底したのであるよ。

プログラミングGROOVY
プログラミングGROOVY
posted with amazlet at 14.10.24
関谷 和愛 上原 潤二 須江 信洋 中野 靖治
技術評論社
売り上げランキング: 48,552

*1:改行を含んでいるとドキュメントっぽいから、ということが理由なら、バックスラッシュ('\')で行末エスケープした通常の文字列定数もヒアドキュメントと呼ぶべき。さらに「ヒア」の意味があるものすべてにヒアをつけるべき。ヒア整数、ヒア引数、ヒア関数…

ペアワイズ法でSpockのテストデータを生成する

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を見て適当にダウロードしてください。

関連エントリ

すごい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

関連エントリ

すごい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作る
    • :

関連エントリ

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

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

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

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

プログラミングHaskell

プログラミングHaskell

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

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

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