Ruby で,特定のクラスやモジュールを拡張する方法として refinements というものがある。Ruby 2.0 で試験的に導入され,Ruby 2.1 で正式に採用された。
一言で言えば,「拡張はするんだけど,その拡張はあるスコープ内でのみ効く。スコープ外では元のまま」を実現する仕組みだ。
ここでは,単位換算のために Numeric を拡張するということをやってみる。
動機
cairo というグラフィックライブラリーがある。PDF や PNG や EPS なんかを統一的なインターフェースで描画・生成することができる。
Ruby にはその名もずばり cairo
という gem があり,cairo の機能が簡単に使える。
このライブラリーで PDF を作るとき,寸法の単位は pt(ポイント)を使用しなければならない。
そのため,たとえば,A4 判(横 210 mm,縦 297 mm)の PDF ファイルを作ろうとすると,単位の換算の式を入れて,
require 'cairo'
width = 210 / 25.4 * 72 # ← ここで mm を pt に換算してる
height = 297 / 25.4 * 72
surface = Cairo::PDFSurface.new 'sample.pdf', width, height
context = Cairo::Context.new surface
# コンテキストに描画
surface.finish
と書くことになる。
(このスクリプトを実行すると白紙の 1 ページだけの PDF ファイルが実際に出来る)
※cairo の pt は「DTP ポイント」と呼ばれるやつで,1 pt = 1/72 inch。
でも,この単位換算が面倒くさい。あらゆる場所で mm を pt に換算しなければならない。
もちろん
def mm2pt(x)
x / 25.4 * 72
end
なんて換算メソッドを作ってもいいのだが,いっそのこと Numeric
を拡張して,
class Numeric
def mm
self / 25.4 * 72
end
end
みたいにしておけば,
width = 210.mm
height = 297.mm
なんて書けて,ちょっとクールじゃね?
(ちなみに 12.5.mm
みたく数値に小数点があっても大丈夫)
回転角などの角度も cairo
ではラジアンで指定しなきゃいけないんだが,
class Numeric
def deg
self * Math::PI / 180
end
end
と deg
を定義しておけば,
angle = 90.deg # 90度
なんて書けていいだろ。
ちょっと待て
いくら〈Ruby は組込みクラスもいじり放題〉だと言っても,これはちょっと乱暴じゃないか?
自分だけで使う小さなスクリプトならいいけど,スクリプトが大きく複雑になってくると,Numeric
みたいな基礎的なクラスにグローバルに手を入れるのは危なっかしい1。
ライブラリーなんかでは,絶対こんなことやってはいけない。
いや,そもそも,だ。この拡張によって 20.mm
が 20 ミリメートルを意味するのは,換算後の数値が pt 単位であることを前提にしているが,このことに一般性は無い。
つまり cairo じゃなく他の目的でなら,インチやメートルに換算したいかもしれないではないか。
ではどうすればいいのか。
refinements 参上
もしも,
-
20.mm
のような表記を使いたい場所でのみNumeric
を拡張し, - 他の場所では
Numeric
を無傷で残す
ことができるなら,問題は一応解決するだろう。
そのために用いるのが refinements という仕組み。
まず以下のようなモジュールを定義する。これは Numeric
クラスを拡張するためのモジュールだ。
module NumericExt
refine Numeric do
def mm
self * 72 / 25.4
end
def deg
self * Math::PI / 180
end
end
end
モジュール名の Ext
は extension(拡張)から名付けてみたが,好きに命名していい。
refine
はメソッドだ。引数として,拡張したいモジュールを与える。もちろんクラスはモジュールの一種だから,クラスを与えることもできる。
そして,ブロックの中でメソッドを定義する。いや,メソッドだけでなく定数などを定義したっていい。
しかし,これだけでは Numeric
には何の変化も起こらない。実際に拡張を行うには,
using NumericExt
と書く。すると,〈using NumericExt
したスコープ〉の内部でのみ Numeric
が拡張される。
スコープに注意すれば,十分に狭い範囲でのみ,このパワーアップした Numeric
が使えることになる。
スコープ
では,using
が有効となるスコープはどうなっているのか。
ローカル変数のスコープなんかよりはずっと広いようだ。
詳細は下記(英語)を見ていただくとして,ここでは少しだけ見てみよう。
using
したところ以降
当たり前っぽいが using
より前はスコープ外だ。
# ここでは無効
using NumericExt
# ここ以降は有効
これは,ローカル変数のスコープが,最初の代入以降であることと似ている。
モジュール定義の中
# 無効
module M
# 無効
using NumericExt
# 有効
end
# 無効
クラスもモジュールの一種なので,クラス定義内も同様だ。
注意したいのは,〈モジュール定義の中で using
してれば,そのモジュールの中では有効〉というわけ ではない ことだ。以下の例を見てほしい。
module M
using NumericExt
end
module M
# 無効
end
モジュールの中ではなく,あくまで モジュール定義の中 なんである。
有効なスコープ内のメソッド定義の中
using NumericExt
def foo
# 有効
end
モジュールの話と絡めると,以下のこともいえる。
module M
using NumericExt
def m
# 有効
end
end
module M
def m2
# 無効
end
end
おわりに
組込みクラスでも何でも拡張しちゃうぜ!という Ruby 的奔放さをスコープという箱庭に閉じ込め,柔軟性と保守性を両立する面白い仕組みだと思う。
みなさんの refinements の使い途を教えてください。
-
mathn ライブラリー(Ruby 2.2 で非推奨)を require して
/
の挙動が変って泣いた人いるよね? ね? ↩