パイプライン演算子(擬)

  • 3
    Like
  • 0
    Comment
More than 1 year has passed since last update.

これはマイネット 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を使わないと関数をオブジェクト化出来ないので、関数を接続するのではなくオブジェクトを接続する形を取りたいと思います。

まずはパイプライン演算子を使用可能にするためのクラス

pipe.rb
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クラスのようになります。

そして、パイプライン上でデータを処理するフィルタクラス

filter.rb
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メソッドに記載します。

で、これをなんのために作ったかと言えば、前回の続きな訳です。
下記のようにして使用します。

Log.rb
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のデータが抽出される。え? 例が恣意的? σ(のヮの)ナンノコトヤラ)
存在していれば、いろいろ面白い使い方が出来そうな演算子ではないでしょうか。