uehaj's blog

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

Reactはリアクティブプログラミングなのか?

Reactとは

Reactは、Facebookが開発した、JSのUIフームワークもしくはライブラリです。Reactが提供する中核機能は以下です。

  • イミュータブルなUIビルダー
    • Virtual DOMによる効率的更新
  • 上記に付随するイベントハンドラ群を編成していくための方法論
    • React単体ではコールバックの組合せで、Fluxの一部として使用するとオブザーバーパターンで実現

効用は、再利用性と保守性・可読性向上です。特に、Reactで作成した画面部品のコンポーザビリティが高く、細粒度のUI部品利用の発展充実が期待されます。作りは通常のJSクラスライブラリであり、覚えたりすることは多くありません。

設計をとりもどす

Reactが解決しようとする問題は、大規模化するSPAにおいて、大域状態をDOM内の値として管理し、無数のイベントハンドラがただあるだけ、といった「設計不在」への対処です。Reactは「UIはこう設計しようぜ」という導きであり、設計を大事だと思う一部の人には大受けします。やんややんや。

Reactでやってみる

さて、以前、以下の記事では、関数型リアクティブプログラミング(FRP)を実現するAlt JSであるElm言語を用いて、マウスストーカーというマウスを追い掛ける★を表示するプログラムを実装しました。

uehaj.hatenablog.com

上記の元になった記事は、Bacon.jsで実装した記事なのですが、ElmもBaconJSもリアクティブプログラミングを実現する技術です。ElmのHtmlライブラリ「Elm-html」はReactと同様にVirtual DOMをレンダリング効率化技術として採用しているので、Reactだとどうなるかなと思ってやってみました。

以下がReactでのマウスストーカーの実装です。Resultsで実行できます。

画面キャプチャは以下です。

gyazo.com

コードは以下になります。

'use strict';
var React = require('react');
var ReactDOM = require('react-dom');

var Star = React.createClass({
    getInitialState: function() {
        return {x:30, y:30}
    },
    setPosition: function(x, y, refs) {
        this.setState({x:x, y:y})
        if (this.props.followedBy != null) {
            setTimeout(()=>{
                refs[this.props.followedBy].setPosition(x, y, refs)
            }, 100);
        }
    },
    render: function() {
        return (
            <div style={{position:'absolute', left:this.state.x, top:this.state.y, color:'orange'}}>
                ★
            </div>
      );
  }
});

var Screen = React.createClass({
    onMouseMove: function(ev) {
        this.refs._1.setPosition(ev.clientX, ev.clientY, this.refs)
    },
    componentDidMount: function() {
        document.addEventListener('mousemove', this.onMouseMove);
    },
    componentWillUnmount: function() {
        document.removeEventListener('mousemove', this.onMouseMove)
    },
    render: function() {
        return <div>
            <Star ref="_1" followedBy="_2"></Star>
            <Star ref="_2" followedBy="_3"></Star>
            <Star ref="_3" followedBy="_4"></Star>
            <Star ref="_4" followedBy="_5"></Star>
            <Star ref="_5" followedBy="_6"></Star>
            <Star ref="_6" followedBy="_7"></Star>
            <Star ref="_7" followedBy="_8"></Star>
            <Star ref="_8"></Star>
        </div>
    }
});

ReactDOM.render(<Screen />, document.getElementById('container'));

Elm-HtmlとReactの対応

Elm版では、DOMベースではなくてElmのCanvasベースのAPIを呼んでいたので、本当の比較は実はできません。なので「Elm-Htmlで作っていたら」という想定をした上での比較になりますが、以下のとおり。

Elm-Html React
Signalに包まれた値 Reactコンポーネントに包まれたstate
SIgnal Htmlを返す関数 stateを持ったReactコンポーネントのrender()
Signalには包まれないHtml値を返す関数 stateを持たないReactコンポーネントのrender()。実際、stateを持たないreact componentは、React 0.14ではクラスではなく関数として書ける。
純粋関数でのビュー構築 コンポーネントAPIのrenderメソッド
mainでの、純粋関数をSignal.map*してシグナル値を当ててSignal Htmlを得る操作 ReactDOM.renderComponent()
on*で設定したイベントハンドラからport経由でSignalをキック、そしてそのSignalに結びついたfoldpが起動されるまで コンポーネントでの各種イベントハンドラを実行してthis.setState()
Elm Archtecture ルートコンポーネントのみにstateを置くという方針
- flux
Singalの合成や加工操作(Signal.sampleOn, Time.delay,..) なし

このように対応付けることができます。一見すると、両者はとても似ているとも思えます。しかし、その類似性の多くは、両者ともVirtual DOMを採用し、さらにやっている処理が同じであることに由来しており、必然です。最終的にはJS上で、同じようにDOMイベントを処理して、同じDOMを描画する以上、対応付かないはずがないのです。

なので差異の方が自分としては興味深いところです。たとえば、Elmでは純粋関数しかないので「直接Signalを発行できない(setStateできない)」という制限が大きく、Reactではハンドラ内のsetState()で済むところをport経由でTaskを起動したりしないとなりません(MessageやAddressを経由した暗黙にせよ)。

また、ElmではTaskの定義場所と、そのタスクをキックするハンドラなどがポートを介してばらばらになりますが*1、両者を一つのクラスとして書いて対応付けるReactの方が私にはわかりやすく感じました。

ReactはFRPか?

まず、FRP(関数型リアクティブプログラミング)の定義ですが、こちらを参考にすると、「動的であり変化する値(すなわち、“時間とともに変化する(Time Varring Value)”値)をファーストクラスの値として、それらを定義し、組み合わせ、そして関数の入力・出力に渡すことができる」という感じでしょうか。

とすると、ReactはFRPと言えません。なぜなら時間に伴なって変更される値(Time Varrying Value, Elmで言うSignal)に対する抽象操作ライブラリが整備されていないし、一次イベントを組み合わせて、新たなSignal(二次イベント)を構築する方法も提供されておらず、イディオムとしても確立されていないからです。

たとえば前述のマウスストーカーで、Elm版では★が前の★の位置においつこうとする動作を、Time.delayを用いて以下のように記述できました。

          trace = Time.delay 100 -- 100ms遅延を与えたSignalを生成
          p1 = Signal.sampleOn AnimationFrame.frame Mouse.position -- 最初はマウス座標を追う
          p2 = trace p1 -- 以降、一個前の座標を追うようにする

上ではtraceという一時関数を定義していますが、それを展開すると

          p1 = Signal.sampleOn AnimationFrame.frame Mouse.position -- 最初はマウス座標を追う
          p2 = Time.delay 100 p1 -- 以降、一個前の座標を100ms遅延を与えて追う

になります。Signal(ElmにおけるファーストクラスのTime Varrying Value)であるマウス位置(Mouse.position)と、同様にSIgnalであるAnimation Frameのサンプリング(AnimationFrame.frame)を、合成操作「Signal.sampleOn」で組合せたり、「引数のSignalを500ms遅延して発火する操作Time.delay」で、Time varring valueを操作したものをTime varring valueとして生成しています。

かたや、Reactで同じことをするのは、

    setPosition: function(x, y, refs) {
        this.setState({x:x, y:y})
        if (this.props.followedBy != null) {
            setTimeout(()=>{
                refs[this.props.followedBy].setPosition(x, y, refs)
            }, 100);
        }

の部分で、要は、位置の変更時(setState())に、setTimeoutで自分を追跡する★の位置を指定時間遅延させて発火させています。JSでの泥臭いイベントハンドラの定義と呼び出しの処理です。

最終的には、Elmは上記と同じ結果を生じさせるJSにコンパイルされます。FRPに魔法はありません。イベントハンドラとコールバックの組合せで実現しなければならなかったことを、抽象操作として利用できるので便利! 偉い! 見易い! わかりやすい!ということです。その抽象層がなければ定義上はFRPではない、と言えると思います。

とはいえ、一次イベントについては、renderから見たstateは透過的にアクセスでき、限定的にはFRPを実現しているとも言えます。先の例でいうmousemoveイベントで取得できるマウス位置は、刻々とかわるTime Varring Valueですが、renderの中では通常の値のようにあつかえているでしょう。二次イベントは、手作業でがんばってstateに登録することになります。

さらに、Reactを含むフレームワーク(もしくはそのアーキテクチャ)であるFluxは、Time Varring Valueの合成や、二次イベントのイディオム的にうまく実装できるのかもしれません。たとえば、あるStoreで、2つのイベントをまちあわせて発火する、みたいな感じ? Fluxはまだよく調べてないので、要調査です。またReactは意図的にUIライブラリに特化しているので、組合せて使えるFRPライブラリがあるのかもしれません。

まとめると、Reactは単体では定義上のFPRではありません。しかしFRPが提供する利点を一次イベントに関しては享受できます。

ReactはFPか?

私はYesだと思います。Reactの思想と実装はFP(関数型プログラミング(スタイル))に基いています。

Reactのコンポーネントのrenderは以下のような純粋関数です。

| 入力 | 出力 | |:-|:-| | this.props, this.state| React DOM, this.state |

おいおいまってくださいよ、propsはともかく、stateは変更するんだから純粋ではない、と思うかもしれません。しかし、setStateは、それは実際の変更ではなく、新しい値の設定なので、それを出力として返していると見ることができるのです。 stateを持つにせよもたないにせよ、renderは純粋関数として以下のように考えることができます。 |引数| |返り値 | |:-|:-|:-| | (props, state) | -> | (React DOM, state') | ここでの、stateは、Reactコンポーネント1つのstateではなく、ReactDOM.render()全体で構築しようとするReact DOMツリー全体のstateを統合したものと考えてください(setStateがマージしていると考える)。

すると、おお、このstateとstate'を見ると、まさしくアレではないか。Stateモナド*2としてstateをもちまわり、renderとrenderが数珠繋ぎになって、貼り合さった全体のReact DOMが返却されるようにすれば、関数型としか言いようがありません。

つまり、Reactコンポーネントは、render以外を無視すれば、モナディックに合成され得る関数呼び出しを表現しています。render以外のメソッドは、これはただの関数であって、コンポーネントのクラスは関数置き場でしかありません。DOMのイベントハンドラに登録したり、他のコンポーネントのイベントをコールバックするのに用います。

(2015/11/08)上記はまちがっている。renderが純粋、いうのは正しいが、stateを変更するのはイベントハンドラの流れなのでrenderではもともとstateを更新も新規作成もしない。

FPとOOPの真の関係

そうだとしても、先のFRPの論法にしたがえば、FPとしての見た目、抽象があってこそのFPなのではないか? オブジェクト指向の部品を使って実現したものがFPと言えるのか、と思うかもしれません。

でも、FPというのは「値を変更しない」という制約にすぎません。「何かをしない」という制約は、FPを想定していない言語でも、BASICでも実現可能です。たとえば変数に再代入しないように注意したり、破壊的操作のないライブラリを使用すれば良いでしょう。だから、OOPでFPが実現できて何の不思議もない。

そしてそれは、varを使わずconstを使えといったミクロなレベルの話だけでなく、OOPの部品(クラス、オブジェクトインスタンス)を注意深く選択配置すれば、構造としてFPの思想をOOP上で実現でき、場合によってはFP言語による実現よりもわかりやすくなる場合すらあるのだ、と今では考えています。

なお、言わずもがなですが、Reactの利用者は、FPがどうのとか認識する必要はありません。単に使って、便利にあらわれてきている特徴を享受すれば良いです。

まとめ

  • Reactは(単体では少なくとも)FRPではない。二次イベントのTime Varring Valueとしての構成や、Time Varring Valueの抽象操作を一般にサポートしていないため。ただし、一次イベントについては、renderから透過的にアクセスでき、FRPが提供する利点を得ることができる。
  • ReactはFPである
  • Reactはいいね

追記

こちらの記事によれば、

React が Reactive プログラミングを採用しなかった理由
React.js の開発者で Facebook につとめる Sebastian 氏は Staltz 氏の開発する Cycle.js を賞賛しながらも、Reactive プログラミングのスタイルを採用しなかった理由として、大多数の人が学ぶには大変であることを述べています (Hacker News)。

とのことです。

*1:モジュール・パッケージとしてまとめられるか?

*2:Stateモナドとしてみると、順序性の担保は重要ではない。ReactにとってはStateを切り離すことによって、ホットリロード、ヒストリ、タイムトラベリングデバッグ、リロード耐性などが実現されることが重要。

Elmでやってみるシリーズ16: マウスストーカーを実装する

リアクティブプログラミングの技術を用いてマウスストーカーを実装する - はこべブログ ♨」という記事があり、興味深いのでElmのリアクティブプログラミングで似たようなことをやってみました。

全画面表示はこちらから。

コードは以下で、プロジェクト全体はこちらにあります。

import Text
import Window
import Time
import Mouse
import List
import Signal
import Graphics.Element(..)
import Graphics.Collage(..)
import Color(..)
import Signal(Signal,(<~),(~))
import AnimationFrame

-- マウスの座標をCollageの座標に変換するいつもの関数
mousePos : Int -> Int -> Int -> Int -> (Float, Float)
mousePos x y w h = (toFloat x-(toFloat w/2)
                   , -(toFloat y)+(toFloat h/2))

-- ★の表示
star : Int-> Int -> (Int, Int) -> Form
star w h (x, y) = Text.fromString "★"
                        |> Text.color orange
                        |> Text.centered
                        |> toForm
                        |> move (mousePos x y w h)

-- ビューの定義
view : List (Int, Int) -> (Int, Int) -> Element
view posList (w,h) = collage w h (List.map (star w h) posList)

-- ★の座標のリストのSignalを作る
stars : Signal(List (Int, Int))
stars = let 
          trace = Time.delay 100 -- 100ms遅延を与えたSignalを生成
          p1 = Signal.sampleOn AnimationFrame.frame Mouse.position -- 最初はマウス座標を追う
          p2 = trace p1 -- 以降、一個前の座標を追うようにする
          p3 = trace p2
          p4 = trace p3
          p5 = trace p4
          p6 = trace p5
          p7 = trace p6
          p8 = trace p7
          p9 = trace p8
          p10 = trace p9
          p11 = trace p10
          p12 = trace p11
          p13 = trace p12
          p14 = trace p13
        in (\a b c d e f g h i j k l m n -> [a, b, c, d, e, f, g, h, i, j, k, l, m, n])
         <~ p1 ~ p2 ~ p3 ~ p4 ~ p5 ~ p6 ~ p7 ~ p8 ~ p9 ~ p10 ~ p11 ~ p12 ~ p13 ~ p14

-- 表示関数viewをliftして★の座標をあてがう
main : Signal Element
main = view <~ stars ~ Window.dimensions

非常に簡潔です。

説明と注意点など

  • Mouse.positionおよびTime.delayが、シグナル(beacon.jsでいうEventStreamに相当)を生成しています。ちなみに次期Elm 0.15では、Signalは名称変更(Stream/Varyingに分割)が検討されているようです
  • 上記コードではマウス位置のサンプリング間隔を効率化するために AnimationFrameというコミュニティパッケージを使用しています。なので他のパッケージを使用する機能がないtry-elmshare-elmでは実行できません。sampleOnをfps 60とかですれば、もしくはsampleOnを使用せず直接Mouse.positionを使用すれば、AnimationFrameへの依存は除去できます。
  • 制限としては、ElmのCanvasベースのAPIであるGraphics.Collageを用いているため、★がifameの枠外まで追随することはありません。Nativeやportを用いてJSと連携すれば、真のマウスストーカーができるかもしれませんが試してはおりません。ちなみに次期版0.15ではJSとの連携能力が大きく変わるようです。
  • 上記コードでp1〜p14を列挙していますが、例えばmapを使用してリストを生成していないのは、Signalは動的に生成できないからです。Elmでは、すべてのSignalの値の依存関係に対応する「Signalグラフ」というものがコンパイル時に静的に決定されます。そのこともあり、不定件数の「Signalのリスト(List Signal a)」というものは作成できたとしても、(Signal (List a))に変換できないのです。sequence :: [m a] -> m [a] が欲しいところですが、そういう関数はなく、定義することも型制約*1で意図的に禁止されています。

余談

もうすぐ出るらしいElm 0.15は、非常に楽しみです。

参考リンク

Elmシリーズの他のエントリはこちら

*1:Signalはアプリカティブでモナドではない。

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設計は慎重にやれ、ということか。

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