Ruby でブロックを遅延評価するための gem を作った

Ruby で初めて gem をつくりましたので紹介。
まぁつくったといっても2ヶ月ぐらい前にリリースしたんですが。
ちなみに最初に言い訳しておくと Ruby 歴1ヶ月ぐらいではじめて書いた gem なのでお作法的によろしく無い部分があると思いますが、お察しください。
自分でいうのもなんですが、かなり便利です。

開発経緯

当たり前ですが Ruby を書いている時にはブロックをよく使います。
その中には

["homu", "mami", "mado"].map { |it|
    it.capitalize
}

["homu", "mami", "mado"].select{ |it|
    it =~ /^m/
}

[{ name: "homu" }, { name: "mami" }, { name: "mado" }].map { |it|
    it[:name]
}

のように処理をブロックで定義する事がよくあると思います。
しかし、こういう単純な処理をいちいちブロックで書くのは地味に手間です。
例えば、1つ目の処理は、

["homu", "mami", "mado"].map &:capitalize

のように Symbol#to_proc を利用して簡略化することも可能です。
ですが、他のブロックはこれ以上簡略することはできません。


そ こ で ! こういう1行で書けるようなブロックをより単純に、より直感的に定義することができる gem を書きました。

gem-iolite

インストール

Add this line to your application's Gemfile:

gem 'iolite'

And then execute:

$ bundle

Or install it yourself as:

$ gem install iolite

使い方

require "iolite"

include Iolite::Placeholders


# プレースホルダ(arg1)が第一引数に置き換わり
# #capitalize メソッドが呼び出される
p ["homu", "mami", "mado"].map &arg1.capitalize
# => ["Homu", "Mami", "Mado"]


# プレースホルダに対して、演算子も使用できる
p ["homu", "mami", "mado"].select &arg1 =~ /^m/
# => ["mami", "mado"]

[{ name: "homu" }, { name: "mami" }, { name: "mado" }].map &arg1[:name]
# => ["homu", "mami", "mado"]


# 第二引数を参照したい場合は arg2 を使う
p (1..5).inject &arg1 + arg2
# => 15


# メソッドに対して、引数を渡すこともできる
["homu", "mami", "mado"].map &arg1.index("m")
# => [2, 0, 0]


# 自身を引数にして渡すこともできる
["homu", "mami", "mado"].map &arg1.concat(arg1 + ".")
# => ["homuhomu.", "mamimami.", "madomado."]

だいたいどんな感じのライブラリなのかは上記のコードを見るとわかると思います。
iolite ではプレースホルダに対してメソッド演算子を呼び出し、それをブロックに渡すことでその処理が遅延評価されれます。
これにより演算子を使ったりメソッドを呼び出すような単純な処理であれば、ブロックよりも簡潔に記述する事ができます。

オブジェクトを遅延評価

iolite にはオブジェクト自体を遅延評価させるためのモンキーパッチもあります。

require "iolite"

# Object#to_lazy, #to_l を定義
require "iolite/adaptored/object_with_to_lazy"

include Iolite::Placeholders


str = "saya."

# 引数ではなくてローカルスコープのオブジェクトに対して操作したい
# ["homu", "mami", "mado"].each { |it|
#  str.concat(it + ",")
# }


# Object#to_lazy を呼び出すことで、そのオブジェクトが遅延評価されるようになる
str.to_lazy.length.call()
# => 5


# 上記のようなブロックの場合、#to_lazy を利用して記述することできる
# #to_lazy で呼び出したオブジェクトのメソッドに対してプレースホルダを渡すことで、
# そのプレースホルダの引数に置き換えて評価される
["homu", "mami", "mado"].each &str.to_lazy.concat(arg1 + ".")
# ["homu", "mami", "mado"].each { |it| str.concat(it + ".") }
# => ["homu", "mami", "mado"]


# これは Kernel のメソッドを呼び出したい場合にも利用できる
["homu", "mami", "mado"].each &to_lazy.puts(arg1)
# ["homu", "mami", "mado"].each { |it| puts it }

Object#to_lazy を呼び出すことで、以降に呼び出したメソッドが遅延評価されるようになります。
また、遅延評価するメソッドの引数にはプレースホルダを渡すこともできます。

実装

これ、どうやって実装しているのかって言うと割と単純でプレースホルダ#method_missing を定義して、遅延評価したいメソッドが呼び出される度に遅延評価するオブジェクトを返すようになっています。
実装が気になる方はここら辺 に簡易実装のコードがあるので読んでみるとよいと思います。

注意点

プレースホルダを使用する場合、必ず左辺に定義する必要があります。

# OK
["homu", "mami", "mado"].map &arg1 + "."

# Error
["homu", "mami", "mado"].map &"." + arg1

これは、遅延評価するオブジェクトがプレースホルダ#method_missing から生成されるためです。
なので必ず最初にプレースホルダメソッドを呼び出す必要が出てきます。
ちなみに #to_lazy を使用すれば左辺にもプレースホルダ以外のオブジェクトを定義することはできます。

# OK
["homu", "mami", "mado"].map &".".to_lazy + arg1

所感

さて、知ってる人はもうわかっていると思いますが、iolite は Boost.Lambda/Boost.PhoenixRuby に落とし込んだ形のライブラリになります。
プレースホルダを使用して式を定義する書き方は Boost と全く同じですね。
Boost.Lambda も元々はラムダ式を簡潔に記述する手段だったので、それが利用できないかと思いこんな感じのライブラリになりました。
実装コードがアレなのはともかくとして使い勝手としてはだいぶいい感じなんじゃないかと思います。
はじめて Ruby でがっつりとコード書いたんですが、メタプログラミングが思いの外楽しかったです。
#method_missing がやばい。

ちなみに似たような手段のライブラリとして lambda_driver があります。

実際のところ、iolite と lambda_driver では目的がちょっと違うと思うんですが、両方を比較してみると面白いと思います。

と、いう感じでざっくりと簡単に書いてみました。 もう少し詳しい使い方を知りたい方はここら辺を読んでみるとよいと思います。