signalは使うときに工夫しないと使い物にならない
我ながら意味不明なタイトルだが気にしない。もう少しきちんとかくと、「FRPで言うところのsignalとかbehaviorとかは、そのままでは使いにくい。ちょっと工夫しないとすぐに困ったことになる」という事。
例えば単純化して次のようなSignalモジュールがあったとする。
module type Signal = sig type 'a t val make : 'a -> 'a t * ('a -> unit) val map : ('a -> 'b) -> 'a t -> 'b t val read : 'a t -> 'a end
これを使って親のGUIコンポーネントから子供のコンポーネントの大きさを定義するとする。
let child1 = { (* 子供コンポーネントのサイズを親の半分と定義 *) size = Signal.map (fun (x,y) -> x/2, y/2) parent.size; }
signalは依存関係を自動的に更新してくれるから、子供のsizeをいつreadしても親のsizeの半分になっている訳。signalはこういう定義に便利。
加えて、先ほどの子供コンポーネントの大きさに依存して、さらに別の子供コンポーネントの大きさを定義しよう。
let child2 = { (* 別の子供コンポーネントのサイズ *) size = Signal.map (fun (x,y) -> x, y/2) child1.size; }
これでchild1の大きさに依存してchild2の大きさも決まる。何の問題もない。
ところが、上記定義の後、やっぱりchild1のサイズを親のコンポーネントの大きさに依存させるのではなく、window全体のサイズに依存させるよう変更したいとなったら、どうすればいいか?
child1.sizeは既にparent.size依存のsignalである。もうこれはどうしようもない。変えられない。
じゃぁ、sizeレコードをmutableにしておいて破壊的代入したらどうか。
(* 子供コンポーネントのサイズをウィンドウの大きさに依存させる *) child1.size <- Signal.map (fun (x,y) -> x/2, y/2) window.size;
これはアウト。なぜならchild2.sizeは相変わらず前のchild1.sizeに依存したままであり嬉しくない。解決策はない。
ただし、Signalモジュールにjoin : 'a signal.t signal.t -> 'a signal.tがあれば解決する。子供コンポーネントのsizeを定義するときに、予めjoinを挟んでおくのだ。
let ss, sender = Signal.make () in let child1 = { size = Signal.join ss; } in sender (Signal.map (fun (x, y) -> x/2, y/2) parent.size);
上記のように定義しておくと、
sender (Signal.map (fun (x, y) -> x/2, y/2) window.size);
とすれば、child1.sizeはウィンドウの大きさに依存し直され、child1とchild2の依存関係も保たれる。
こう考えると、joinもしくはbindのないsignalは実用上使えない代物である。あと、全てのsignalには予めjoinを挟んでおけばいいような気がする。
run-camlしなイカ?
このエントリーはhttp://partake.in/events/5784afd8-d43b-4cbe-9256-430d5ababa2bへの参加記事でゲソ!一年前経つのは早いじゃなイカ。
run-camlを知っているでゲソ?run-camlはOCamlに標準装備されているcaml-modeの機能の一つで、emacs上でtoplevelを動かしてくれる優れモノでゲソ。今日はこのrun-camlで侵略を加速する方法を紹介しようじゃなイカ!
まずOCamlとcaml.elをインストールするでゲソ。Debian系なら、
で入るでゲソ。準備ができたらemacsで最低限の設定を記述して評価しておくでゲソ。
(if window-system (require 'caml-font))
(setq auto-mode-alist
(cons '("\\.ml[iylp]?$" . caml-mode) auto-mode-alist))
(autoload 'caml-mode "caml" "Major mode for editing Caml code." t)
(autoload 'run-caml "inf-caml" "Run an inferior Caml process." t)
(setq inferior-caml-program "ocaml -I +threads -I +camlp4")
簡単じゃなイカ?
じゃぁ例えば、次のようなHello Worldプログラムをhello.mlとして書いていたとするでゲソ。
let hello person = print_string ("hello " ^ person)
この時に、M-x run-camlすると下段バッファにtoplevelが開くでゲソ。
この状態でプログラムを範囲選択し、Ctrl-c Ctrl-rすると選択された範囲がtoplevel上で評価されるでゲソ。
toplevelを生で使っていると式の度に;;が無いとうまく評価されない問題が発生するけど、run-caml経由なら複数の式を一気に評価してもよろしくやってくれるでゲソ。hello関数を使ってみようじゃなイカ。
ちゃんと使えるでゲソ。#の直後にカーソルを置けばCtl-↑でヒストリーも出てくるでゲソ。これでちょっとしたプログラムならサクサク試しながら作れるじゃなイカ?
でも、これだけじゃぁ実際には足りないでゲソ。実際の開発では色々なライブラリや自前関数を使いながらプログラムするので、単純に実行したい行を評価しても未定義の関数があると言われてしまうでゲソ。
そういう時のために、toplevelには#loadディレクティブが用意されているでゲソ。toplevel上で、#load "使いたいライブラリ.cma";;とすると、所望のライブラリをtoplevel環境にロードできるでゲソ。camlimagesライブラリをロードしておけば、プログラムを動的に評価しながらグラフィックを描画する事も可能でゲソ。@yoshihiro503のOCamltterだってemacs上で動くでゲソ。pa_monadを使いたい人も#load "pa_moand.cmo"とすると拡張構文が使えるでゲソ。
毎回#loadディレクティブを実行するのが面倒なら、#loadディレクティブを列挙した.ocamlinitファイルをホームディレクトリに作っておけば、toplevelが立ち上がる時に自動的にロードされるでゲソ。ちなみにイカは私が使っている.ocamlinit。
#load "dynlink.cma"
#load "camlp4orf.cma"
#load "unix.cma"
#load "threads.cma"
#load "str.cma"
#load "graphics.cma"
#load "camlimages_core.cma"
#load "camlimages_all.cma"
#load "camlimages_graphics.cma"
さらに豆知識でゲソ。trampというssh経由でファイルやシェルを扱えるelispと一緒に使えば、リモート上の.mlファイルをssh経由で編集しながら、run-camlでリモート上のtoplevelを起動して開発ができるでゲソ。開発機がローカルVMじゃない時には必須のテクニックでゲソ。
さあ、みんなもrun-camlで侵略を開始するでゲソ!
第一級モジュールで単体テスト可能なコードを保つ
コードを単体テスト可能なよう保つためには色々なやり方がある訳ですが、一番いいのは副作用を分離して局所に押し込めておく手法でしょう。でも、処々の事情によりそんなに綺麗に分離する事もできないので、次善の策としては副作用部分をパラメーター化して別のモックに置き換え可能にしておくというやつがある訳です。そしてOCamlではそのパラメーター化の方法にも幾つかやり方があって、次の4通りの方法が考えられます。
- 副作用を行う部分を関数化して引数として渡す。
- 副作用を伴う部分が複数ある時はレコードにして渡す。
- レコードの代わりにファンクター経由で渡す。
- ファンクターの代わりに第一級モジュールで渡す。
例えばこういうコードを単体テスト可能にしたいとします。(単純化のため省略した関係で殆どなんのロジックもない関数になっていますが、実際にはもう少し条件分岐が入ったコードだと思ってください。)
(* 単体テスト可能にしたい関数。このままでは副作用があるのでできない。 *) let take_candles paramter = with_db (fun db -> (* DBハンドラを受け取るローンパターン *) let hist_candles = load_candles paramter (* データベースから情報をロード *) in match hist_candles with [] -> load_realtime_candle db paramter (* 条件によっては別の情報をロード *) | _ -> hist_candles)
もちろん、この関数はデータベースがないと動きません。このままでは回帰単体テストできません。まず1.の方法を採用するとすると、widh_db, load_candles, load_realtime_candleの3個の関数をtake_candles関数の引数に渡す事になります。3個も引数増えるのかよ!面倒ですね。レコードにしましょう。すると2.の方法になる訳ですが、実際のDBアクセスをテスト用のモックに切り替えようとするとちょっと困った問題が出ます。
type db_access = { (* レコード化したDBアクセス *) with_db : 'a. (db_connection -> 'a) -> 'a; load_candles : db_connection -> parameter -> candle list; load_realtime_candle : db_connection -> parameter -> candle list; }
db_connection型はPostgreSQLなどDBへのハンドラを想定しています。これが邪魔で、単体テスト用のモックを作りたいのに本物のDBハンドラを用意しないければインスタンス化できないという事態になります。困った困った。じゃぁ、db_connectionをレコードの中に隠すとどうでしょう。
type db_access = { (* DBへのハンドラを隠した版 *) load_candles : parameter -> candle list; load_realtime_candle : parameter -> candle list; close : unit -> unit }
db_connection型を隠した影響で、ローンパターンのwith_db関数は書けなくなりました。しかもwith_db関数で制御していたハンドラのクローズも出来なくなったので、明示的にハンドラを閉じるclose関数を追加する必要も出てきました。これは正直嬉しくない。そこでファンクターの登場です。ファンクターなら型も一緒に抽象化できるので、db_connection型も一緒に抽象化してしまいましょう。
module type DBAccess = sig type db_connection (* DBアクセスを想定した抽象型 *) val with_db : (db_connection -> 'a) -> 'a val load_candles : db_connection -> parameter -> candle list val load_realtime_candle : db_connection -> parameter -> candle list end module Make( D:DBAccess ) = struct let take_candles paramter = D.with_db (fun db -> (* DBハンドラを受け取るローンパターン *) let hist_candles = D.load_candles paramter (* データベースから情報をロード *) in match hist_candles with [] -> D.load_realtime_candle db paramter (* 条件によっては別の情報をロード *) | _ -> hist_candles) end
これでめでたしな感じがしますが、実用的にはちょっと問題が。実際にはtake_candles関数も単体で存在している訳じゃなくて、他の複数の関数と一緒にどこかのモジュールの一部な訳です。そんな一つの関数の都合のためにモジュール全体がファンクターとなったのでは、take_candles関数を含むモジュールの関数を利用している個所全部でファンクター適用しないといけなくなり、単体テストのためのコード変更が余計な複雑さを生みます。そこで最後に登場するのが第一級モジュールです。第一級モジュールならこう書けます。
module type DBAccess = sig type db_connection (* DBアクセスを想定した抽象型 *) val with_db : (db_connection -> 'a) -> 'a val load_candles : db_connection -> parameter -> candle list val load_realtime_candle : db_connection -> parameter -> candle list end let take_candles loader paramter = let module D = (val loader : DBAccess) (* 第一級モジュールを受け取る! *) in D.with_db (fun db -> (* DBハンドラを受け取るローンパターン *) let hist_candles = D.load_candles paramter (* データベースから情報をロード *) in match hist_candles with [] -> D.load_realtime_candle db paramter (* 条件によっては別の情報をロード *) | _ -> hist_candles) let test1 paramter = (* take_canldes関数をテストする関数 *) let module DBMoc = struct type db_connection = unit (* どうせ使わないのでunit型 *) let with_db f = f () let load_candles = [] (* なにかテストデータを作って入れる *) let load_realtime_candle = [] end in let result = take_candles (module DBMoc:DBAccess) parameter in assert ( result = ... ) (* テスト判定 *)
モックを作る時はdb_connectionを適当な型にできる上に、take_candles関数の引数を一つ増やすだけで対応できています。
まとめです。
- 次善の策として副作用をパラメーター化しよう。
- パラメーター化するには高階関数かレコード。
- 型が要るならファンクターか第一級モジュール。
ちなみに、db_connection型を型パラメーター化する別解もあります。
type 'connection db_access = { (* db_connectionを型パラメーター化 *) with_db : 'a. ('connection -> 'a) -> 'a; load_candles : 'connection -> parameter -> candle list; load_realtime_candle : 'connection -> parameter -> candle list; }
今回はこれでもいいと思います。でも、http://www.math.nagoya-u.ac.jp/~garrigue/papers/ocamlum2010.pdfの「型安全なプラグイン」にあるように、異なる型のdb_accessをリストで扱いたくなると、型パラメーター版では対応できなくなります。加えて、外部に公開する必要のない具体的な型が外に露出してしまうので、private関数をpublic関数にしている的な気持ち悪さが残ります。
第2版 Scalaスケーラブルプログラミング
第2版の「Scalaスケーラブルプログラミング」をid:kmizushima氏より頂戴しました。ありがとうございます。ちょうどScala 2.9でアレコレしようと思っていたところなので、大変助かります。
- 作者: Martin Odersky,Lex Spoon,Bill Venners,羽生田栄一,水島宏太,長尾高弘
- 出版社/メーカー: インプレスジャパン
- 発売日: 2011/09/27
- メディア: 単行本(ソフトカバー)
- 購入: 12人 クリック: 235回
- この商品を含むブログ (46件) を見る
まず、コレクションに関する標準ライブラリの解説は、私のような怠惰なプログラマには結構嬉しい内容でした。APIドキュメントだけでは分からない設計方針と全体像が書かれていて短時間でコレクションの概要を把握するにはもってこいです。あと、派生クラスの詳細やパフォーマンス特性の一覧もあり、どの派生クラスを使えばいいのか迷った時に頼りになりそう。viewやiteratorの動作についても「正直知らなかったよ」という内容が多く、まとめて勉強しておくとお得感が半端ないです。コレクションAPIの内部アーキテクチャについてはマニアックなので知っておいて損はないレベルだと感じますが、既存のコレクションから新しいコレクションクラスを作成する手順は知らないままでいたら無駄骨を折るところでした。確かに安い本ではありませんが、2.8のコレクションについてよく知らないまま開発の仕事をしていたらと想像すると、24,25章だけでも値段分の価値がある本だと思います。
加えて、2.9の新機能紹介も地味に助かりました。parで自動的に並列処理化してくれるのはよく知られていますし知っていたのでまぁいいとして、scala.sys.processパッケージで既存プロセスとの連携が容易になる事に気づけたのは大きかったです。「forkの便利版でしょ?パイプやリダイレクトをScala内部でやって何が嬉しいの?」ぐらいに思っていました。私が間違っていました、ごめんなさい。
あと、29章の「オブジェクトを使ったモジュラープログラミング」の章(第1版では27章)を改めて読み返してみて、実はさらっと重要なことが書いてあるんじゃないかと再認識しました。プログラム部品を入れ替え可能にするという視点は内部設計的には頻出のパターンでありながら、比較的大きい粒度の部品の入れ替えは容易でない場合が多いように感じます。この点について一つの解法を示唆してくれているという点で貴重かもしれません。以前読んだ時にはここまで思い至りませんでした。
第1版を持っている/読んだ人も第2版を機に読み返すと吉な感じです。以上ご参考まで。
ドラッグイベントを作る例
例によってドラッグイベントを作る例。
module E = Pec open Pec.OP let (!%) = Printf.sprintf let (+>) f g = g f let mouse_down, send_down = E.make () let mouse_up, send_up = E.make () let mouse_move, send_move = E.make () let dragging md mu mm = md >>= (fun dloc -> E.choose [ E.map (fun uloc -> `Drop (dloc, uloc)) mu; E.map (fun mloc -> `Drag (dloc, mloc)) mm; ] +> E.take_while_in (function `Drop _ -> false | _ -> true)) let run_all () = while E.run () > 0 do () done let _ = dragging mouse_down mouse_up mouse_move +> E.subscribe (function | `Drag (sloc, eloc) -> print_string (!%"Drag %d,%d\n" sloc eloc) | `Drop (sloc, eloc) -> print_string (!%"Drop %d,%d\n" sloc eloc)); send_down 10; (* ドラッグ開始 *) send_move 11; (* 移動 *) send_move 12; (* 移動 *) send_up 13; (* ドロップ *) send_move 14; (* 無視される *) send_move 15; (* 無視される *) send_down 16; (* ドラッグ開始 *) send_move 17; (* 移動 *) send_move 18; (* 移動 *) send_up 19; (* ドロップ *) run_all ()
a push style event combinator
OCaml用のPush型のイベントコンビネーターです。とりあえずGitHubにアップしたので、記念。
https://osiire@github.com/osiire/Pec.git
.mliはこんな感じ。
type 'a event (** [make ()] makes a new event and sender function.*) val make : unit -> 'a event * ('a -> unit) val map : ('a -> 'b) -> 'a event -> 'b event (** [choose l] is a event which will be raised when one of specified events occurred. *) val choose : 'a event list -> 'a event val never : 'a event (** [join ee] is a event which will be raised when a inner event occurred. "Inner event" is a event taken from outer event [ee].*) val join : 'a event event -> 'a event (** [bind e f] is [join (map f e)] *) val bind : 'a event -> ('a -> 'b event) -> 'b event val scan : ('a -> 'b -> 'a) -> 'a -> 'b event -> 'a event val filter : ('a -> bool) -> 'a event -> 'a event val filter_map : ('a -> 'b option) -> 'a event -> 'b event val zip : 'a event -> 'b event -> ('a * 'b) event val take_while : ('a -> bool) -> 'a event -> 'a event val take_n : int -> 'a event -> 'a event val once : 'a event -> 'a event (** [subscribe f e] attaches the [f] to the specified event. The [f] will be called when the [e] will occurred. *) val subscribe : ('a -> unit) -> 'a event -> unit (** [run ()] runs PEC event system and returns a number of queuing size of sended data. *) val run : unit -> int
実装してみて分かったのですが、純粋にPush型のイベントコンビネーターにはreturnがうまく入らないですね、たぶん。だって、subscribe print_int (return 1)とか何が起きればいいのか、うまく説明できないから。
レイトン教授で始めるAlloy Analyzer入門
8月15日にギークバーで使った資料をアップします。
//No50 enum Cow { A, B, C, D, E } sig Liar in Cow {} { #Liar = 3 } // ソーウ種は3匹 pred whoIsLiar { not A in Liar iff D in Liar not B in Liar iff C in Liar not C in Liar iff not A in Liar not D in Liar iff E in Liar not E in Liar iff B in Liar } run whoIsLiar
// No.039 abstract sig Color {} one sig Red, Blue, White extends Color {} sig Shirt { // シャツ color : disj Color } sig Trousers { // ズボン color : disj Color } sig Person { shirt : disj Shirt, trousers : disj Trousers } one sig A, B, C extends Person {} fact { // 全員上下バラバラの色 all p : Person | not p.shirt.color = p.trousers.color // Cだけは自分の色を着ていない。 not C.shirt.color = White && not C.trousers.color = White Red in A.shirt.color + A.trousers.color Blue in B.shirt.color + B.trousers.color // Cのズボンは赤。 C.trousers.color = Red } pred problem {} run problem
//No.87 abstract sig Number { // 二次元配列。 table : Number -> one Card } one sig N1, N2, N3, N4 extends Number {} enum Card { S, H, C, D } // スペード、ハート、クラブ、ダイヤ fact { // 行と列で4種類並ぶ all row : Number | table[row, Number] = Card all cols : Number | table[Number, cols] = Card // 斜め1 table[N1, N1] + table[N2, N2] + table[N3, N3] + table[N4, N4] = Card // 斜め2 table[N1, N4] + table[N2, N3] + table[N3, N2] + table[N4, N1] = Card // 初期制限 table[N1, N1] = D table[N1, N3] = C table[N3, N1] = H table[N4, N2] = S } pred show() {} run show