Bacon.jsで眺めるFunctional reactive programming
JavaScriptのFunctional reactive programming(FRP)ライブラリ「Baconjs」を使ってFRPの考え方を勉強してみたメモ。
なお、このメモはFRPの勉強のためにいくつかの資料をざっくりまとめただけなので、Bacon.jsの使い方は解説しない。
なのでBacon.jsの使い方を知りたいだけなら元記事を読んだほうが良い。
三行で
- FRPでは従来のプログラミングでは間接的にしか扱えなかった「徐々に変化する値」を第一級のオブジェクトとして扱うことができる。
- さらに関数型のcompositionalな性質が合わさって素敵。
- Bacon.jsは良いものだ。
従来のプログラミングでは時間とともに変化する値の扱いが難しい
例えば次のフォームの例を考えてみる。
<form> <input type="text" id="username" name="username" placeholder="ユーザー名"/> <input type="text" id="fullname" name="fullname" placehodler="フルネーム"/> <button id="registerButton" disabled="disabled">登録</button> </form>
このフォームに登録ボタンを押すとユーザー名とフルネームを送信する処理を実装してみる。
registerButton.click(function(event) { event.preventDefault(); var data = { username: usernameField.val(), fullname: fullnameField.val()}; $.ajax({ type: "post", url: "/register", data: JSON.stringify(data) }); })
楽勝だ。
しかしこれではちょっと実用に耐えない。もう少し機能を追加してみる。
- ユーザー名が空の場合は送信ボタンを無効化する
- フルネームが空の場合は送信ボタンを無効化する
- ユーザー名が一文字入力されるごとに重複した名前があるかどうかをチェックする
- ユーザー名が重複していれば送信ボタンを無効化する
↓実装後
var usernameAvailable, checkingAvailability; var usernameField = $('#username'); var fullnameField = $('#fullname'); var registerButton = $('#registerButton'); usernameField.keyup(function(){ $.ajax({ url : "/usernameavailable/" + username}).done(function(available) { usernameAvailable = available; updateButtonState(); }); }); registerButton.click(function(){ updateButtonState(); var data = {username: usernameField.val()}; $.ajax({ type: "post", url: "/register", data: JSON.stringify(data) }); }); var updateButtonState = function(){ setEnabled(registerButton, usernameavailable && nonEmpty(usernameField.val()) && nonEmpty(fullnameField.val()) && !checkingAvailability); };
たった2つの入力項目をもつフォームのはずが、やや複雑になってしまった。
この調子でさらに
- ajax通信中はインジケーターを表示する
- 登録処理中はボタンを無効化する
といった処理を追加していくと、誰も保守できないコードになっていく。
このコードを改善するにはどうすればいいのか。
たしかにMVCやオブジェクト指向を使ってコードをある程度改善することはできる。しかし、複雑さの根本には時間の経過(例えばユーザー名が入力によって少しずつ変化していくこと)が第一級のオブジェクトとして扱えないことにある。オブジェクト指向では状態とその変更という手段では時間経過を間接的にしか表現できないことが複雑さの原因だ。
FRPは「時間の経過を表現するデータ型」を提供する
FRPは徐々に変化する値(上の例ではユーザーの入力によって変化していくテキストフィールドの値)を第一級のオブジェクトとして扱うことができる。
さきほどの例をFRPライブラリのBacon.jsを使って書き換えていく。
まず、「入力によって変化していくユーザー名」を表現すると次のようになる。
var username = $('#username').asEventStream("keyup").map(function(event) { return $(event.target).val(); }).toProperty("");
このような値をFRPでは「振る舞い」と呼ぶ。
例えばユーザー名が変化するごとに内容をconsoleに出力するコードはこんな感じだ。
username.onValue(function(value){console.log(value);});
これを実行するとこうなる。
h he hel hell hello hellow hellowo hellowor helloworl helloworld
この程度なら従来の方法を使って同じことができると考えるかもしれない。
$('#username').keyup(function(e){ console.log($(this).val()); }
FRPの利点は振る舞いを自由に加工したり、貼りあわせたりして新しい振る舞いを定義できることにある。
例えばユーザー名が空ではないことを表す振る舞いは以下のコードで表現できる。
var nonEmpty = function(x){ return x.length > 0;}; var usernameEntered = username.map(nonEmpty);
イベントを柔軟に扱えるようになった。
- ユーザー名が空の場合は送信ボタンを無効化する
- フルネームが空の場合は送信ボタンを無効化する
を実装すると
//usernameEnteredとほとんど同じ fullnameEntered = fullname.map(nonEmpty); buttonEnabled = usernameEntered.and(fullnameEntered); //小さな振る舞いを組み立てていくことで副作用の部分を一箇所にまとめることができる buttonEnabled.onValue(function(enabled){ $("registerButton").attr("disabled", !enabled); });
複雑な条件を簡潔に表現できて良い
疲れてきたので適当になるけど、FRPではajaxも同じ枠組みで扱うことができる。
var availabilityResponse = username.changes().map(function(user) { return { url: "/usernameavailable/" + user };}).ajax(); var usernameAvailable = availabilityResponse.toProperty(true); //updatebuttonstateと同等の処理が次のようにかける! var buttonEnabled = usernameEntered.and(fullnameEnterd).and(usernameAvailable);
ミュータブルな状態がなく小さなものから組み立てているので最初の実装例に比べて簡潔で拡張性が高い。
Bacon.jsすごく良い
Bacon.jsはjQueryプラグインとしても提供されているので既存のプロジェクトに混ぜるのも楽そう。
TypeScriptと組み合わせるともっと幸せになれそう。
FRPのテストについてとか、もっと大規模なアプリではどう構造化するべきなのかについても調べて後で書きたい
ウェブサービスのログ設計についてメモ
ログの設計について、ググっても断片的にしか見つからなかったのでメモ。
ログの目的
- 不具合の原因を特定するため
- 不正利用の検出
- 利用統計の取得
ログレベル
- fatal
サービス全体の提供が不可能な状態になる状態。DBに接続できないとか、syntaxエラーがある場合など。このレベルのログが出るとインフラ担当の人達にメールが飛んだりする。
- error
全体が止まるほどではないが、ユーザーの一部の処理が完了しないような場合が該当する。エラーページが表示されるような場合はこのレベル。
- warn
言語自体のwarningや処理フロー上来ることがない値がかえって来た場合などはこのレベル。(例えばユーザーが不正にAPIを叩いてブロックされた時とか) 状況によってはerror相当なので場合によりけり。
- info
このレベルから下は開発者の胃を傷めないログレベル。アクセスログなど。hadoopでコネコネしたりして企画担当が利用したりする。
- debug
これ以下は開発時に欲しい情報。本番環境では出力しない。
- trace
println("ここまで処理が来た")
何をログとして残すか
例えばユーザーの行動についてなら
- [いつ]アクセス日時
- [誰が]ユーザ情報(ID)/ホスト/ポート/ユーザーエージェント
- [何をした] ログファイルを切り替えてファイル名で表現する(例)login_failure.log
異なるファイルにログを出力するのは重要。特定のログに素早くアクセスでき、かつログの種類によって保存期間や閲覧権限を変えたりできる。機能とユーザー数が増えるほどその恩恵にあずかれる。
ログをどれだけの期間保存するか。
ログの重要度と書き込み頻度によって決める。
(容量肥大対策の一例)
ログを一日ごとのログローテーションで記録し、30日間保存する場合、前日のログはすぐアクセスできるようにファイル圧縮して同じディレクトリにおき、それ以前はバックアップサーバーに送って30日前まで保存するようにするなど。
ログのドキュメントに何を書くか。
ドキュメントを整備・保守しつづけないと、「どこで使用されているかわからないので消せないログ出力」が生まれてコードのエントロピーが増大する。
ログに何を書いてはいけないか
メールアドレスなどの特に重要な個人情報をログに残さないようにする。
ログに書くこと
- 作成者
- 出力条件
(例)認証キー発行APIを実行した時
- 利用目的
(例)不正利用がないか記録を残します
- フォーマット
- ログの例
ログ設計について何かよい資料があったら誰か教えてください。
play-slickを試した
play framework + Java + ebeanで色々やったので、今度はscalaでDB操作をしたかった。
調べるとSquerylとslickとで人気を二分しているらしい。今回はscalaの勉強であることとtypesafe stackに入っていることも考えてslickを試してみた。
play frameworkとslickを使うサンプルがすでにgithub上にあるが、どうもうまく統合できていないように感じたので調べたところplay-slickというプラグインがあるらしいので動かすところまでメモ。
環境
- play framework 2.1.1
- slick 2.10.0-RC5
- play-slick 0.3.2
howto
いつもどおりplay new
play new mySlickSample
project/Build.scalaに追加
val appDependencies = Seq( //...他の依存ライブラリ "com.typesafe" % "slick_2.10.0-RC5" % "0.11.2", "com.typesafe.play" %% "play-slick" % "0.3.2" )
application.confにDBの設定
db.default.driver=org.h2.Driver
db.default.url="jdbc:h2:mem:play"
play-slickを有効にする設定をapplication.confに追加。
このへんはebeanと同じですね。
# slick # ~~~~~ # It is possible to specify individual objects like: # slick.default="models.Users,models.Settings" slick.default="models.*"
modelを作成
package models import play.api.db.slick.Config.driver.simple._ case class User( id: Option[Long],//privary keyの場合はOption name: String) object Users extends Table[User]("USERS") { def id = column[Long]("ID", O.PrimaryKey, O AutoInc) // This is the primary key column def name = column[String]("COF_NAME") def * = id.? ~ name <> (User.apply _, User.unapply _) //とりあえずfindAllだけ def findAll(filter: String = "%") = { //scalaのコレクションっぽく扱えるのが素敵 for { u <- Users if (u.name like ("%" + filter)) } yield u } }
controllerも
package controllers import models.Users import play.api._ import play.api.Play.current import play.api.db.slick.Config.driver.simple._ import play.api.mvc._ object Application extends Controller { def index = Action { //controllerでwithSessionを使うのが気持ち悪い気がするのだが、これでいいのだろうか? play.api.db.slick.DB.withSession { implicit session => val users = Users.findAll().list Ok(views.html.index(users)) } } }
view
@(users: List[User]) @main("slick test") { <ul> @for(u <- users){ <li>@u.toString()</li> } </ul> }
あとappのdefault packageにGlobal.scalaとか作っておいて、
import play.api.Application import play.api.GlobalSettings import play.api.Play.current import play.api.db.slick.Config.driver.simple._ import models.Users import models.User object Global extends GlobalSettings { override def onStart(app: Application) { play.api.db.slick.DB withSession {implicit session => Users.insertAll( User(None, "jhon"), User(None, "bob")) } } }
とやればテスト用データを入れておける。
localhost:9000にアクセスすれば表示できる。
次はtodoアプリを作ろう。
参考
Slick ガイド - tototoshiの日記
Slick
追記(2013/6/25)
今後play frameworkではAnormからslickに移行していくようだ。
play-slickがplay framework本体にプルリクされているようで
https://github.com/playframework/Play20/pull/1230
wktkが止まらないですね。
MS932で指定した方がよかったメモ
charset=Shift_JIS
なのに
Source.fromURL(vipSubjectsUrl, "Shift_JIS")
で「たまに」失敗することがあって困った。他言語でよく指定する"CP932"がないので30分くらい詰まった。 正解として "CP932"ではなく"MS932"を指定するらしい。
参考: http://blue-red.ddo.jp/~ao/wiki/wiki.cgi?page=Java+%A4%CE+MS932%2C+Cp943C%2C+SJIS+%A4%CE%B0%E3%A4%A4
play framework + Java + Ebean + eclipseでJUnitテストが起動に失敗するときの対処法
現象: play.db.ebean.Modelを継承したModelをテストする際に以下のメッセージがコンソールに出てエラーになる。
環境:
- play framework 2.1.1
- eclipse juno(4.2 RC2)
対処法: VM引数にjavaagentを設定する.
- ebeanを公式(http://www.avaje.org/download.html)から適当な場所にダウンロード&&解凍
- テストのVM引数に-javaagent:/path/to/ebean-2.7.0-agent.jarを設定
3.実行すれば問題が解消されるはず。
参考: http://blog.matthieuguillermin.fr/2012/03/unit-testing-tricks-for-play-2-0-and-ebean/
残る疑問