スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

[Ruby]メソッドチェーンをオブジェクト化するGemを作りました

2~3週間前からRubyを使い始めまして、折角だから何か作ろうかということで、Gemを作りました。
作ったのは、表題の通りメソッドチェーンそのものをオブジェクトとして扱えるようにするGemで、
名前はfunction_chainといいます。

使い方としてはこんな感じになります。

# こういうメソッドチェーンを
account.user.name

# こんな風に書いてオブジェクト化し、callメソッドで後から呼び出せます。
chain = PullChain.new(account) << :user << :name
chain.call

# また、こういうコードを
filter3(filter2(filter1(value)))

# オブジェクト化することもできます
chain = RelayChain.new >> :filter1 >> :filter2 >> :filter3
chain.call("XXX")

概要


FunctionChainはメソッドチェーンをオブジェクト化し、主に次のようなことを可能にします。
  • 後で呼び出す。
  • メソッドをチェーンに追加する。
  • メソッドをチェーンに挿入する。
  • メソッドをチェーンから削除する。

インストール方法


gem install function_chainGithubのリポジトリはこちら

PullChain & RelayChain


FunctionChain モジュールには、PullChain RelayChain という2つのクラスが定義されています。
冒頭でご紹介したように、これらのクラスはそれぞれ別々のタイプのチェーンのオブジェクト化を
サポートしています。

使用に必須となるコード


次のコードは、PullChainとRelayChainのどちらを使う場合にも必要になるコードです。
require "function_chain"
include FunctionChain
: このドキュメントではこれ以降、このコードの記述を省略します。

PullChain


PullChain は所謂メソッドチェーンをオブジェクト化するクラスです。

使用例

Account = Struct.new(:user)
User = Struct.new(:name)
account = Account.new(User.new("Louis"))

# チェーンオブジェクトを生成
chain = PullChain.new(account, :user, :name, :upcase)

# callメソッドでメソッドチェーンを実行
chain.call # => LOUIS
上記と同様の動作をするコード
  1. スラッシュ区切りの文字列を使う
    PullChain.new(account, "user/name/upcase").call
  2. << 演算子を使う
    chain = PullChain.new(account)
    chain << :user << :name << :upcase
    chain.call
  3. addメソッドを使う
    chain.add(:user).add(:name).add(:upcase).call
  4. add_allメソッドを使う
    chain.add_all(:user, :name, :upcase).call
  5. Procを使うchain = PullChain.new(account)
    chain << Proc.new { user } << Proc.new { name } << Proc.new { upcase }
    chain.call
    Procではなくlambdaを使う場合、次のようにブロックパラメータを明示する必要があります。chain = PullChain.new(account)
    chain << lambda { |account| account.user } <<
    lambda { |user| user.name } <<
    lambda { |name| name.upcase }
    chain.call
    (チェーン上にある)前のメソッド呼び出しの結果がlambdaを評価するレシーバとなります。
    そのため直接レシーバのメソッドを呼ぶことができます。chain = PullChain.new(account)
    chain << lambda { |_| user } << lambda { |_| name }
    chain << lambda { |_| upcase }
    chain.call
    Procを使えばブロックパラメータを省略できるので、レシーバを明示したい場合を除き、
    通常はProcの方を使うことになるでしょう。

  6. addメソッドのブロックを使う
    PullChain.new(account).add { user }.add { name }.add { upcase }.call

次のように、チェーンの途中にnilが含まれる場合、例外は発生せず、nilが返されます。
user.name = nil
chain.call # => nil
Insert, Delete, Clear
insert及びinsert_all メソッドは、メソッドをチェーンに挿入します。
delete_at メソッドは、指定した位置のメソッドをチェーンから削除します。
clear メソッドは、チェーンに追加されたすべてのメソッドを削除します。

引数を必要とするメソッドをチェーンに追加する

class Foo
def say(speaker, message)
puts "#{speaker} said '#{message}'"
end
end
上のように引数をとるメソッドをチェーンに追加するには、4通りの方法があります。
  1. 文字列を使う。
    chain = PullChain.new(Foo.new) << "say('John', 'Goodbye')"
    chain.call # => John said 'Goodbye'
  2. 配列を使う。
    配列のフォーマット:[メソッドのシンボル, [*引数]]
    chain = PullChain.new(Foo.new) << [:say, ["Andres", "Hello"]]
    chain.call # => Andres said 'Hello'
  3. Procを使うchain = PullChain.new(Foo.new)
    chain << Proc.new { say('Julian', 'Nice to meet you') }
    chain.call # => Julian said 'Nice to meet you'
  4. addメソッドのブロックを使うchain = PullChain.new(Foo.new).add { say('Narciso', 'How do you do?') }
    chain.call # => Narciso said 'How do you do?'

ブロックを必要とするメソッドをチェーンに追加する

[1,2,3,4,5].inject(3) { |sum, n| sum + n } # => 18上のようにブロックを必要とするメソッドをチェーンに追加するには、4通りの方法があります。
  1. 文字列を使う。
    chain = PullChain.new([1,2,3,4,5])
    chain << "inject(3) { |sum, n| sum + n }"
    chain.call # => 18
  2. 配列を使う。
    配列のフォーマット: [メソッドのシンボル, [*引数, Procかlambda]]
    chain = PullChain.new([1,2,3,4,5])
    chain << [:inject, [3, lambda { |sum, n| sum + n }]]
    chain.call # => 18
  3. Procを使うchain = PullChain.new([1,2,3,4,5])
    chain << Proc.new { inject(3) { |sum, n| sum + n } }
    chain.call # => 18
  4. addメソッドのブロックを使うchain = PullChain.new([1,2,3,4,5])
    chain.add { inject(3) { |sum, n| sum + n } }
    chain.call # => 18

チェーンの途中で取得したオブジェクトを後方から参照する

以下は例で使用するクラスFoo = Struct.new(:bar)
Bar = Struct.new(:baz) {
def speaker
"Julian"
end
}
class Baz
def say(speaker, message)
puts "#{speaker} said '#{message}'"
end
end
foo = Foo.new(Bar.new(Baz.new))
チェーンの途中で取得したオブジェクトは保存されており、次のようにして後方から参照することができます。
  1. 文字列を使う場合
    #barの呼び出しで取得したオブジェクトを後方で参照
    chain = PullChain.new(foo) << "bar/baz/say(bar.speaker, 'Good!')"
    chain.call # => Julian said 'Good!'
    文字列を使う場合、取得したオブジェクトに名前をつけることもできます。
    次の例ではbarの呼び出し結果のオブジェクトに@bという名前をつけています。
    chain = PullChain.new(foo) << "@b = bar/baz/say(b.speaker, 'Cool')"
    chain.call # => Julian said 'Cool'
  2. 配列を使う場合
    Procを使用して、チェーン途中のオブジェクトにアクセスすることができます。
    chain = PullChain.new(foo) << :bar << :baz
    chain << [:say, Proc.new { next bar.speaker, "Oh" }]
    chain.call # => Julian said 'Oh'
    取得したオブジェクトを保存しているオブジェクトを介してアクセスすることもできます。
    chain = PullChain.new(foo) << :bar << :baz
    chain << [:say, lambda { |accessor| next accessor.bar.speaker, "Oh" }]
    chain.call # => Julian said 'Oh'

スラッシュで区切られた文字列中でスラッシュを使うには

次のようにエスケープして下さい。
chain = PullChain.new("AC") << "concat '\\/DC'"
chain.call # => AC/DC

return_nil_at_error= メソッドを使ってエラーを無視する

return_nil_at_error= メソッドにtrueをセットすると、
チェーンの途中でエラーが起きた場合に例外を投げるのではなく、nilを返すようになります。
chain = PullChain.new("Test") << :xxx
begin
chain.call # => undefined method `xxx'
rescue
end
chain.return_nil_at_error = true
chain.call # => nil

文字列のチェーン中で[]や<<などの演算子を使う場合の注意

例えば文字列のチェーンでHashオブジェクトから
[]演算子を使って値を取り出すには、self[:key]とする必要があります。
table = {name: %w(Bill Scott Paul)}
PullChain.new(table, "[:name]").call # => [:name] NG
PullChain.new(table, "self[:name]").call # => ["Bill", "Scott", "Paul"] OK
配列のチェーンでHashオブジェクトの[]演算子を使う場合は以下で問題ありません。
PullChain.new(table, [:[], [:name]]).call # OK
以下の演算子についても同様です
Stringオブジェクトの << 演算子
PullChain.new("Led", "<< ' Zeppelin'").call # NG syntax error
PullChain.new("Led", "self << ' Zeppelin'").call # => "Led Zeppelin"
Arrayオブジェクトの [] 演算子
PullChain.new(%w(Donald Walter), "[1]").call # NG => [1]
PullChain.new(%w(Donald Walter), "self[1]").call # OK => Walter
サポート対象外のクラス
Fixnum、Bignum、Floatのインスタンスに対しては文字列のチェーンを使えません。PullChain.new(999999999999999, "self % 2").call # NG
PullChain.new(999999999999999, [:%, [2]]).call # => 1 OK

RelayChain


RelayChainはメソッドの出力とメソッドの入力を繋ぐようなチェーンを表現するクラスです。
名前の由来はリレーレース。
メソッドがランナーで、入出力がバトンみたいだなと思ったのでこう名づけました。

使用例

class Decorator
def decorate1(value)
"( #{value} )"
end

def decorate2(value)
"{ #{value} }"
end
end

chain = RelayChain.new(Decorator.new, :decorate1, :decorate2)
chain.call("Hello") # => { ( Hello ) }
上記と同様の動作をするコード
  1. スラッシュ区切りの文字列を使う
    chain = RelayChain.new(Decorator.new, "decorate1/decorate2")
    chain.call("Hello")
  2. >> 演算子を使う
    chain = RelayChain.new(Decorator.new)
    chain >> :decorate1 >> :decorate2
    chain.call("Hello")
  3. メソッドオブジェクトを使う
    chain = RelayChain.new
    chain >> decorator.method(:decorate1) >> decorator.method(:decorate2)
    chain.call("Hello")
  4. addメソッドを使う
    chain.add(:decorate1).add(:decorate2).call("Hello")
  5. add_allメソッドを使う
    chain.add_all(:decorate1, :decorate2).call("Hello")
Insert, Delete, Clear
insert及びinsert_all メソッドは、メソッドをチェーンに挿入します。
delete_at メソッドは、指定した位置のメソッドをチェーンから削除します。
clear メソッドは、チェーンに追加されたすべてのメソッドを削除します。

異なるクラスのメソッド同士を繋げる

class Decorator
def decorate1(value)
"( #{value} )"
end

def decorate2(value)
"{ #{value} }"
end
end

class Decorator2
def decorate(value)
"[ #{value} ]"
end
end
上のように異なるクラスの2つのメソッドを繋ぐ方法は2通りあります。
  1. 文字列を使う場合
    次のようにadd_receiverメソッドを使います。chain = RelayChain.new(Decorator.new)

    # Decorator2のインスタンスにd2という名前をつけて登録
    chain.add_receiver("d2", Decorator2.new)

    # 登録したインスタンスは文字列中で使用可能
    chain >> "/decorate1/decorate2/d2.decorate"
    chain.call("Hello") # => [ { ( Hello ) } ]

    # add_receiver_tableメソッドを使えば、名前とインスタンスの組み合わせを
    # まとめて登録することができる。
    chain.add_receiver_table({"x" => X.new, "y" => Y.new})
  2. 配列を使う場合
    配列のフォーマットは [インスタンス, メソッドのシンボルかメソッド名の文字列]
    # メソッドのシンボルを使うバージョン
    chain = RelayChain.new(Decorator.new)
    chain >> :decorate1 >> :decorate2 >> [Decorator2.new, :decorate]
    chain.call("Hello") # => [ { ( Hello ) } ]

    # メソッド名の文字列を使うバージョン
    chain = RelayChain.new(Decorator.new)
    chain >> :decorate1 >> :decorate2 >> [Decorator2.new, "decorate"]
    chain.call("Hello") # => [ { ( Hello ) } ]

出力の数と入力の数が異なるメソッド同士を繋げる

次のクラスのメソッドを見て下さい。decorateの戻り値の数は1つで、unionの引数の数は2つです。
class Decorator
def decorate(value)
"#{value} And"
end

def union(value1, value2)
"#{value1} #{value2}"
end
end
このようなメソッド同士を繋げるには、2通りの方法があります。
  1. 接続するためのメソッドを定義する
    次の例では、引数の数が1つで、戻り値が2つのメソッド connectを定義し使用しています。
    class Decorator
    def connect(value)
    return value, "Palmer"
    end
    end

    chain = RelayChain.new(Decorator.new)
    chain >> :decorate >> :connect >> :union
    chain.call("Emerson, Lake") # => Emerson, Lake And Palmer
  2. 接続するためのProcかlambdaを定義する
    Procもしくはlambdaのフォーマット# parameter:chain チェーンオブジェクトです。
    #                 callメソッドを呼ぶことで次のメソッドを呼び出すことができます。
    # parameter:*args 前のメソッドの戻り値です。
    # *args_of_next_funcは次のメソッドに渡す引数です。
    lambda {|chain, *args| chain.call( *args_of_next_func ) }
    実際の使用例
    connect = lambda { |chain, value| chain.call(value, "Jerry") }

    chain = RelayChain.new(Decorator.new)
    chain >> :decorate >> connect >> :union
    chain.call("Tom") # => Tom And Jerry
    次のようにも書けます。chain = RelayChain.new(Decorator.new)
    chain.add(:decorate).add { |chain, value| chain.call(value, "Jerry") }
    chain.add(:union)
    chain.call("Tom") # => Tom And Jerry

おまけ

上の例のように、Procやlambdaを使用すると、
通常はチェーンオブジェクトにより自動で行われているメソッドの呼び出しをこちら側で制御することができます。
これを利用してチェーンの呼び出しを途中で止めてみます。class Decorator
def decorate1(value)
"( #{value} )"
end

def decorate2(value)
"{ #{value} }"
end
end

# 停止条件つきチェーンストッパーを生成
def create_stopper(&stop_condition)
lambda do |chain, value|
# 渡されたブロックを使って条件判定を行い、
# 条件を満たすならば次のメソッドは呼び出さず、前のメソッドの戻り値をそのまま返す。
if stop_condition.call(value)
value
else
chain.call(value)
end
end
end

chain = RelayChain.new(Decorator.new, :decorate1, :decorate2)

# チェーンストッパーを生成して挿入
# 停止条件は前のメソッドの戻り値が数値を含むかどうか
chain.insert(1, create_stopper { |value| value =~ /\d/ })

chain.call("Van Halen 1984") # => ( Van Halen 1984 )
chain.call("Van Halen Jump") # => { ( Van Halen Jump ) }
# 前者は条件を満たすためdecorate2が呼ばれず{}に囲まれていない。
# 後者は条件を満たさないためdecorate2が呼ばれ{}に囲まれている。
関連記事
スポンサーサイト

コメントの投稿

非公開コメント

プロフィール

Kenji Suzuki

Author:Kenji Suzuki
IT技術に関するあれこれを書いているブログです。
Pujohead Softの方では開発したソフトを公開しています。

最新記事
最新コメント
最新トラックバック
月別アーカイブ
カテゴリ
タグ

プログラミング デザインパターン コードリーディング bat 

検索フォーム
RSSリンクの表示
リンク
ブロとも申請フォーム

この人とブロともになる

QRコード
QR
メールフォーム

名前:
メール:
件名:
本文:

上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。