Freedom Patching の実用例(?)

Ruby東海第15回勉強会 2010/12/10

後藤@antimon2

おことわり

名古屋式ではありません。

自己紹介(1)

かための自己紹介

  • 名前:後藤 俊介 (GOTOH Shunsuke)
  • 所属:株式会社コスモルート STG
  • 肩書:プログラマ/Webエンジニア
  • やってること:JavaScript、ActionScript、ちょっとJava(、Rails3 ? )

Freedom Patching

って何だっけ?

  • モンキーパッチ(Monkey Patching)のこと。
  • 「今後はMonkey PatchingじゃなくてFreedom Patchingと呼ぼう」 by DHH
    (引用元:Matzにっき(2010-11-13))
  • つまり「考えなしにやっちゃったコード追加」の蔑称じゃなくて、
    「意図的なオープンクラスによる機能追加」の建設的な表現。

⇒有用なメソッドは、迷わず追加しちゃおう!

※ご利用は計画的に。

ということで

個人的に今まで書いてきた Freedom Patching の例を紹介します。

Integer#fact(1)

Integer クラスに階乗メソッドを追加する。

4.fact	# => 24 (== 1*2*3*4)
10.fact	# => 3628800
20.fact	# => 2432902008176640000
0.fact	# => 1
-1.fact	# => NaN
  • ※ 仕様:
    • self < 0NaN
    • self == 01
    • self > 01 * … * self

Integer#fact(2)

実装例

class Integer
  def fact
    self < 0 ? 0.0/0 : self <= 1 ? 1 : (1..self).inject(:*)
    # ↓ for Ruby 1.8.6 or less
    #self < 0 ? 0.0/0 : self <= 1 ? 1 : (1..self).inject{|r,n|r*n}
  end
end

Integer#fact(3)

※参考

関数的メソッドとしての実装例:

def fact num
  num < 0 ? 0.0/0 : num <= 1 ? 1 : (1..num).inject(:*)
  # ↓ for Ruby 1.8.6 or less
  #num < 0 ? 0.0/0 : num <= 1 ? 1 : (1..num).inject{|r,n|r*n}
  #num < 0 ? 0.0/0 : num <= 1 ? 1 : num * fact(num - 1)	# 再帰による実装例
end
  • × 引数チェックが必要(Integer以外の値を渡した場合の結果が…)。
  • △ 再帰を利用するならEnumerable#injectを利用した方が直感的で効率的。

Integer#sq?(1)

Integer クラスに平方数かどうかを返すメソッドを追加する。

4.sq?	# => true (== 2**2)
399999999999999960000000000000001.sq?	# => true (== 19999999999999999**2)
257.sq?	# => false
0.sq?	# => true
-1.sq?	# => false
  • ※ 仕様:
    • self < 0false
    • self == 0true
    • self > 0true(ある整数の2乗である場合)/ false(それ以外)

Integer#sq?(2)

実装例1(途中)

class Integer
  def sq?
    return false if self < 0
    Math.sqrt(self).round**2==self	# 問題あり!(※1)
  end
end

Integer#sq?(3)

実装例1(問題)

  • ※1 充分に小さい整数値(<2**53)ならこれでも問題ない。
    例:
    3999999999999996000000000000001.sq?	# => true
    # Math.sqrt(3999999999999996000000000000001).round	# => 1999999999999999
  • その範囲を超えると、桁落ちが発生し期待通りの結果にならない。
    例:
    399999999999999960000000000000001.sq?	# => false
    # Math.sqrt(399999999999999960000000000000001).round
    	# => 20000000000000000
  • ⇒ Floatに変換せず、整数のままで正確な平方根を返すメソッドが必要!
  • ⇒ そこで…

Integer#isqrt(1)

Integer クラスに整数範囲の平方根(>0)を返すメソッドを追加する。

4.isqrt	# => 2 (4 == 2**2)
399999999999999960000000000000001.isqrt	# => 19999999999999999
257.isqrt	# => 16 (257 == 16**2+1)
0.isqrt	# => 0
-1.isqrt	# => NaN
  • ※ 仕様:
    • self < 0NaN
    • self == 00
    • self > 0n**2 ≦ selfとなる最大の整数n

Integer#isqrt(2)

実装例

class Integer
  def isqrt
    return 0.0/0 if self < 0
    return self if self < 2
    return 1 if self < 4
    # === ニュートン法(整数範囲バージョン) ===
    n = self.div 2	# self / 2
    while n*n>self	# ※1
      on = n
      n = (n * n + self).div(2 * n)
      break if n == on
    end
    n
  end
end

Integer#isqrt(3)

解説

  • Math.sqrtに頼らず、ニュートン法で求根。
  • ※1 Rubyっぽくないけれど、効率を考えてwhileを使用。
  • ※ もっと効率を追求するなら、self < 2**53の範囲ならMath.sqrt(self).floorを返すようにした方が良いかも。

Integer#sq?(4)

実装例2(完成)

Integer#sq?を、先ほど実装したInteger#isqrtを利用して修正!

class Integer
  def sq?
    return false if self < 0
    #Math.sqrt(self).round**2==self	# 問題あり!
    # ↓
    self.isqrt**2==self
  end
end

他にはないの?

…じゃ、他の例も。

Enumerable#inject_map(1)

Enumerable モジュールに、injectメソッドとmapメソッドを組み合わせたようなメソッドinject_mapを追加する。

(1..10).inject(:+)	# => 55
(1..10).inject_map(:+)	# => [1, 3, 6, 10, 15, 21, 28, 36, 45, 55]
Math.sin 1	# => 0.8414709848078965
# ↓ sin(1)の近似を求める式
(1..10).inject(0.0){|r,n|r+(-1)**(n-1)/(2*n-1).fact.to_f}	# => 0.8414709848078965
# 前に定義した Integer#fact を使用。
# ↓ sin(1)の近似を途中経過付きで
(1..10).inject_map(0.0){|r,n|r+(-1)**(n-1)/(2*n-1).fact.to_f}
# => [1.0, 0.8333333333333334, 0.8416666666666667, 0.841468253968254, 0.8414710097001764, 
0.841470984648068, 0.8414709848086585, 0.8414709848078937, 0.8414709848078965, 0.8414709848078965]
  • ※ 仕様:
    • 書式:
      • inject_map(初期値){|累計, 要素| 演算}
      • inject_map{|累計, 要素| 演算}
      • inject_map(初期値, シンボル)
      • inject_map(シンボル)
    • 結果は配列で、n番目の要素は元のEnumerable要素のn番目までの要素に対するinjectメソッドの結果に一致

Enumerable#inject_map(2)

実装例

module Enumerable
  def inject_map(*args)
    if args.size > 0 && Symbol === args[-1]
      sym = args.pop
    end
    result = args[0]
    sym ? self.map do |item|
      result = (result == nil) ? item : result.__send__(sym, item)
    end : self.map do |item|
      result = (result == nil) ? item : yield(result, item)
    end
  end
end

※詳細略

Enumerable#inject for Ruby<=1.8.6 (1)

ついでだから、Ruby ver.1.8.6 以下でもシンボル(メソッド名)を受け取って処理できる inject メソッドを Enumerable モジュールに追加しちゃう。

RUBY_VERSION	# => "1.8.6"
(1..10).inject(:+)	# => 55
  • ※ 仕様:略

Enumerable#inject for Ruby<=1.8.6 (2)

実装例

module Enumerable
  if RUBY_VERSION < "1.8.7" && !(method_defined? :org_inject)
    alias :org_inject :inject
    def inject(*args, &proc)
      if args.size > 0 && Symbol === args[-1]
        sym = args.pop
        org_inject *args do |result, item|
          result.__send__(sym, item)
        end
      else
        org_inject *args, &proc
      end
    end
  end
end

※時間がないので詳細略

…で?

何の役に立つの?

色々使い道はあると思うよ。

…多分。

何かもっと…

他に 面白い 実用的な 例はないの?

※(ry

おしまい

ご清聴 ありがとうございます。

※このスライドで作成したRubyのソース: