RubyのRefinement(リファインメント)とレキシカルスコープを理解する
Refinement(リファインメント)を使ったことがない方にとっては、その挙動は意外に映るかもしれません。Refinementはモンキーパッチ(monkey patching)の置き換え手段として導入された機能なので、ActiveSupportのhoursメソッドのような拡張を実装できると期待するのが自然でしょう。ところが、次のコードを実行しても、期待どおりには動きません。
module TimeExtension
refine Integer do
def hours
self * 60 # 分に変換
end
end
end
class MyFramework
using TimeExtension
end
class MyApp < MyFramework
def index
1.hours
end
end
MyApp.new.index # => undefined method `hours' for 1:Integer (NoMethodError)
※ Ruby 2.4以降ではFixnumはIntegerに統合されているため、本記事のコード例ではIntegerを使用しています。
いったい何が起きているのでしょうか?
多くの優れたアイデアと同じように、Refinementの当初の構想も、厳しい現実に適応させるためにいくつかの調整を経ることになりました。先ほどのコードに見える不思議な挙動は、まさにその調整のひとつ――「Refinementはレキシカルスコープ(lexical scope)に限定される」というルール――が原因なのです。
レキシカルスコープとは何か
あるものが「レキシカル」であると言うとき、それはテキスト、すなわち画面上に書かれたコードそのものに関係するという意味です。コードが何を意味するかではなく、コードがどこに書かれているかが問題になります。
二つの行が同じレキシカルスコープにあるとは、端的に言えば、それらが同じコードブロック内に存在しているということです。そのブロックが実行時にどう評価されるかは一切関係ありません。
言葉で説明するよりも、例を見たほうがずっと分かりやすいでしょう。
class B
# x と y は同じレキシカルスコープを共有する
x = 1
y = 1
end
class B
# z は同じクラス内にあっても、x や y とは異なるレキシカルスコープに属する
z = 3
end
Refinementはレキシカルスコープに限定される
usingキーワードでRefinementを適用すると、そのRefinementが見えるのは同じレキシカルスコープの内部だけです。
具体的な例を見てみましょう。
module TimeExtension
refine Integer do
def hours
self * 60
end
end
end
class MyApp
using TimeExtension
def index
1.hours
end
end
class MyApp
def show
2.hours
end
end
MyApp.new.show # => undefined method `hours' for 2:Integer (NoMethodError)
indexメソッドとshowメソッドは、どちらも同じMyAppクラスの一部です。それにもかかわらず、Refinementにアクセスできるのはindexメソッドだけです。usingステートメントと同じレキシカルスコープ(同じコードブロック)を共有しているのが、index側の定義だけだからです。
これは少し奇妙に感じられるかもしれません。Rubyの他のほとんどの仕組みは動的な振る舞いをするからです。クラス定義の中でメソッドを追加すれば、その定義を抜け出した後もメソッドはクラスに残り続けます。しかしRefinementは、そのようには動作しないのです。
そしてこの仕様には、最初は気づきにくいいくつかの重要な帰結があります。
Refinementしたメソッドは動的に呼び出せない
sendメソッドは、usingステートメントを含むコードブロックの中で定義・実行されるわけではないため、Refinementを見ることができません。したがって、次のコードは動作しません。
class MyApp
using TimeExtension
def index
1.send(:hours) # => NoMethodError
end
end
Refinementメソッドの存在を確認できない
respond_to?もまた、同じコードブロック内には存在しないため、sendとまったく同じ理由で期待どおりに動作しません。
class MyApp
using TimeExtension
def index
1.respond_to?(:hours) # => false
end
end
補足しておくと、Refinementが有効になるのはusingを記述したファイルやクラス・モジュール本体のレキシカルスコープだけであり、evalで評価した文字列コードや、別ファイルから読み込んだコードへは自動的には適用されません。この「静的な範囲でのみ有効」という設計こそが、モンキーパッチのようにプログラム全体へ意図せず影響を及ぼしてしまうリスクを避けるための工夫なのです。
まとめ
本記事が、Refinementをめぐる混乱を少しでも解消する助けになれば幸いです。Refinementは間違いなくRubyの強力で有用な機能になり得ますが、初めて使う場合には「レキシカルスコープに限定される」という性質ゆえに、少し癖があることも事実です。モンキーパッチの安全な代替として活用するためにも、まずはこのスコープの仕組みをしっかり押さえておきましょう。
-
Rubyで学ぶ挿入ソート:仕組みから計算量まで徹底解説
※本記事は、Rubyでさまざまなソートアルゴリズムを実装するシリーズの第4回です。第1回ではバブルソート、第2回では選択ソート、第3回ではマージソートを取り上げました。データのソート手法をさまざまな角度から探っていくシリーズもいよいよ折り返し地点。今回は挿入ソート(Insertion Sort)に焦点を当てます。挿入ソートには魅力的な特徴がたくさんあります。まず、挿入ソートは安定(stable)なアルゴリズムです。つまり、同じキーを持つ要素同士の相対的な順序が入れ替わることがありません。また、インプレース(in-place)アルゴリズムでもあるため、ソート結果を保存するための新しい配列を作成す
-
Rubyのfreezeメソッド完全解説 – オブジェクトの可変性と不変性を理解しよう
オブジェクトが「変更可能(ミュータブル)」であるとは、どういう意味なのでしょうか? 難しい言葉に構える必要はありません。「可変性(ミュータビリティ)」とは、単純に「オブジェクトの内部状態を後から変更できる」という意味です。これはすべてのオブジェクトのデフォルトの挙動であり、freeze(凍結)されたオブジェクトや、言語側で特別扱いされている一部のオブジェクトだけが例外となります。 つまり、Rubyのすべてのオブジェクトが変更可能というわけではないのです。 なぜ数値やシンボルは変更できないのか? たとえば、整数・シンボル、さらにはtrueやfalse(これらもすべてオブジェクトです)が変化するの