FRPライブラリを一日かけて作ってて思いました。これはメインループの復権だ!!
例のごとくまさかりよろしくです!!
--
頭の体操をしましょう!!
FRPライブラリである、Bacon.jsの例題を引用します。
https://baconjs.github.io/tutorials.html
- Username availability checking while the user is still typing the username
- Showing feedback on unavailable username
- Showing an AJAX indicator while this check is being performed
- Disabling the Register button until both username and fullname have been entered
- Disabling the Register button in case the username is unavailable
- Disabling the Register button while the check is being performed
- Disabling the Register button immediately when pressed to prevent double-submit
- Showing an AJAX indicator while registration is being processed
- Showing feedback after registration
雑に日本語に訳します。
- usernameが使えるかどうかリアルタイムでチェック
- usernameが使えないなら通知する
- チェックを行っている最中インジゲータを表示
- 登録ボタンはusernameとfullnameが入力されるまで無効化
- usernameが使えないなら登録ボタンは無効化
- 二重登録を避けるためにクリックされたら即座に無効化
- 登録処理中はインジゲータを表示
- 登録処理がおわったら通知する
んん?余計わかりにくくなった気がしますね……プログラム書きならコードで語るのだ@v@!!
CoffeeScriptっぽい疑似コードで書きましょう。
available = (name)->
showIndicator()
result = checkAvailableAjax(name)
hideIndicator()
result
processRegistration = (user, full)->
showIndicator()
registerAjax(user, full)
hideIndicator()
check = ()->
disableButton()
unless available(@username)
feedbackUnavailableUsername()
else if @username and @fullname
enableButton()
return true
return false
register = ()->
disableRegisterButton()
processRegistration(@username, @fullname)
feedbackRegistraction()
おお、なんか簡単そうですね!!
しかし!!!非同期の魔物が待ち受けています!!これをまともに組んだらうん10倍くらいに膨れ上がるでしょう。(気になった方だけ実装してください……)
しかしです、拙作のライブラリを使えばこのようになります。
available = ($)->
$(showIndicator)()
result = $.async(checkAvailableAjax)($.username).get()
$(hideIndicator)()
result
processRegistration = ($)->
$(showIndicator)()
$.async(registerAjax)($.username, $.fullname).get()
$(hideIndicator)()
form.on.keydown = ($)->
$(disableButton)()
unless available($)
$(feedbackUnavailableUserName)()
else if $.username and $.fullname
$(enableButton)()
return true
return false
form.after.keydown = (value)->
form.checkResult = value
form.on.register = ($)->
unless $.checkResult
return false
$(disableButton)()
processRegistration($)
$(feedbackRegistration)()
return true
なんということでしょう、疑似コードとあまり変わらないではないですか!!
あとは各々の関数に対して、実装していくだけですね。
秘密はFRP
これはBacon.jsと同じく、FRPと呼ばれるパラダイムに基づいています。
FRPの説明の前に、Reactiveであることからです。
Reactive Programming
みなさま、あるいは皆様の近くに方眼紙マスター(!?)がいらっしゃると思いますですのでご存知かとおもいますが、Excelの式は値を変更したらリアルタイムに値が書き変わります。
= E4 + E5
このセルはE4が3、E5が5ならば8になります。E5が10に書き変わったとたん、このセルは13になります。
ですが、プログラミングの世界では、この挙動は不思議です。
a = 1
b = 2
c = a + b
#同一関数のどこか遠くは慣れた場所
a = 3
このとき、cの値は3です。5にはなりませんよね。
ですが、値が書き変わったとき、データの依存関係に応じて再計算を行う、これがReactiveであるということです。
この考え方に基づいたプログラミングとRactive Programmingと呼ぶ事にします。
なんでFunctional?
ここで少し疑問が生まれます。RPはわかった、でもなんでわざわざFRPなんでしょう。
答えは簡単で、RPにおいて、副作用はいろいろ困るからです。
たとえば以下のコードを見てみましょう。
#Reactiveな関数??1
a = 1
b = Math.random()
c = a + b
#Reactiveな関数??2
a = 1
console.log b
b = 2
c = a + b
1を見てみましょう。
ここでaを書き換えたとすると、素朴に再計算してしまったら、cの値は不定です。
2はもっとたちが悪いです。
aを書き換えるたびに、bが変わっていないにも関わらず、bが出力されてしまいますね。
このように、RPにおいて副作用を持つ関数は依存関係を見た上で、特別扱いしなければならないのです。
理想を言えば、外に追い出せばいいですよね、と言う訳で、副作用と作用を厳密に切り分けられる、FPの出番になります。
##リアクティブな二つの値、Behavior、Event
余談ですが重要な概念として、この変更可能な値、二つに分類すると都合が良い事がわかってるらしいです。
それはBahaviorとEventです。
Behaviorは連続的な値です。一番わかりやすいのは時間でしょうか。
Eventは離散的な値です。例えばクリックされた、という状態を、時間とともにまとめた値等です。
#解説の準備は整った!!
というわけで解説です。遠いので一部だけ抜き出しますね。
form.on.register = ($)->
unless $.checkResult
return false
$(disableButton)()
processRegistration($)
$(feedbackRegistration)()
return true
form.on.registerはregisterというイベントを監視する関数の受け皿です。
これが FRPのエントリポイントです 。registerのイベントの値が更新されたら、直ちに実行されます。
そして、ここがポイントになってるのですが、副作用を持つ関数には、$()や、$.async()という、 特別なマークをつけます。
このマークが付いた場合、その関数を実行中にリアクティブな値を参照すると、依存関係があると見なします。この情報を元にすることで、副作用の値の保持、再計算を可能になります。
このマークが付いてない場合、参照透過関数であると見なし、常時再計算を行います。
値の参照に失敗した場合(例えばイベントなのに更新されていなかった場合)例外を投げて大域脱出します。そして値が更新されたら、再度実行されます。
一度実行した副作用を持つ関数は、依存する値が更新されるまで、実際には計算しません(キャッシュを参照します)。よって、次回からは参照透過です。
#あれ、どの辺がFPなの?
白状しますと、JavaScriptの記法で関数を自在に取り回すのは難しいです。
より厳密にするなら、Bacon.jsのようにならざるを得ません。
よって手続き型に見えるように書ける、そのようにしています。これが作った理由になります。
Haskellは相変わらずすごかった
Haskellのアロー記法なるもので書いたら、こんな感じになるかもしれません。(ならないかもしれません)
arr checkResult >>> arr disableButton >>> arr processRegistration >>> arr feedbackRegistration
アロー記法は、関数をつないで大きな関数を作る物ですが、アロー記法の特異点は、そのつなぎ方によって制御構造を成す事ができると言う点です。
具体的には、以下の演算によって、制御構造を成す事が可能になります。
- A >>> B 関数をつなぐ(順次)
- A &&& B 同一入力をA、Bに流し込む(変数)
- A ||| B 右、左がわかる特別な型を受け取って、右ならB、左ならAに流し込む(条件分岐)
- 最後の口を最初にする(繰り返し)
言い換えると、 関数を手続き的につなぐ 手法です。
これはPure FPであるHaskellで実現できるからうまみがあるのでしょうが(命令型言語なら普通に表現できる)、考え方は同じです。
- 副作用を分離する
- 関数をつないで大きな関数を紡ぐ
- 関数の実行制御を行う
命令型の観点から
では、命令型の方から見てみましょうか。
疑似コードをちょっと眺めてもらえますか。
これはint main()の時代だと、わりと自然に書けるものでしたよね?
ですが、そんな素朴な時代は終わってます。猫も杓子もイベントドリブンです。
ところが、ライブラリを使って書いたFRPのコードを見てみましょう。
わざとそうしてるとはいえ、メインループでかけた、すっきりとした流れそのものじゃありませんか!!
これはいうなれば イベントドリブンで分断された、メインループの復権 です。
難しい理論はどうでもいいんです!!これは喜ばしい事ですよ!!
#結論
関数型視点で見ると、FRPは、弱点だった離散時間を、意味を崩す事無く扱えます。
命令型視点で見ると、FRPは、ちょっとの約束事を守るだけで、メインループをこの手に取り戻せます。
FRPすごいのです!!
#で、肝心のブツは……?
https://gist.github.com/necoco/b9752cc5444ebc776c75
まだバグ残ってます(特にキャッシュ周り……)ので遊ぶ程度にしてくださいね。
書いた本人もわかりませんので解説は期待しないでください!!