※ Ruby2.0以上の話です。
ときにActiveRecord::Relationが便利なのは、実際にto_a
されるまでSQLが発行されないことですよね。SQLが発行されるまではいろいろな条件をインスタンス内に保持しておいてくれます。全件取得してインスタンス化してから絞り込む、なんてしていたら死んでしまいますからね。これ、無限に要素がある配列から特定条件の要素のみ10個取り出したい、というときでも似たようなことできませんかね?
Enumerable#lazyを使えばできるよ
redditのWhats you’re favorite ruby trick or quirk that most people don’t know about.というスレでも話題になっていたのですが、例えば
- API経由でとあるサイトの記事が取得できるとする。
- 記事はすっごくたくさんある。全部取得とかしたら死ねる。
- 条件付きで指定した数の記事だけ抜き出すようなメソッドが欲しい。
としたときに、「こんな風に書けばできるよ!」という例が紹介されていました。
def all_posts return to_enum(__callee__) unless block_given? while api.has_next_post? yield api.next_post end end all_posts.lazy.take_while{ |p| p.date <= 1.week.ago }.take(10) # => 一週間の間の記事が10件取得できる
to_enum(__callee__)
あたりが超トリッキーですね。__callee__
は現在のメソッド名(この例の場合は:all_posts
)をあらわし、to_enum
はEnumuratorクラスのインスタンスを返します。ここでto_enum(__callee__)
と、引数をつけて書くと、
- Enumerableで定義されているメソッド(mapとかselectとか)を呼び出したときに引数に指定されたメソッドを呼び出す。
- つまりイテレータがまわる度に上記の例だと
__callee__
つまりall_postsメソッドが呼び出される。 - イテレータがまわる度にall_postsが呼ばれるので、呼ばれる度に
yield api.next_post
が実行されることになる(こういうの、コルーチンと言うらしい)。api.has_next_post?
がfalseになると処理が止まる。
という感じになります。
ここで普通に一週間の間の記事を10件取得しようとして
all_posts.take_while{|post| post.date <= 1.week.ago}.take(10)
なんてしてしまうと全記事取得してしまって永久に処理が終わらないのですが、
all_posts.lazy.take_while{|post| post.date <= 1.week.ago}.take(10)
とすることで、全記事取得することなく遅延評価を行いつつ、一週間の間の記事を10件取得してくれるようになるのです。
確かにAPI呼び出しを条件付きで行うときにかなり便利なテクだなーと思いました。