Bacon.js で skip・take 処理
リアクティブプログラミング用ライブラリの Bacon.js を Node.js 上で使用し、「RxJS で行単位のファイル処理」 で試したような skip・take 処理のサンプルを実装してみました。
ソースは http://github.com/fits/try_samples/tree/master/blog/20150104/
はじめに
npm でインストールするために下記のような package.json を用意します。 今回は CoffeeScript で実装するので coffee-script も追加しています。
package.json
{ ・・・ "dependencies": { "baconjs": "*", "coffee-script": "*" } }
npm install でインストールします。
インストール
> npm install
Bus の利用
まずは Bus
を使って、ストリームへ push
した内容をそのままログ出力する処理を実装してみます。
Bus は後から要素を push できるストリームですが、Bacon.js には他にも色々とストリームを作る方法が用意されています。 (下記は一部)
- 配列からストリームを作る
Bacon.fromArray
- コールバック関数を使ってストリームを作る
Bacon.fromCallback
,Bacon.fromNodeCallback
(Node.js 用のコールバック関数へ適用) - 任意のストリームを作る
Bacon.fromBinder
sample1.coffee
Bacon = require('baconjs').Bacon bus = new Bacon.Bus() bus.log() [1..6].forEach (i) -> bus.push "sample#{i}" bus.end()
実行結果は下記の通りです。 Bus へ push した sample1 ~ sample6 を順次出力しています。
実行結果
> coffee sample1.coffee sample1 sample2 sample3 sample4 sample5 sample6 <end>
skip・take 処理
次に、下記のような加工処理を追加してみました。
- (1) 2つの要素を無視 (skip)
- (2) 3つの要素だけを取得 (take)
- (3) 先頭に # を付ける (map)
sample2.coffee
Bacon = require('baconjs').Bacon bus = new Bacon.Bus() bus.skip(2).take(3).map((s) -> "##{s}").log() [1..6].forEach (i) -> bus.push "sample#{i}" bus.end()
実行結果は下記の通りです。
実行結果
> coffee sample2.coffee #sample3 #sample4 #sample5 <end>
ジニ不純度の算出3 - Python, R, CoffeeScript
前々回 と前回 に続き、下記のようなプログラム言語でジニ不純度(ジニ係数)の算出処理を同様に実装してみました。
今回のソースは http://github.com/fits/try_samples/tree/master/blog/20140622/
Python で実装
- Python 2.7
- IronPython 2.7
Python では itertools モジュールの groupby
や combinations
関数が使えます。
groupby
は Haskell と同様に隣り合う同じ値をグルーピングできます。 (今回のケースでは sorted
でソートが必要)
groupby 結果の値部分(グルーピング部分)には直接 len
関数を使えないようなので list
関数でリスト化してから len を適用します。
また、combinations
関数を使用すると Scala の combinations と同様に要素の組み合わせを取得できます。(下記では AB、AC、BC の 3種類)
gini.py
from itertools import * def size(xs): return float(len(xs)) # (a) 1 - (AA + BB + CC) def giniA(xs): return 1 - sum(map(lambda (k, g): (size(list(g)) / size(xs)) ** 2, groupby(sorted(xs)))) def countby(xs): return map(lambda (k, v): (k, size(list(v))), groupby(sorted(xs))) # (b) AB * 2 + AC * 2 + BC * 2 def giniB(xs): return sum(map( lambda ((xk, xv), (yk, yv)): xv / size(xs) * yv / size(xs) * 2, combinations(countby(xs), 2) )) vlist = ["A", "B", "B", "C", "B", "A"] print giniA(vlist) print giniB(vlist)
実行結果
> python gini.py 0.611111111111 0.611111111111
Python 3 で実行するには
Python 3.4 で実行するにはラムダ式と print のところを書き換える必要があります。
Python 3 のラムダ式ではタプル内の複数の要素を個別に引数として取得できないようなので、Python 2 のように lambda (k, v): ・・・
とは書けず、lambda x: ・・・
として個々の要素をインデックスで参照 (x[0]
等) する事になります。
gini3.py (Python 3.4)
・・・ # (a) 1 - (AA + BB + CC) def giniA(xs): return 1 - sum(map(lambda x: (size(list(x[1])) / size(xs)) ** 2, groupby(sorted(xs)))) ・・・ # (b) AB * 2 + AC * 2 + BC * 2 def giniB(xs): return sum(map( lambda x: x[0][1] / size(xs) * x[1][1] / size(xs) * 2, combinations(countby(xs), 2) )) vlist = ["A", "B", "B", "C", "B", "A"] print(giniA(vlist)) print(giniB(vlist))
実行結果 (Python 3.4)
> python gini3.py 0.6111111111111112 0.611111111111111
R で実装
R では table
関数で要素毎のカウント値を取得でき、combn
関数で Scala や Python の combinations と同様の組み合わせを行列(matrix)として取得できます。
lapply
の結果(リスト)には sum
関数を適用できないようなので Reduce
を使って合計しています。
また、apply
の第2引数を 2 とすれば列単位にデータを処理できます。
gini.R
# (a) 1 - (AA + BB + CC) giniA <- function(xs) { 1 - Reduce("+", lapply(table(xs), function(x) (x / length(xs)) ^ 2)) } # (b) AB * 2 + AC * 2 + BC * 2 giniB <- function(xs) { sum(apply(combn(table(xs), 2), 2, function(x) (x[1] / length(xs)) * (x[2] / length(xs)) * 2)) } list <- c("A", "B", "B", "C", "B", "A") giniA(list) giniB(list)
実行結果
・・・ > giniA(list) [1] 0.6111111 > giniB(list) [1] 0.6111111
備考
各処理の結果は下記のようになります。
table(list) の結果
> table(list) list A B C 2 3 1
combn(table(list), 2) の結果
> combn(table(list), 2) [,1] [,2] [,3] [1,] 2 2 3 [2,] 3 1 1
ちなみに、上記は以下のような組み合わせのカウント値です。
[,1] [,2] [,3] [1,] "A" "A" "B" [2,] "B" "C" "C"
CoffeeScript で実装
- CoffeeScript 1.7
CoffeeScript では、Underscore.js 等のライブラリを使用しない限り、グルーピングや組み合わせの処理を自前で実装する事になると思います。
gini.coffee
countBy = (xs) -> xs.reduce (acc, x) -> acc[x] ?= 0 acc[x]++ acc , {} sum = (xs) -> xs.reduce (acc, x) -> acc + x # (a) 1 - (AA + BB + CC) giniA = (xs) -> 1 - sum( (v / xs.length) ** 2 for k, v of countBy(xs) ) flatten = (xs) -> xs.reduce (x, y) -> x.concat y combination = (xs) -> flatten( [x, y] for x in xs when x isnt y for y in xs ) # (b) BA + CA + AB + CB + AC + BC giniB = (xs) -> sum( (x[1] / xs.length) * (y[1] / xs.length) for [x, y] in combination([k, v] for k, v of countBy(xs)) ) list = ["A", "B", "B", "C", "B", "A"] console.log giniA(list) console.log giniB(list)
実行結果
> coffee gini.coffee 0.6111111111111112 0.611111111111111
Java Scripting API で CoffeeScript を実行
はじめに
Java SE 6 から実装されている Java Scripting API を使えば JavaScript を JVM 上で実行できます。そして、CoffeeScript は JavaScript にコンパイルできます。
と言う事で Java Scripting API を使って CoffeeScript を実行する Groovy スクリプトを作成してみました。
ソースは http://github.com/fits/try_samples/tree/master/blog/20130618/
CoffeeScript から JavaScript への変換方法
今のところ Java Scripting API で直接 CoffeeScript を実行できませんので、まずは JavaScript へ変換する必要があります。
通常の CoffeeScript コンパイラは基本的に Node.js 用ですので、今回の用途には向いていないと思われます。
そこで、Web ブラウザ用の CoffeeScript コンパイラ(CoffeeScript ソース の下記ファイル)を使用する事にします。
- extras/coffee-script.js
CoffeeScript を実行する Groovy スクリプト作成
coffee-script.js を使って CoffeeScript を実行する Groovy スクリプトは下記のようになりました。(安全な実行にはスコープ等を考えたほうが良いかもしれません)
run_coffee.groovy
import javax.script.* // (1) def engine = new ScriptEngineManager().getEngineByExtension('js') // (2) engine.eval(new FileReader('coffee-script.js')) // (3) engine.put('coffeeSrc', new File(args[0]).getText('UTF-8')) // (4) engine.eval('eval(CoffeeScript.compile(coffeeSrc))')
処理内容は単純で、JavaScript 用の ScriptEngine を取得し (1) 、coffee-script.js をロード(評価)します (2)。次に実行時引数で指定した CoffeeScript ソースファイルの内容を JavaScript 上の coffeeSrc 変数へ設定し (3)、最後に CoffeeScript.compile(coffeeSrc) で生成された JavaScript を eval で直接実行 (4) しています。
なお、(2) と (3) はどちらを先に実行しても支障は無いはずです。
単純な CoffeeScript の実行
それでは、下記のような単純な CoffeeScript ソースファイルを実行してみます。
sample.coffee
total = 0 total += value for value in [1..5] println total
実行結果は下記の通り。一応動作しているようです。
実行結果
> groovy run_coffee.groovy sample.coffee 15
RxJava を利用した CoffeeScript の実行
次は、外部の JAR ファイルを用いた CoffeeScript ソースファイルを実行してみる事にします。 今回は fits:id:20130310 でも試した RxJava を使ってみました。
- rxjava-core-0.9.0.jar
rx_sample.coffee
importClass java.util.Arrays importPackage Packages.rx Observable.from(Arrays.asList [1..10]).filter( (a) -> a % 2 is 0 ).skip(1).take(2).subscribe (a) -> println a
処理内容は、1 ~ 10 の数値から偶数を取り出し、先頭をスキップして 2 つのデータを取得して出力しています。
ここで、外部 JAR のパッケージやクラスを参照する場合は、Packages を使用する点に注意が必要です。
- Packages.<パッケージ名>
- Packages.<パッケージ名>.<クラス名>
上記 CoffeeScript を実行するには、Groovy スクリプトが rxjava-core-0.9.0.jar をロードできるように、ユーザーホームディレクトリの .groovy/lib へ rxjava-core-0.9.0.jar を配置する等しておきます。
実行結果
> groovy run_coffee.groovy rx_sample.coffee 4 6
RxJS で行単位のファイル処理
前々回(id:fits:20130224)・前回(id:fits:20130310)のファイル処理と同様の処理を RxJS を使い CoffeeScript で実装してみました。
今回作成したソースは http://github.com/fits/try_samples/tree/master/blog/20130313/
RxJS インストール
RxJS をインストールするため下記のような package.json を用意して npm install しました。
package.json
{ ・・・ "dependencies": { "coffee-script": "*", "rxjs": "*" } }
npm でインストール
> npm install ・・・ npm http GET https://registry.npmjs.org/rxjs npm http GET https://registry.npmjs.org/coffee-script ・・・ rxjs@1.0.10621 node_modules\rxjs coffee-script@1.6.1 node_modules\coffee-script
readFile を使ったサンプル
ファイルの内容を一括で読み込んでコールバック関数を呼び出す fs.readFile を使って実装してみました。
1行分のデータを PUSH する Observable はこれまでと同じように Observable.create を使って作成します。
ファイルの内容がコールバック関数の data に設定されてくるので、改行で split して 1行毎に onNext を呼び出します。
今回もキャンセル機能は用意しないため、何もしない関数 "->" を返しています。*1
ここで関数を返さないと実行時に Type Error が発生する事になります。
readline_file.coffee
rx = require 'rxjs' fs = require 'fs' fromFile = (file) -> rx.Observable.create (observer) -> fs.readFile file, (err, data) -> if err # エラー発生時 observer.onError err else data.toString().split(/\n|\r\n/).forEach (line) -> # 1行分のデータを PUSH observer.onNext line # 完了時 observer.onCompleted() # 何もしない関数を返す -> # 1行目をスキップして 2・3 行目の先頭に # を付けて出力 fromFile(process.argv[2]).skip(1).take(2).subscribe (x) -> console.log '#' + x
実行結果
> coffee readline_file.coffee sample.txt #サンプル #
Stream を使ったサンプル
次は Stream を使って同様の処理を実装してみました。
fs.createReadStream で Stream を取得します。
ここで encoding を指定しておくと data イベントで取得できる内容が encoding で指定した文字コードの文字列となります。
Stream の場合は、基本的にバッファサイズでデータが分割される点に注意が必要で、適切な encoding を指定しておかないとバッファサイズによっては文字化けする事になります。
readline_file2.coffee
rx = require 'rxjs' fs = require 'fs' fromFile = (file) -> rx.Observable.create (observer) -> stream = fs.createReadStream file, {encoding: 'utf-8'} # エラー発生時 stream.on 'error', (ex) -> observer.onError ex # 完了時 stream.on 'end', -> observer.onCompleted() stream.on 'close', -> console.log '*** close' buf = '' stream.on 'data', (data) -> list = (buf + data).split(/\n|\r\n/) # 次に持ち越すデータ buf = list.pop() # 1行分ずつデータを PUSH observer.onNext line for line in list # 何もしない関数を返す -> # 1行目をスキップして 2・3 行目の先頭に # を付けて出力 fromFile(process.argv[2]).skip(1).take(2).subscribe (x) -> console.log '#' + x
実行結果
> coffee readline_file2.coffee sample.txt #サンプル # *** close
JavaScript で List モナド - Monadic
Monadic は JavaScript 用のモナドライブラリです。
今回はこの Monadic を使って、以前 (id:fits:20120912, id:fits:20120930) に Scalaz や Functional Java で実装したナイト移動の List モナド処理 *1 を JavaScirpt で実装してみました。
- Monadic 0.0.5
- Node.js v0.8.18
サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20130120/
Monadic のインストール
npm install monadic でインストールするか、以下のように package.json の dependencies に monadic の設定を追加して npm install しておきます。
package.json
{ "name": "monadic-list-sample", "version": "0.0.0", "dependencies": { "monadic": "*" } }
実行例
> npm install
3手版
3手先のナイトの移動位置を全て列挙する処理と3手後に指定の終了位置に到達するか否かを判定する処理を実装します。
Monadic でバインド処理 *2 を行うには mbind を使います。List モナドは require('monadic').list() で取得できます。
3手版 move_knight.js
var monadic = require('monadic'); var listM = monadic.list(); // ナイトの次の移動先を列挙 var moveKnight = function(p) { return [ {c: p.c + 2, r: p.r - 1}, {c: p.c + 2, r: p.r + 1}, {c: p.c - 2, r: p.r - 1}, {c: p.c - 2, r: p.r + 1}, {c: p.c + 1, r: p.r - 2}, {c: p.c + 1, r: p.r + 2}, {c: p.c - 1, r: p.r - 2}, {c: p.c - 1, r: p.r + 2} ].filter(function(t) { return (1 <= t.c && t.c <= 8 && 1 <= t.r && t.r <= 8); }); }; // 3手先の移動位置を列挙(重複あり) var in3 = function(start) { return listM.mbind( listM.mbind( // 下記の listM.mbind(・・・) は moveKnight(start) でも可 listM.mbind( [start], moveKnight ), moveKnight ), moveKnight ); }; console.log(in3({c: 6, r: 2})); // 指定位置に3手で到達できるか否かを判定 var canReachIn3 = function(start, end) { return in3(start).some(function(p) { return (p.c == end.c && p.r == end.r); }); }; console.log(canReachIn3({c: 6, r: 2}, {c: 6, r: 1})); console.log(canReachIn3({c: 6, r: 2}, {c: 7, r: 3}));
実行結果は以下の通り。
3手版の実行結果
> node move_knight.js [ { c: 8, r: 1 }, { c: 8, r: 3 }, { c: 4, r: 1 }, { c: 4, r: 3 }, ・・・ { c: 3, r: 4 }, { c: 3, r: 8 } ] true false
do 記法 3手版
上記サンプルの in3 の処理を Monadic の do 記法を使って実装し直したのが下記です。
Monadic の do 記法は以下のように実装します。(Haskell の do に近い記述が可能になっています)
do 記法 3手版 move_knight.mjs
var monadic = require('monadic'); var listM = monadic.list(); var moveKnight = function(p) { ・・・ }; var in3 = function(start) { // do 記法で実装 return do listM { first <- moveKnight(start) second <- moveKnight(first) third <- moveKnight(second) return third }; }; console.log(in3({c: 6, r: 2})); var canReachIn3 = function(start, end) { ・・・ }; console.log(canReachIn3({c: 6, r: 2}, {c: 6, r: 1})); console.log(canReachIn3({c: 6, r: 2}, {c: 7, r: 3}));
do 記法を使った .mjs ファイルのオーソドックスな実行方法が分からなかったので、-e オプションを使って実行してみました。
require('monadic').transform() で 'move_knight.mjs' を純粋な JavaScript コードへ変換し eval 実行しています。
do 記法 3手版の実行結果
> node -e "eval(require('monadic').transform(process.argv[1]))" move_knight.mjs [ { c: 8, r: 1 }, { c: 8, r: 3 }, { c: 4, r: 1 }, { c: 4, r: 3 }, ・・・ { c: 3, r: 4 }, { c: 3, r: 8 } ] true false
transform による変換結果は以下のようになります。
require('monadic').transform('move_knight.mjs') の変換結果(do の箇所)
・・・ var in3 = function (start) { return listM.mbind(moveKnight(start), function (first) { return listM.mbind(moveKnight(first), function (second) { return listM.mbind(moveKnight(second), function (third) { return function () { return listM.mreturn(third); }.bind(this)(); }.bind(this)); }.bind(this)); }.bind(this)); }; ・・・
N手版
N手版は、moveKnight を使ったバインド処理 (listM.mbind) を入れ子に N個連結するため reduceRight を使いました。
N手版 move_knight_many.js
var monadic = require('monadic'); var listM = monadic.list(); var moveKnight = function(p) { ・・・ }; // N手 (num) 先の移動位置を列挙(重複あり) var inMany = function(num, start) { // num 個の moveKnight から成る配列を作成 var items = new Array(num); for (var i = 0; i < num; i++) { items.push(moveKnight); } // num 個の moveKnight を listM.mbind で連結 return items.reduceRight( function(acc, elem) { return listM.mbind(acc, elem); }, [start] ); }; console.log(inMany(3, {c: 6, r: 2})); // 指定位置に N手(num) で到達できるか否かを判定 var canReachInMany = function(num, start, end) { return inMany(num, start).some(function(p) { return (p.c == end.c && p.r == end.r); }); }; console.log(canReachInMany(3, {c: 6, r: 2}, {c: 6, r: 1})); console.log(canReachInMany(3, {c: 6, r: 2}, {c: 7, r: 3}));
実行結果は以下の通り。
N手版の実行結果
> node move_knight_many.js [ { c: 8, r: 1 }, { c: 8, r: 3 }, { c: 4, r: 1 }, { c: 4, r: 3 }, ・・・ { c: 3, r: 4 }, { c: 3, r: 8 } ] true false
CoffeeScript 版
最後に、3手版とN手版のサンプルを CoffeeScript で実装し直してみました。
- CoffeeScript 1.4.0
Monadic の do 記法に関しては、TameJS の時 (id:fits:20120415) と同様に Embedded JavaScript を使えばいけるかもしれませんが今回は調査していません。
CoffeeScript 3手版 move_knight.coffee
monadic = require 'monadic' listM = monadic.list() moveKnight = (p) -> [ {c: p.c + 2, r: p.r - 1}, {c: p.c + 2, r: p.r + 1}, {c: p.c - 2, r: p.r - 1}, {c: p.c - 2, r: p.r + 1}, {c: p.c + 1, r: p.r - 2}, {c: p.c + 1, r: p.r + 2}, {c: p.c - 1, r: p.r - 2}, {c: p.c - 1, r: p.r + 2} ].filter (t) -> t.c in [1..8] and t.r in [1..8] in3 = (start) -> listM.mbind( listM.mbind( listM.mbind( [start], moveKnight ), moveKnight ), moveKnight ) console.log in3 {c: 6, r: 2} canReachIn3 = (start, end) -> in3(start).some (p) -> p.c == end.c and p.r == end.r console.log canReachIn3 {c: 6, r: 2}, {c: 6, r: 1} console.log canReachIn3 {c: 6, r: 2}, {c: 7, r: 3}
CoffeeScript 3手版の実行結果
> coffee move_knight.coffee [ { c: 8, r: 1 }, { c: 8, r: 3 }, { c: 4, r: 1 }, { c: 4, r: 3 }, ・・・ { c: 3, r: 4 }, { c: 3, r: 8 } ] true false
N手版は以下の通りです。
[1..num].map( -> moveKnight ) で num 個の moveKnight から成る配列を作っています。
CoffeeScript N手版 move_knight_many.coffee
monadic = require 'monadic' listM = monadic.list() moveKnight = (p) -> [ {c: p.c + 2, r: p.r - 1}, {c: p.c + 2, r: p.r + 1}, {c: p.c - 2, r: p.r - 1}, {c: p.c - 2, r: p.r + 1}, {c: p.c + 1, r: p.r - 2}, {c: p.c + 1, r: p.r + 2}, {c: p.c - 1, r: p.r - 2}, {c: p.c - 1, r: p.r + 2} ].filter (t) -> t.c in [1..8] and t.r in [1..8] inMany = (num, start) -> [1..num].map( -> moveKnight ).reduceRight (acc, elem) -> listM.mbind(acc, elem) , [start] console.log inMany 3, {c: 6, r: 2} canReachInMany = (num, start, end) -> inMany(num, start).some (p) -> p.c == end.c and p.r == end.r console.log canReachInMany 3, {c: 6, r: 2}, {c: 6, r: 1} console.log canReachInMany 3, {c: 6, r: 2}, {c: 7, r: 3}
CoffeeScript N手版の実行結果
> coffee move_knight_many.coffee [ { c: 8, r: 1 }, { c: 8, r: 3 }, { c: 4, r: 1 }, { c: 4, r: 3 }, ・・・ { c: 3, r: 4 }, { c: 3, r: 8 } ] true false
Vert.x で WebSocket
Vert.x 1.3.1 Final で単純な WebSocket 処理を実装してみました。
単純な JSON データを送受信するチャットサーバーを Vert.x で、動作確認のためのクライアントを HTML5 で実装する事にします。
サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20130114/
クライアントの作成 (HTML5)
まずはクライアントです。
以下のような JSON データを送受信する WebSocket の処理を CoffeeScript で実装しました。送信する画像データは Web ブラウザへ画像ファイルをドラッグアンドドロップする事で切り替えられるようにしています。
{"message": <メッセージ>, "image": <Data URL 形式の画像データ>}
index.coffee
$( -> # ドラッグアンドドロップ処理 addEventListener 'dragover', (ev) -> ev.preventDefault() ,false # ドロップした画像で img の src を置き換える処理 addEventListener 'drop', (ev) -> ev.preventDefault() file = ev.dataTransfer.files[0] if file.type.indexOf('image/') is 0 r = new FileReader() r.onload = (ev) -> $('#image').attr 'src', ev.target.result r.readAsDataURL file # WebSocket ws = new WebSocket 'ws://localhost:8080/connect' # JSONデータ受信時の処理 ws.onmessage = (event) -> obj = JSON.parse event.data $('#list').prepend "<div><img class=\"chatimg\" src=\"#{obj.image}\" /><p class=\"msg\">#{obj.message}</p></div>" # 接続時の処理 ws.onopen = (event) -> console.log "open : #{event}" # 切断時の処理 ws.onclose = (event) -> console.log "close : code = #{event.code}, reason = #{event.reason}" # JSONデータ送信処理 $('#sendMessage').click -> msgEl = $('#message') params = message: msgEl.val() image: $('#image').attr('src') ws.send JSON.stringify(params) msgEl.val('') # ページ終了時の処理 $(window).bind 'beforeunload', -> ws.close() )
上記ファイルを JavaScript へ変換しておきます。(下記コマンドで index.js ファイルが生成されます)
index.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>image chat sample</title> <link rel="stylesheet" href="index.css" /> <script src="http://code.jquery.com/jquery-1.8.3.min.js"></script> <script src="index.js"></script> </head> <body> <div> <img id="image" src="data:image/png;base64,iVBORw・・・Jggg==" class="chatimg" /> <textarea id="message" cols="50" rows="2" ></textarea> <br /> <button id="sendMessage">send</button> </div> <hr /> <div id="list"></div> </body> </html>
サーバーの作成 (Vert.x)
それでは、本題のサーバー部分を Groovy で実装してみます。
Vert.x の Groovy では、HttpServer の websocketHandler へクロージャを渡して WebSocket の処理を実装します。
このクロージャは新規クライアントからの接続が発生する度に実行され、パラメータとして WebSocket クラスのインスタンスが渡ってきます。
WebSocket クラスには複数のハンドラーメソッド(xxxHandler)が用意されており、任意のハンドラーにクロージャを渡して各処理を実装します。
今回は以下のハンドラーを使いました。
- dataHandler でデータ受信時の処理を実装 (全クライアントへデータ送信)
- closedHandler でクライアントとの接続が切れた際の処理を実装 (wsMap から接続が切れたものを削除)
下記サンプルでは、ConcurrentHashMap で WebSocket のインスタンスを管理しておき、JSON 受信時に全 WebSocket インスタンスへ JSON をそのまま送信するよう実装しています。
server.groovy
def wsMap = [:] as java.util.concurrent.ConcurrentHashMap vertx.createHttpServer().websocketHandler { ws -> ws.dataHandler { data -> // 全クライアントへデータ送信 wsMap.each { k, v -> v.writeTextFrame(data.toString()) } } ws.closedHandler { // 接続切断されたものを wsMap から削除 wsMap.remove(ws.textHandlerID) println "closed : ${ws.textHandlerID}" } println "connect : ${ws.textHandlerID}" wsMap.put(ws.textHandlerID, ws) }.listen 8080 println "server started ..."
下記のように vertx run でサーバーを起動した後、index.html ファイルを複数の Web ブラウザで開いて *1 send ボタンを押下すると JSON の送受信を確認できると思います。
ただし、一度 JSON データを送信した後でないと受信できないようなので注意。(原因はよく分かってません)
実行例
> vertx run server.groovy server started ... connect : 8680eeb8-4666-4ec0-9496-98d8269f8099 connect : 1dd620bb-f9b3-4bed-90c9-aa2bb2243786 closed : 8680eeb8-4666-4ec0-9496-98d8269f8099 ・・・
*1:今回はローカルファイルを直接開きました
軽量 Web フレームワークで REST API を実装 - Vert.x, Gretty, Play2 Mini, Socko, Restify
個人的に REST API の実装では JAX-RS (Java)*1 や Sinatra (Ruby) あたりを使っていますが、今回は選択肢を増やす目的で下記のようなフレームワークを試してみました。
- Vert.x (Java, Groovy, JavaScript, Ruby, Python)
- Gretty (Java, Groovy, Scala)
- Play2 Mini (Java, Scala)
- Socko (Scala)
- Restify (Node.js)
ちなみに、今回試した Java 系のフレームワーク(Restify 以外)は内部的に Netty を使っています。
サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20130106/
はじめに
今回は以下のような単純な REST API を実装する事にします。
- /user/
で JSON 文字列 を GET - /user で JSON 文字列 を POST
動作確認は、以下の Ruby スクリプトで行う事にします。
client.rb
#coding:utf-8 require 'net/http' require 'json/pure' Net::HTTP.start('localhost', 8080) { |http| # GET 処理 res = http.get('/user/1') puts "#{res}, #{res.code}, #{res.content_type}, #{res.body}" data = { 'name' => 'test', 'note' => 'サンプル' } # POST 処理 res = http.post('/user', JSON.generate(data), {'Content-Type' => 'application/json'}) puts "#{res}, #{res.code}, #{res.body}" }
Vert.x
- Vert.x 1.3.0
id:fits:20120513 や id:fits:20120708 で扱った Vert.x です。
Vert.x は JVM 用の Node.js ライクなサーバーフレームワークで、Java・JavaScript・CoffeeScript・Groovy・Ruby・Python と多様な言語をサポートしています。
RouteMatcher を使えば Sinatra のように HTTP Method と URL パターンの組み合わせで処理を実装できます。
下記サンプルは Groovy で実装しました。
vert.x/server.groovy
import org.vertx.groovy.core.http.RouteMatcher import org.vertx.java.core.json.impl.Json def rm = new RouteMatcher() rm.get '/user/:id', { req -> def res = req.response res.putHeader('Content-Type', 'application/json') res.end Json.encode([ id: req.params['id'], name: 'vert.x sample' ]) } rm.post '/user', { req -> req.bodyHandler { // JSON を Map へデコード def data = Json.decodeValue(it.toString(), Map) println data req.response.end() } } vertx.createHttpServer().requestHandler(rm.asClosure()).listen 8080 println "server started ..."
実行と動作確認結果(サーバー側)
> vertx run server.groovy server started ... [name:test, note:サンプル]
動作確認結果(クライアント側)
> jruby client.rb #<Net::HTTPOK:0x889b125>, 200, application/json, {"id":"1","name":"vert.x sample"} #<Net::HTTPOK:0x7ff843da>, 200,
Gretty
- Gretty 0.4.302
Gretty は Netty をベースにしたサーバーフレームワークで Java・Groovy・Scala をサポートしています。
下記サンプルは Groovy++ で実装していますが、Groovy++ は今のところ Groovy 2.0 をサポートしていないようで、Groovy 1.8 で実行する必要がありました。
gretty/server.groovy
/* * Groovy 1.8 で実行する必要あり * Groovy 2.0 では ExceptionInitilizerError が発生 */ @GrabResolver(name = 'gretty', root = 'http://groovypp.artifactoryonline.com/groovypp/libs-releases-local/') @Grab('org.mbte.groovypp:gretty:0.4.302') import static java.nio.charset.StandardCharsets.* import static org.mbte.gretty.JacksonCategory.* import org.mbte.gretty.httpserver.GrettyServer GrettyServer server = [] server.groovy = [ localAddress: new InetSocketAddress('localhost', 8080), '/user/:id': { get { response.json = [ id: request.parameters['id'], name: 'gretty sample' ] } }, '/user': { post { /* * request.contentText を使うとプラットフォームの * デフォルトエンコードが使われるようなので * 明示的に UTF-8 で処理 */ def data = fromJson(Map, request.content.toString(UTF_8)) println data response.json = '' } } ] server.start()
実行と動作確認結果(サーバー側)
> groovy server.groovy 1 06, 2013 10:25:00 午前 org.mbte.gretty.AbstractServer 情報: Started server on localhost/127.0.0.1:8080 [name:test, note:サンプル]
動作確認結果(クライアント側)
> jruby client.rb #<Net::HTTPOK:0x12da7d5f>, 200, application/json, {"id":"1","name":"gretty sample"} #<Net::HTTPOK:0x889b125>, 200,
Play2 Mini
- Play2 Mini 2.0.3
Play2 Mini は Play2 をベースにした簡易フレームワークで、Java や Scala をサポートしています。
Scala で URL パスを正規表現マッチさせるには Through を使います。 *2
Through は引数の種類によってブロックに渡る引数の内容(下記サンプルの groups: List[String])が異なります。
Throughの引数 | ブロックの引数 | Throughの引数例 | URL例 | ブロックの引数例(List[String]) |
---|---|---|---|---|
正規表現 | 正規表現のグループ | "/user/(.*)".r | /user/a/b | ["a/b"] |
文字列 | 指定の文字列以降を "/" でスプリットしたもの | "/user" | /user/a/b | ["a", "b"] |
なお、Play2 Mini では以下のようにして実装します。
- (1) com.typesafe.play.mini.Application トレイトを extends して route を実装
- (2) (1) を指定した com.typesafe.play.mini.Setup を extends したグローバルパッケージの Global を作成
- (3) play.core.server.NettyServer で実行
play-mini/Server.scala (1)
package fits.sample import com.typesafe.play.mini._ import play.api.mvc._ import play.api.mvc.Results._ import play.api.libs.json._ object Server extends Application { def route = Routes( Through("/user/([^/]*)".r) { groups: List[String] => Action { val id :: Nil = groups Ok(Json.toJson { Map( "id" -> id, "name" -> "play-mini sample" ) }) } }, { case POST(Path("/user")) => Action { req => val data = req.body.asJson data.foreach(println) Ok("") } } ) }
play-mini/Global.scala (2)
object Global extends com.typesafe.play.mini.Setup(fits.sample.Server)
play-mini/build.sbt (3)
scalaVersion := "2.9.2" resolvers += "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/" libraryDependencies += "com.typesafe" %% "play-mini" % "2.0.3" mainClass in (Compile, run) := Some("play.core.server.NettyServer")
デフォルトのポート番号 9000 ではなく 8080 で実行するため、sbt run 時に -Dhttp.port=8080 を指定します。
実行と動作確認結果(サーバー側)
> sbt -Dhttp.port=8080 run ・・・ [info] play - Listening for HTTP on port 8080... {"name":"test","note":"サンプル"}
動作確認結果(クライアント側)
> jruby client.rb #<Net::HTTPOK:0x7ff843da>, 200, application/json, {"id":"1","name":"play-mini sample"} #<Net::HTTPOK:0x1d7ae341>, 200,
Socko
- Socko 0.2.3
Socko は Netty と Akka をベースとした Web サーバーフレームワークです。
Akka の Actor として処理を実装するので、多少コード量が多くなります。
socko/Server.scala
package fits.sample import scala.util.parsing.json.{JSON, JSONObject} import org.mashupbots.socko.events.HttpRequestEvent import org.mashupbots.socko.routes.{Routes, GET, POST} import org.mashupbots.socko.webserver.{WebServer, WebServerConfig} import akka.actor.{Actor, ActorSystem, Props} object Server extends App { class UserGetHandler extends Actor { def receive = { case req: HttpRequestEvent => val path = req.request.endPoint.path val id = path.replace("/user/", "").split("/").head req.response.write( JSONObject( Map( "id" -> id, "name" -> "socko sample" ) ).toString(), "application/json" ) context.stop(self) } } class UserPostHandler extends Actor { def receive = { case req: HttpRequestEvent => val content = req.request.content.toString val data = JSON.parseFull(content) data.foreach(println) req.response.write("") context.stop(self) } } val actorSystem = ActorSystem("SampleActorSystem") val routes = Routes({ case GET(req) => actorSystem.actorOf(Props[UserGetHandler]) ! req case POST(req) if req.endPoint.path == "/user" => actorSystem.actorOf(Props[UserPostHandler]) ! req }) val server = new WebServer(WebServerConfig(port = 8080), routes, actorSystem) server.start() Runtime.getRuntime.addShutdownHook(new Thread() { override def run { server.stop() } }) }
socko/build.sbt
scalaVersion := "2.9.2" resolvers += "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/" libraryDependencies += "org.mashupbots.socko" %% "socko-webserver" % "0.2.3" mainClass in (Compile, run) := Some("fits.sample.Server")
実行と動作確認結果(サーバー側)
> sbt run ・・・ [info] Running fits.sample.Server 11:14:00.300 [run-main] INFO o.m.socko.webserver.WebServer - Socko server 'WebServer' started on localhost:8080 11:14:21.032 [New I/O worker #1] DEBUG o.m.socko.webserver.RequestHandler - HTTP EndPoint(GET,localhost:8080,/user/1) CHANNEL=-384208271 11:14:21.098 [New I/O worker #1] DEBUG o.m.socko.webserver.RequestHandler - HTTP EndPoint(POST,localhost:8080,/user) CHANNEL=-384208271 Map(name -> test, note -> サンプル)
動作確認結果(クライアント側)
> jruby client.rb #<Net::HTTPOK:0x7ff843da>, 200, application/json, {"id" : "1", "name" : "socko sample"} #<Net::HTTPOK:0x6eddcf85>, 200,
Restify
- Restify 1.4.2
Restify は Express によく似た Node.js 用のフレームワークで、REST API の実装に特化しています。
最新バージョンは Restify 2.0.4 でしたが、Windows OS 上の npm install に失敗するので、今回は古いバージョンを使っています。
app.coffee
restify = require 'restify' server = restify.createServer() server.use restify.bodyParser() server.get '/user/:id', (req, res, next) -> res.json id: req.params.id name: 'restify sample' next() server.post '/user', (req, res, next) -> data = JSON.parse req.body console.log data res.json null next() server.listen 8080, -> console.log "server started ..."
実行と動作確認結果(サーバー側)
> coffee app.coffee server started ... { name: 'test', note: 'サンプル' }
動作確認結果(クライアント側)
> jruby client.rb #<Net::HTTPOK:0x14980563>, 200, application/json, {"id":"1","name":"restify sample"} #<Net::HTTPOK:0x42eded9>, 200,