0

RubyのlambdaやProcとblock渡しとコールバック関数

先日のRuby初級入門2013で事前に「Rubyのlambdaとかblockとか教えて」と言われていたけど、どうみても初心者向けの内容ではないので無視していたので解説する


lambda{ コード }とProc.new{ コード }の差がよくわからない

ここ読め
RubyでlambdaとProcの違いは? – QA@IT
rubyのlambdaとprocの比較実験 – Qiita [キータ]
まとめると
内部でreturnした時と、引数の数が違うときの挙動が違うそうです


block渡しを使ったイベント登録について

do 〜 endブロックで渡された処理はすぐにyieldで返さなくてもいい


block渡しを受け取れる関数を宣言する方法

例えば、フィボナッチ数列を次々と生成する場合を考える。
フィボナッチ数列は無限に続くので、計算が終わらない。
無限に終わらない処理は結果をreturnで返せないので、1つフィボナッチ数を生成するごとにコールバック呼び出しで結果を通知する仕様にする。

Fib#start(&block) 関数を宣言すると、その中の block_given? でブロック渡しされたか判定できる
# -*- coding: utf-8 -*-
## フィボナッチ数列を1つずつ計算し、1つずつコールバックで返す

class Fib
def initialize
@f0 = 0
@f1 = 1
end

def start(&block)
loop do
f2 = @f0 + @f1 # フィボナッチ数列を計算
@f0 = @f1
@f1 = f2
if block_given? # ブロック渡しされているかどうかチェック
yield f2 # コールバック呼び出し
else
puts f2 # コールバック関数が登録されてない場合、自前で標準出力する
end
break if f2 > 1000000 # キリがないので終了
end
end
end

## フィボナッチ数計算開始 ##

## コールバックなしで計算スタート
Fib.new.start


## コールバック関数登録しつつ、計算スタート
Fib.new.start do |num|
puts "フィボナッチ数列 #{num}"
sleep 1
end
このように、時間がかかるor無限に計算が終わらない処理はコールバックで結果を返すといいと思います。
Webクローラとか。
〜〜のページを取得中、完了、などを計算する側でprint出力しているとコードが汚くなります。経過をお知らせするのもコールバック関数を登録する方式でやればいい。


コールバック関数を何個でも登録できるようにする

Fib#start 関数を実行する前に、コールバック関数を登録しておく。
長い処理の場合、計算の経過を通知・計算結果を表示・ファイルに保存・twitterに通知・・・等と色々やりたい場合がある。
コールバックを1つしか登録できないよりも、複数のコールバックを登録できるようにした方がいい。

コールバック登録関数 Fib#regist_callback(&block) を作り、&blockを普通に配列に貯めて、あとで使う。
# -*- coding: utf-8 -*-
## 複数のコールバックを登録する例

class Fib
def initialize
@f0 = 0
@f1 = 1
@callbacks = Array.new # コールバック関数を保存しておく配列
end

def regist_callback(&block) # コールバック関数を登録する
raise ArgumentError, "block not given" unless block_given? # 正しくblockが渡されない場合、エラーを投げる
@callbacks.push block
end

def start
loop do
f2 = @f0 + @f1 # フィボナッチ数列を計算
@f0 = @f1
@f1 = f2
@callbacks.each do |c| ## 全てのコールバック関数を順に呼び出す
c.call f2
end
break if f2 > 1000000 # キリがないので終了
end
end
end

## フィボナッチ数計算開始 ##
fib = Fib.new

# 1つ目のコールバック関数を登録する
fib.regist_callback do |num|
puts "フィボナッチ数列 #{num}"
end

# 2つ目のコールバック関数を登録する
fib.regist_callback do |num|
puts "result = #{num}"
end

fib.start ## 計算開始


&blockとは何か

do 〜 endが&blockに渡る。
&blockをprintしてみれば何なのかわかる。
# -*- coding: utf-8 -*-

def func(arg0, arg1, &block)
if block_given?
puts "blockとは -> #{block}"
block.call "ほげ", "ふが"
else
puts "block not given"
end
end

func 1, 2 do |a, b|
puts "this is block (#{a}, #{b})"
end

proc = Proc.new{|a, b|
puts "this is block 2 (#{a}, #{b})"
}
func(1, 2, &proc)

結果
blockとは -> #<Proc:0x007ff85410ca08@block.rb:12>
this is block (ほげ, ふが)
blockとは -> #<Proc:0x007ff85410c850@block.rb:16>
this is block 2 (ほげ, ふが)
&blockはProcだった。
doで渡しても、Proc.newを参照渡ししても同じように動く。


EventEmitterでコールバックを管理する場合

コールバック関数を登録するための関数を自分で用意する必要がなくなる、EventEmitterというrubygemを作りました。

インストール

gem install event_emitter

使ってみる

クラスにincludeするとイベント管理機能が追加されます。だいぶ綺麗に書けるようになりましたね
# -*- coding: utf-8 -*-
## 複数のコールバックをEventEmitterで管理する例

require 'rubygems'
require 'event_emitter'

class Fib
include EventEmitter # EventEmitterを使う

def initialize
@f0 = 0
@f1 = 1
end

def start
loop do
f2 = @f0 + @f1 # フィボナッチ数列を計算
@f0 = @f1
@f1 = f2
emit :number, f2 # numberイベントを発行
break if f2 > 1000000 # 終了
end
emit :end # 終了イベントを発行
end
end

fib = Fib.new

# 1つ目のコールバック関数を登録する
fib.on :number do |num|
puts "フィボナッチ数列 #{num}"
end

# 2つ目のコールバック関数を登録する
fib.on :number do |num|
puts "result = #{num}"
end

# 終了イベントを登録する
fib.on :end do
puts "終了しました"
end

fib.start ## 計算開始
コールバック関数を分類できるようになってる。
この例では:numberと:endという2種類のコールバックを登録している。それぞれ、新しいフィボナッチ数が作られた時と、計算が終わった時に呼び出される。
event_emitterにはコールバック解除のremove_listener関数や、1回呼び出されたら消滅するコールバック once なども付いてきて大変便利。


最近作ったもので、わかりやすい例

を紹介すると
twitterでエゴサーチしてSkypeに通知するやつや、gyazzの差分を監視してSkypeに通知するやつは両方共event_emitterを活用していてエラーもクロールの経過報告も取得したページも全部コールバックを登録して受け取るようになっています。

0

RocketIOの全イベント取得方法

RocketIOではevent_emitterという(node.jsのを参考にして作った)イベント管理機構を使っている(Ruby用JavaScript用


event_emitterの全てのイベントを取得する手段がある


Ruby

user.on :* do |event_name, args|
puts event_name # 呼び出されたイベント名
p args
end


JavaScript
user.on('*', function(event_name, data){
console.log(event_name + ' was called');
console.log(data);
});


RocketIOでも、これを使っているので
Sinatra::RocketIO.on :* do |event_name, args|
puts "#{event_name} - #{args.inspect}"
end
とするとサーバー側で起こったイベント全部取れるし

var io = new RocketIO().connect();
io.on("*", function(event_name, args){
console.log(event_name);
console.log(args);
});
これでクライアント側のイベント全部取れる。


関係ないクライアントにデータ送信してるかも、とか、余計なイベント送ってるかも等の調査に便利。

0

ブラウザ用EventEmitterを作った

もうあるので車輪の再発明だけど、作った
https://github.com/shokai/event_emitter.js


元々sinatra-cometioの中で使っていたのを切り出して、テストとか書いた。

Ruby版といっしょに使うと楽しい。

event emitterとは


Node.jsに含まれているライブラリ。をWebブラウザだけで動くように実装しなおした。


こんな感じでapplyすると
var User = function(){
new EventEmitter().apply(this);
this.name = '';
};

イベント登録して
var user = new User();
user.name = 'shokai';
user.on('go', function(data.place){
alert(user.name+' -> '+data.place);
});

呼び出せるようになる
user.emit('go', {place: 'mountain'}); // "shokai -> mountain"

あと、onのかわりにonceで登録すると1回だけ呼び出せるイベントになったりとかある。
JavaScriptの全てオブジェクトに簡単にイベント機能が追加できる。
くわしくはREADMEを見て。


test


ブラウザで使うjsだけどterminalでテストしたかったので、nodeunitを使った。


このようにnode実行した時はnode moduleとして読み込めるように、event_emitter.jsの末尾でmodule.exportsに登録しておいた。
var EventEmitter = function(){
/** 略 **/
};

if(typeof module !== 'undefined' && typeof module.exports !== 'undefined'){
module.exports = EventEmitter;
}

あとはnodeunitでテストを書いた。


JavaScriptの圧縮


uglify-jsで圧縮した

0

Node.jsのEventEmitterをRubyに移植した

ものすごい必要だったので作った。


rubygemsに登録しておいたので

gem install event_emitter
で使える。

ドキュメントはここ
Ruby EventEmitter

バグ、機能要望などは
githubのissuetwitterかメールでお願いします


EventEmitterとは


Node.jsのコアライブラリ。lib/events.jsにある。
nvmでnodeをインストールすると ~/.nvm/src/node-v0.8.14/lib/events.js とかにある。

どんなオブジェクトにでも一発でonとemitという関数を追加できて、
obj.on(‘イベント名’, コールバック)でコールバック登録して
obj.emit(‘イベント名’, 引数)でイベントを発火させる。

nodeの全てのライブラリがイベント駆動するためによく使ってる機能。
addEventListenerとかよりも使いやすい。


ruby実装


onとemitをクラス宣言時にmix-inしたり、既存のクラスや特定のインスタンスに後から追加したりできる。

require "rubygems"
require "event_emitter"

class User
include EventEmitter ## mix-in
attr_accessor :name
end

user = User.new
user.name = "shokai"
user.on :go do |data|
puts "#{name} #{data[:place]}に行く"
end

user.emit :go, {:place => "かずすけ"}
# => "shokai かずすけに行く"
userインスタンスに登録したイベントはuserインスタンスの中で実行されるので、”name”だけでuser.nameの中身の”shokai”が参照できている。



onのかわりにonceで登録すると、一度だけ実行されるイベントを登録できる。
user.once :eat do |data|
puts "#{name} -> #{data}"
end

user.emit :eat, "BEEF" # => "shokai -> BEEF"
user.emit :eat, "Ramen" # => 実行されない


ごく普通のインスタンスにEventEmitter.applyすると、on/emitが追加される。特異メソッド。
EventEmitter.apply インスタンス名


既存のクラスにapplyするとクラスが拡張される
EventEmitter.apply クラス名


イベント駆動じゃないクラスとかも強引にイベント駆動にできそうですね。


参考にした


JSだと全部オブジェクトなのでシンプルなんだけど、Rubyのクラスやインスタンスやら全部にon/emit追加できるようにするのちょっと理解が大変だった。
このへんの黒魔術を参考にした。