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" }
これで準備は終わり。あとは
配下に、たとえば
- <GRAILS_PROJ>/grails-app/views/pages/index.gsp
- <GRAILS_PROJ>/grails-app/views/pages/a.gsp
- <GRAILS_PROJ>/grails-app/views/pages/c.gsp
という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ページサイトでも不要でしょう。でも、ヘルプページとか、社長のご挨拶ページとか、サイトマップとか、そういうページを含むサイト全体を作るために、この技法を織り交ぜて適用するのが有用なことがあるでしょう。
ということで、選択肢の一つとして心にとめておいておくと便利なのではないかと思い、紹介させてもらいました。
ヒアドキュメントと複数行文字列について
「ヒアドキュメント」をなんで「ヒアドキュメント」っていうかを調べてみた。以下が参考ページ。
- http://programmers.stackexchange.com/questions/143918/why-is-it-called-a-here-document
- http://ja.wikipedia.org/wiki/%E3%83%86%E3%83%AC%E3%82%BF%E3%82%A4%E3%83%97%E7%AB%AF%E6%9C%AB#.22Here_is.22_.E3.82.AD.E3.83.BC
わかったこと
上記の内容が正しいとして、読み取ったことは以下のとおり。
- 「ヒアドキュメント」は「"Here is" document」から来ている。
- "Here is"は何からきているかというと、昔テレタイプ端末にあった「Here is」というキー。
- 「Here is」キーは何かというと、押すとあらかじめ端末ごとに設定できた20文字ぐらいの文字列をホストに送り返すキー。この用途は、例えば、その文字列に端末の識別IDなどを設定しておいて、1キーで「この端末はXXXだよ」とホストに送ること。
- さらに、ホストが送ったENQという制御文字を端末が受けとると、自動的にHere isキー登録文字列をホストに送信するように設定することも可能。ENQの送信は、ホスト側から、ログインしてるあんたの端末どれよ/あんた誰よ(操作者の名前をhere isキー文字列に登録しておいた場合か)、という情報の問合せをするのに用いられた。
ShellスクリプトにおけるHere documentの意味について
上記参考リンクには、それほど詳細には説明されていないので、ここからは推測も交えて、になる。
Shellスクリプトにおけるヒアドキュメントは、Shellが起動する、コマンドのプロセスに対して特定の文字列をパイプ(もしくはシングルタスクOSでは一時ファイル?)を通じて送りつける、という機能である。
cat <<EOT ... EOT
上では、プログラム中に書かれた固定文字列「...」の部分をShellが切り出し、そのデータをパイプ(やひょっとしたら一時ファイル)を通じてcatプロセスに送り込んでいる。これがホストに対してHere isキー登録文字列(=固定文字列)を送付している振舞に似ている。だからこの機能をHere (is) documentと名付けたのではなかろうか。
複数行を含むことができる文字列定数をヒアドキュメントと呼ぶことについて
時はながれて。
これを調べたのは、現代のプログラミング言語のいくつかにおいて、「改行文字を含むことができる」程度の機能をもった文字列定数の表記法を、「ヒアドキュメント」と呼ぶ場合があることにもともと違和感があったからである。
Shellスクリプトにおいては、任意の文字列を処理対象としてコマンドに渡すのにパイプやファイルを経由するしかない場合があるが、その処理を簡易に記述する機能を「ヒアドキュメント」と呼んだのは、当時において合理性があったように思われれる。(まさか、"Here is"キーがその後消滅することなんか、誰にも想像つかないし!)
また、PerlやRubyなどの言語における「ヒアドキュメント」機能は、その背景機構や元々の命名理由とは無関係にShellスクリプトとの表記上の類似性だけでそう呼んでいると推測できる。なぜなら、Shellにあった、起動したプロセスにパイプ繋いで送り込む、という様相が存在しないためである。このことを批判するつもりは別にないが、Shellスクリプトにおいて「ヒアドキュメント」という名称が背後機構をちゃんと説明するものであった、という利点は失うこととなっている。
しかし、PythonやGroovyなどの複数行文字列定数で使用する"""〜"""などには、表記上の類似性すらもないので、ヒアドキュメントと呼ぶ必要が全くないと思う*1。なので自分はそう呼ばないことにしている。なので「プログラミングGroovy」の本にもヒアドキュメントという用語を使うことは意図的に避け、確か複数行文字列定数と呼ぶように徹底したのであるよ。
(2014/12/24)
記憶をたどれば、昔のGroovy(Classic Groovy)には、「本当の(RubyやPerlの意味での)」ヒアドキュメントが実際にあったのですが、JSRに提案される段階で削除されました。ヒアドキュメントの廃止 - どうせ見苦しいですから (smile)。もし使いたい場合はかわりにトリプルクォートを使いましょう
この意味でも、Groovyの複数行文字列定数をヒアドキュメントと呼ぶのはまぎらわしい。
*1:改行を含んでいるとドキュメントっぽいから、ということが理由なら、バックスラッシュ('\')で行末エスケープした通常の文字列定数もヒアドキュメントと呼ぶべき。さらに「ヒア」の意味があるものすべてにヒアをつけるべき。ヒア整数、ヒア引数、ヒア関数…
ペアワイズ法でSpockのテストデータを生成する
(関連記事:http://uehaj.hatenablog.com/entry/2015/10/18/155107 )
JCUnitというすばらしいJUnitの拡張があります。
JCUnitではペアワイズ法もしくはオールペア法という手法でテストデータを生成します。ペアワイズ法というのは、試験対象コードに対するパラメタのバリエーションテストをする際に、より少ないテストデータの件数で、効率良くバグを発見できるというテストデータの選択技法だそうです。(オールペア法の記事)
折角の自動テストならば、テストデータも自動生成してしまおうと。しかし単純に総当たりだと、指数的にケース数が増えて手に負えなくなるので、統計学に基づいて、優れているとされている方法でデータを選択し実用的には十分なようにしよう、ということです。
JCUnitはJUnit4のRunner(@RunWithアノテーション)を使って実現されていますが、本記事では、そのエンジンだけを呼び出すことで、JCUnitをSpockのデータドリブンテストで使う方法を紹介します。
方法
Spockのテストケースで以下のように「genPairwiseTestData」を使用してデータを生成します。genPairwiseTestDataはJCUnitの機能を呼び出すためのラッパーとして機能するstaticメソッドで定義は後述のテストコード全体に含まれています。
@Unroll def "分配法則(a=#a,b=#b,c=#c)"() { expect: c*(a+b) == c*a + c*b where: [a,b,c] << genPairwiseTestData(new Object(){ @FactorField public int a,b,c } ) }
上記中、「[a,b,c] <<は」Spockのデータパイプという機能で、a,b,cというテストコードで使用する変数に対して、3要素のリストの集合を流し込むことで、パラメータのバリエーションに対してテストコードが複数回呼び出されるというものです。@Unrollはさらにそれをテストレポート上で複数のテストメソッドがあるかのように表示するアノテーションです。これらはいずれもSpockの機能に乗っとっていて、データパイプに流し込むデータをJCUnitの機能を使って生成することで、機能連携しています。
@FactorFieldはJCUnitが提供するアノテーションで、JCUnitで指定できる機能を利用できます*1。
上記では、[a,b,c]がすべてintの場合ですが、型がもし違っていれば、
[a,b,c] << genPairwiseTestData(new Object(){ @FactorField public int a @FactorField public String a @FactorField public double c } )
のようにそれぞれ指定します。<<の左辺に表われる変数(上記ではa,b,c)は、右辺の@FactorFieldで同じ順序で一回ずつ指定される必要があります(冗長だが、現状の仕組みでは仕方がない)。
長いテストコード例
以下は単独で実行できるように@Grabで指定したSpockテストコードです。Grails中のSpockテストコードやGradleからの使用では適切な依存関係の指定で置き換えることになるでしょう。
groovy JCUnit.groovy
で実行できます。
@Grab('com.github.dakusui:jcunit:0.4.10') @Grab('org.spockframework:spock-core:0.7-groovy-2.0') import com.github.dakusui.jcunit.core.FactorField; import com.github.dakusui.jcunit.generators.TupleGeneratorFactory; import com.github.dakusui.jcunit.core.tuples.Tuple; import com.github.dakusui.jcunit.generators.ipo2.IPO2; import com.github.dakusui.jcunit.generators.ipo2.optimizers.GreedyIPO2Optimizer; import com.github.dakusui.jcunit.constraint.constraintmanagers.ConstraintManagerBase; import com.github.dakusui.jcunit.constraint.ConstraintManager; import com.github.dakusui.jcunit.exceptions.UndefinedSymbol; import spock.lang.* class HelloSpec extends Specification { static ConstraintManager closureConstraintManager(List<String> names, Closure c) { return new ConstraintManagerBase() { @Override boolean check(Tuple tuple) throws UndefinedSymbol { Map map = [:] for (name in names) { if (!tuple.containsKey(name)) { throw new UndefinedSymbol(); } map[name] = tuple.get(name) } c.delegate = map c.call() } } } static Collection genPairwiseTestData(Object object, Closure constraint = null) { def tg = TupleGeneratorFactory.INSTANCE.createTupleGeneratorForClass(object.class) def names = tg.factors.collect{it.name} def cm if (constraint == null) { cm = tg.constraintManager } else { cm = closureConstraintManager(names, constraint) } IPO2 ipo2 = new IPO2(tg.factors, 2, cm, new GreedyIPO2Optimizer()) ipo2.ipo() return ipo2.result.collect{ testData -> names.collect{ testData[it] } } } @Unroll def "分配法則(a=#a,b=#b,c=#c)"() { expect: c*(a+b) == c*a + c*b where: [a,b,c] << genPairwiseTestData(new Object(){ @FactorField public int a,b,c } ) } @Unroll def test1() { expect: c*(a+b) == c*a + c*b where: [a,b,c] << genPairwiseTestData(new Object(){ @FactorField public int a,b,c }, { a > 0 && b != c }) } @Unroll def test2() { expect: (a+b).size() == a.size() + b.size() where: [a,b] << genPairwiseTestData(new Object(){ @FactorField public String a,b }) } @Unroll def test3() { expect: a+b == b+a where: [a,b] << genPairwiseTestData(new Object(){ @FactorField(intLevels=[1,2,3]) public int a @FactorField public int b }) } @Unroll def test4() { expect: a == b || a != b where: [a,b] << genPairwiseTestData(new Object(){ @FactorField public MyBoolean a @FactorField public MyBoolean b }) } } enum MyBoolean { True, False }
データの生成のされかた
3つのintデータは、デフォルトでは以下のデータがペアワイズ法で生成されます。1, 0, -1, 100, -100, Integer.MAX_VALUE, Integer.MIN_VALUEの7種類の値が、a,b,cのパラメータに流し込まれるので、全組み合せだと7^3=343ケースが実行されるところを、53テストケースに絞り込まれていますね。
[[a:1, b:1, c:0], [a:1, b:0, c:100], [a:1, b:-1, c:-1], [a:1, b:100, c:-100], [a:1, b:-100, c:2147483647], [a:1, b:2147483647, c:-2147483648], [a:1, b:-2147483648, c:1], [a:0, b:1, c:100], [a:0, b:0, c:2147483647], [a:0, b:-1, c:-100], [a:0, b:100, c:0], [a:0, b:-100, c:1], [a:0, b:2147483647, c:-1], [a:0, b:-2147483648, c:-2147483648], [a:-1, b:1, c:2147483647], [a:-1, b:0, c:-2147483648], [a:-1, b:-1, c:1], [a:-1, b:100, c:-1], [a:-1, b:-100, c:100], [a:-1, b:2147483647, c:-100], [a:-1, b:-2147483648, c:0], [a:100, b:1, c:-2147483648], [a:100, b:0, c:-1], [a:100, b:-1, c:0], [a:100, b:100, c:1], [a:100, b:-100, c:-100], [a:100, b:2147483647, c:2147483647], [a:100, b:-2147483648, c:100], [a:-100, b:1, c:1], [a:-100, b:0, c:0], [a:-100, b:-1, c:-2147483648], [a:-100, b:100, c:100], [a:-100, b:-100, c:-1], [a:-100, b:2147483647, c:100], [a:-100, b:-2147483648, c:-100], [a:2147483647, b:1, c:-100], [a:2147483647, b:0, c:1], [a:2147483647, b:-1, c:100], [a:2147483647, b:100, c:2147483647], [a:2147483647, b:-100, c:0], [a:2147483647, b:2147483647, c:-2147483648], [a:2147483647, b:-2147483648, c:-1], [a:-2147483648, b:1, c:-1], [a:-2147483648, b:0, c:-100], [a:-2147483648, b:-1, c:2147483647], [a:-2147483648, b:100, c:-2147483648], [a:-2147483648, b:-100, c:-2147483648], [a:-2147483648, b:2147483647, c:1], [a:-2147483648, b:-2147483648, c:2147483647], [a:-100, b:2147483647, c:2147483647], [a:-2147483648, b:1, c:0], [a:-2147483648, b:-100, c:100], [a:1, b:2147483647, c:0]]
他のデータ型について、デフォルトではどのようなデータの集合(レベル)をつかってデータが生成されるかはこちら。
Enumは勝手に全要素を組合せてくれます。
データ集合の指定
元になるデータの集合(レベル)を指定することもできます。たとえば、intについて1,2,3の3通りのデータからの組合せを生成するには、以下のようにします。
@FactorField(intLevels=[1,2,3]) public int a
制約
データが満たすべき制約條件を与えて、テストケース数をふるいにかけて減らすこともできます。
以下では、変数間の関係がa > 0 && b != cを満たすテストケースに絞り込みます。
[a,b,c] << genPairwiseTestData(new Object(){ @FactorField public int a,b,c }, { a > 0 && b != c })
Groovyなので、クロージャで制約式を与えられるようにしてみました。
おまけ
生成されるテストデータは毎回かわるわけではないので、テストデータをファイルにキャッシュする仕組みがあると良い気もする。
まとめ
JCUnitすばらしい!Spock便利! Groovy超便利!!
追記(2014/10/27)
JCUnitの更新情報として作者の方からコメントを頂いています。記事中では使用していませんが、'levelsFactory'メソッドが0.4.11か0.4.12で'levelsProvider'に改名されていますので注意。
Elmでやってみるシリーズ13:あらためてシダを描く
間が少しあいちゃいましたが、実は続いていたこのシリーズ、「あらためてシダを描く」です。
この図形は「バーンズリー(バーンズレイ)のシダ」と呼ばれる有名な図形で、以前各種の言語で実装するのが少し前に流行ったのが記憶に新しいところです。アルゴリズムなどについては詳しくはこちらをどうぞ。
実は、これについては、前にも「「プログラムでシダを描画する」を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
- 作者: Miran Lipovača,田中英行,村主崇行
- 出版社/メーカー: オーム社
- 発売日: 2012/05/23
- メディア: 単行本(ソフトカバー)
- 購入: 25人 クリック: 580回
- この商品を含むブログ (67件) を見る
- 作者: Miran Lipovaca
- 出版社/メーカー: オーム社
- 発売日: 2012/09/21
- メディア: Kindle版
- 購入: 4人 クリック: 9回
- この商品を含むブログを見る
- 作者: Graham Hutton,山本和彦
- 出版社/メーカー: オーム社
- 発売日: 2009/11/11
- メディア: 単行本(ソフトカバー)
- 購入: 14人 クリック: 503回
- この商品を含むブログ (117件) を見る
- 作者: Simon Marlow,山下伸夫,山本和彦,田中英行
- 出版社/メーカー: オライリージャパン
- 発売日: 2014/08/21
- メディア: 大型本
- この商品を含むブログ (2件) を見る
*2:ただし、Elmの現バージョンでは、エラーメッセージが不適切・もしくは全く出ないことがある(mainをexportしてない、拡張レコード型で暗黙に定義されるデータコンストラクタ関数が、個別export( (..)ではなく個々の関数名指定するケースで)未exportになる…)ので注意。
*3:virtual domのように、FormやElementから構成されるツリーの差分を検出し、差分だけをcanvas反映する機構があれば高速化できるかもしれませんね。でもいかにも難しそうですな。
*4:本質的には、JSにスレッドがないせいなんですが、将来的にはHTML5のWebWorkerでスレッドプーリングも併用するか、クロージャ本体を文字列にしてevalしてしかし引数をキャプチャ不要にしてなんとかとか。CPSに書き換えたりしてもできるんじゃないかな。
Elmでやってみるシリーズ12:遅延ストリームで多段階選抜
Elmでやってみるシリーズ12:遅延ストリームで多段階選抜
横浜へなちょこプログラミング勉強会の過去問より、「多段階選抜 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>2〜9<td>2〜9 の倍数番目を撤去(先頭が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」を明示的に使用することで実現できる。しかし、
- Elmのロゴは「タングラム」を表わしていて、用途によってバリエーションを作るのが良いそうです。
- ElmのMaybeはモナドではない。なにしろ型クラスがないからね。それどころか(それゆえに?)、<~,liftなどは多相的に定義されていないので、アプリカティブ的にも使えない。困るかと思ったけどあまり困らない。Maybe.maybe便利。
- Lazy.Streamのconsのシグネチャは以下。なんで()->なの? なんで a->Stream a -> Stream aじゃないのだろうか? ご存知の方教えてください。
- cons : a -> (() -> Stream a) -> Stream a
関連エントリ
「Elmでやってみる」シリーズのまとめエントリ - uehaj's blog
- 作者: Miran Lipovača,田中英行,村主崇行
- 出版社/メーカー: オーム社
- 発売日: 2012/05/23
- メディア: 単行本(ソフトカバー)
- 購入: 25人 クリック: 580回
- この商品を含むブログ (67件) を見る
- 作者: Miran Lipovaca
- 出版社/メーカー: オーム社
- 発売日: 2012/09/21
- メディア: Kindle版
- 購入: 4人 クリック: 9回
- この商品を含むブログを見る
- 作者: Graham Hutton,山本和彦
- 出版社/メーカー: オーム社
- 発売日: 2009/11/11
- メディア: 単行本(ソフトカバー)
- 購入: 14人 クリック: 503回
- この商品を含むブログ (117件) を見る
- 作者: Simon Marlow,山下伸夫,山本和彦,田中英行
- 出版社/メーカー: オライリージャパン
- 発売日: 2014/08/21
- メディア: 大型本
- この商品を含むブログ (2件) を見る
Elmでやってみるシリーズ11:お絵描きツール
Elmでやってみるシリーズ11:お絵描きツール
今回は、マウスボタンをクリックすると、半径4の赤いドットが描画され、押下したままドラッグすると線が描けるというもの。以下が実行イメージ。
しかし今回は、いきなり完成させるのではなく、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
- 作者: Miran Lipovača,田中英行,村主崇行
- 出版社/メーカー: オーム社
- 発売日: 2012/05/23
- メディア: 単行本(ソフトカバー)
- 購入: 25人 クリック: 580回
- この商品を含むブログ (67件) を見る
- 作者: Miran Lipovaca
- 出版社/メーカー: オーム社
- 発売日: 2012/09/21
- メディア: Kindle版
- 購入: 4人 クリック: 9回
- この商品を含むブログを見る
- 作者: Graham Hutton,山本和彦
- 出版社/メーカー: オーム社
- 発売日: 2009/11/11
- メディア: 単行本(ソフトカバー)
- 購入: 14人 クリック: 503回
- この商品を含むブログ (117件) を見る
*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属性つけられば良いのだが。iPhoneのSafariでも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では本質的に連続的に変化する値を扱うことはない。
関連エントリ
「Elmでやってみる」シリーズのまとめエントリ - uehaj's blog
- 作者: Miran Lipovača,田中英行,村主崇行
- 出版社/メーカー: オーム社
- 発売日: 2012/05/23
- メディア: 単行本(ソフトカバー)
- 購入: 25人 クリック: 580回
- この商品を含むブログ (67件) を見る
- 作者: Miran Lipovaca
- 出版社/メーカー: オーム社
- 発売日: 2012/09/21
- メディア: Kindle版
- 購入: 4人 クリック: 9回
- この商品を含むブログを見る
- 作者: Graham Hutton,山本和彦
- 出版社/メーカー: オーム社
- 発売日: 2009/11/11
- メディア: 単行本(ソフトカバー)
- 購入: 14人 クリック: 503回
- この商品を含むブログ (117件) を見る