uehaj's blog

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

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で対応されるかもしれないんだって。やったー。

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ページサイトでも不要でしょう。でも、ヘルプページとか、社長のご挨拶ページとか、サイトマップとか、そういうページを含むサイト全体を作るために、この技法を織り交ぜて適用するのが有用なことがあるでしょう。

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