読者です 読者をやめる 読者になる 読者になる

デバッグに便利そうなgem LazyMethodを書いた

https://bugs.ruby-lang.org をなんとなく眺めていたら、とあるチケットが目につきました。

Feature #12125: Proposal: Shorthand operator for Object#method - Ruby trunk - Ruby Issue Tracking System

このチケットは、「Method classは便利だけど記述がめんどくさいから新しい記法作ろうぜ」的なものです。

Method classはデバッグやblockとして使うのに便利ですよね。

foo.method(:bar).arity #=> 1
foo.method(:bar).source_location #=> ["file.rb", 25]
Dir["*/*.rb"].map(&File.method(:basename)) #=> ["file.rb"]

Method classの欠点

僕が考えるMethod class唯一の欠点は「インスタンスを作るときにメソッド前後に何かを書かなきゃいけない」だと思います。

# bar methodはどこで定義されてるんだろう?
foo.bar

# 前後にコードを書いて
foo.method(:bar).source_location #=> ["file.rb", "25"]

# わかったから前後のコードを削除
foo.bar

サッと知りたいだけのとき、これは不便です。

methodsなら最後だけ、tapなら好きな場所に一箇所書くだけなので書きやすいですよね。

foo.methods #=> 後ろに付け足すだけ
foo.tap { |i| p i}.bar #=> 真ん中に書くだけ 

解決案

そこで考えた解決案として、「前後ではなく、前か後ろ片方だけなら楽なのでは」というものでした。

# あとに書く
foo.bar.method.source_location

# まえに書く
foo.method.source_location.bar

あとに書くのは文法的に苦しそうなので、まえに書く方法を提案してみたのが先ほどのチケットのコメントというわけです。

これなら新しい文法を追加することなく、前後へのコードの追加から前だけのコードの追加になるので、 ある程度手間を軽減できそうです。

LazyMethod

どうもGemを書くのが趣味みたいなところがあるので、これをGem化したのがLazyMethodです。

github.com

名前は、「まだメソッド名を決めてないからUnboundMethodにあやかって、UndecidedMethod?UnnamedMethod?UnfoundMethod?かな?でもなんか地味でイケてる感じがない……。」と思いましたが、 呼び方にEnumerable#lazyっぽい気がなんとなく感じ取れたので「LazyMethod」としました。

使い方は、Kernel#methodメソッドの呼び方をfoo.method(:bar)からfoo.method.barとするだけです。 引数をつけると、通常のKernel#methodメソッドの動作になります。

Object.instance_methodsに含まれるような基本的なメソッドはLazyMethodと相性が悪いです。 foo.method.to_sのように使うと、文字列がほしいのかto_sメソッドの情報がほしいのかで判断が別れるのですが、 こういう基本的なメソッドを書き換えると混乱を生んだりデバッグしにくかったりと、Methodオブジェクトとして使えてもあまり嬉しい要素がないので、 通常のRubyっぽい動きに舵を切りました。

また、source_locationparametersなどのMethod classのメソッドは使われることが多いでしょう。 ところがfoo.method.bar.source_locationのように書いては元の問題が解決できないので、 foo.method.source_location.barのように遅延的に呼び出すことができるようにしました。

require 'lazy_method'

foo.method #=> #<LazyMethod foo>
foo.method(:bar) #=> #<Method foo:bar>
foo.method.bar #=> #<Method foo:bar>
foo.method(:bar).source_location #=> ["file.rb", 25]
foo.method.bar.source_location #=> ["file.rb", 25]
foo.method.source_location #=> #<LazyMethod foo(source_location>
foo.method.source_location.bar #=> ["file.rb", 25]
foo.method.source_location.baz #=> ["file.rb", 29]
Dir["*/*.rb"].map(&File.method.basename) #=> ["file.rb"]

なんとなく便利っぽい気がしてきませんかね。僕はまだわかりません。