Help us understand the problem. What is going on with this article?

Ruby の refinements の使い途

More than 1 year has passed since last update.

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 が使えることになる。

http://docs.ruby-lang.org/ja/2.2.0/class/Module.html#I_REFINE

スコープ

では,using が有効となるスコープはどうなっているのか。

ローカル変数のスコープなんかよりはずっと広いようだ。

詳細は下記(英語)を見ていただくとして,ここでは少しだけ見てみよう。

http://docs.ruby-lang.org/en/trunk/syntax/refinements_rdoc.html#label-Scope

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 の使い途を教えてください。


  1. mathn ライブラリー(Ruby 2.2 で非推奨)を require して / の挙動が変って泣いた人いるよね? ね? 

scivola
主に Ruby 使ってます。 二十年来のコンパイラー恐怖症が Rust で治癒するか?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away