これはマイネット Advent Calendar 20日目の記事です。
5日目に引き続き、@aonoが担当します。
さて、elixirにはパイプライン演算子というものがあります。Unix系OSにおけるパイプと似たような、関数と関数を接続するための演算子で、左辺の戻り値を右辺に第一引数として渡して実行させると言うものです。F#がルーツになっているそうですね。
[1, 2, 3] |> Enum.map(fn(x) -> x + 1 end) |> Enum.filter(fn(x) -> rem(x, 2) == 0 end)
#=> [2, 4]
こんな感じ。
小さな関数を繋いで式を作れるので、見通しがいいのと、関数の再利用がしやすくなるのが利点かと思います。
で、RubyistとしてはRubyでも同じ事がしたくなるわけです。
ただし、Rubyの仕様上|>
と言う演算子は定義不能(define_methodで無理矢理定義することは不可能ではないが、使用不可能)なので、演算子として使用可能な|
で代用します。
また、Rubyはprocを使わないと関数をオブジェクト化出来ないので、関数を接続するのではなくオブジェクトを接続する形を取りたいと思います。
まずはパイプライン演算子を使用可能にするためのクラス
class PipeBase
def initialize(data)
@data = data
@klasses = []
end
def |(klass)
@klasses << klass
self
end
def lower; end
def self.lift; end
end
class Pipe < PipeBase
def self.lift(data)
new data
end
def lower
if @klasses.length == 0
return @data
end
@klasses.inject(@data) do |d, klass|
break unless d
obj = klass.send(:new, d)
d = obj.execute
end
end
end
|
メソッド(演算子)は実際には右辺のクラスを配列に入れるだけの動作しかしていません。
ThreadやFiberを使用する実装、と言うのも考えた(と言うかやってみた)のですが、試作段階ではメモリの使用量が増えるだけだったのでシンプルな形に落ち着きました。今後いいやり方を思いついたら実装したいです。
使用する場合にはこれを継承し、データをパイプライン上で使用可能にするためのliftメソッドと、パイプラインからデータを持ち出すためのlowerメソッドを定義してやる必要があります。そうすると上記Pipeクラスのようになります。
そして、パイプライン上でデータを処理するフィルタクラス
class FilterBase
def initialize(data)
@data = data
end
end
class FilterA < FilterBase
def execute
return @data if @data[:log_type] == 'kc'
end
end
class FilterB < FilterBase
def execute
return @data if @data[:foo_id] == 9393
end
end
class FilterC < FilterBase
def execute
return @data if @data[:bar_id] == 72
end
end
こちらも親クラスはインスタンスメソッドを一つ定義するだけの簡単なクラスで、実処理は子クラスのexecuteメソッドに記載します。
で、これをなんのために作ったかと言えば、前回の続きな訳です。
下記のようにして使用します。
require 'msgpack'
res = []
File.open('msg_packed.log') do |log|
log.each_line do |l|
l = MessagePack.unpack(l)
pipe = Pipe.lift(l)
pipe | FilterA | FilterB | FilterC
data = pipe.lower
res << data if data
end
end
これで整形済みログファイルから必要なデータを抽出することが出来ます。(この場合、:log_typeがkcで、:foo_idが9393、:bar_idが72のデータが抽出される。え? 例が恣意的? σ(のヮの)ナンノコトヤラ)
存在していれば、いろいろ面白い使い方が出来そうな演算子ではないでしょうか。