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
- osyo-manga/gem-iolite
- v 0.0.3
インストール
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.Phoenix を Ruby に落とし込んだ形のライブラリになります。
プレースホルダを使用して式を定義する書き方は Boost と全く同じですね。
Boost.Lambda も元々はラムダ式を簡潔に記述する手段だったので、それが利用できないかと思いこんな感じのライブラリになりました。
実装コードがアレなのはともかくとして使い勝手としてはだいぶいい感じなんじゃないかと思います。
はじめて Ruby でがっつりとコード書いたんですが、メタプログラミングが思いの外楽しかったです。
#method_missing
がやばい。
ちなみに似たような手段のライブラリとして lambda_driver があります。
実際のところ、iolite と lambda_driver では目的がちょっと違うと思うんですが、両方を比較してみると面白いと思います。
と、いう感じでざっくりと簡単に書いてみました。
もう少し詳しい使い方を知りたい方はここら辺を読んでみるとよいと思います。