SchemeとRubyでデータ抽象を学ぼう

前回に引き続き「[rakuten:book:10825992:title]」を使って
今度はSchemeRubyにおける
データ抽象の違いを見ていこうと思います
なおSchemeのコードは本書からの抜粋で
説明は自分の要約です

有理数演算手続き

有理数に対する演算(例えばadd_rat)を考えるとき
分子と分母の数値を個別で取り扱う手続きを考えるよりも
分子と分母を対とした一つの有理数を対象に
手続きを考えられたら楽である


Schemeでは合成データを使って有理数を表現し
この抽象データに対しての演算手続きを表現することで
データ抽象を実現する


Scheme有理数に対する算術演算
add_rat, sub_rat, mul_rat, div_rat, equal_rat?を考える
有理数に対する演算式は次の通りである


\frac{n1}{d1} + \frac{n2}{d2} = \frac{n1d2 + n2d1}{d1d2}
\frac{n1}{d1} - \frac{n2}{d2} = \frac{n1d2 - n2d1}{d1d2}
\frac{n1}{d1} * \frac{n2}{d2} = \frac{n1n2}{d1d2}
\frac{n1/d1}{n2/d2} = \frac{n1d2}{d1n2}
\frac{n1}{d1} = \frac{n2}{d2} のときに限り n1d2 = n2d1


整数nと整数dを取って
分子がn分母がdの有理数を返す手続きをmake_ratとし
make_ratで作られた有理数の分子を返す手続きをnumer
分母を返す手続きをdenomとした場合
Schemeによる上の演算表現は以下のようになる

 (define (add_rat x y)
        (make_rat (+ (* (numer x) (denom y))
                                (* (numer y) (denom x)))
                           (* (denom x) (denom y))))
 
 (define (sub_rat x y)
        (make_rat (- (* (numer x) (denom y))
                               (* (numer y) (denom x)))
                           (* (denom x) (denom y))))
 
 (define (mul_rat x y)
       (make_rat (* (numer x) (numer y))
                          (* (denom x) (denom y))))
 
 (define (div_rat x y)
       (make_rat (* (numer x) (denom y))
                          (* (denom x) (numer y))))
 
 (define (equal_rat? x y)
       (= (* (numer x) (denom y))
            (* (numer y) (denom x))))


これらの演算をRubyで表現してみる

 def add_rat(x, y)
   make_rat numer(x) * denom(y) + numer(y) * denom(x), 
            denom(x) * denom(y)
 end
 
 def sub_rat(x, y)
   make_rat numer(x) * denom(y) - numer(y) * denom(x),
            denom(x) * denom(y)
 end
 
 def mul_rat(x, y)
   make_rat numer(x) * numer(y),
            denom(x) * denom(y)
 end
 
 def div_rat(x, y)
   make_rat numer(x) * denom(y),
            denom(x) * numer(y)
 end
 
 def equal_rat?(x, y)
   numer(x) * denom(y) == numer(y) * denom(x)
 end

有理数のデータ表現

Schemeに戻ろう
次に有理数を表現するために
手続きconsで構成される対を使う
consは2つの引数を取り
これらを部分として含む合成データオブジェクトを返す
合成データオブジェクトの部分は手続きcarとcdrで取り出せる

 (define x (cons 1 2))
 (car x)
 1
 (cdr x)
 2


これらを使って有理数を表現する

 (define (make_rat n d) (cons n d))
 (define (numer x) (car x))
 (define (denom x) (cdr x))

また結果を表示する手続きを加える

 (define (print_rat x)
 	 (newline)
 	(display (numer x))
 	(display "/")
 	(display (denom x)))

これで有理数演算ができるようになった

 (define one_half (make_rat 1 2))
 
 (print_rat one_half)
 1/2
 (define one_third (make_rat 1 3))
 
 (print_rat (add_rat one_half one_third))
 5/6
 (print_rat (mul_rat one_half one_third))
 1/6
 (print_rat (add_rat one_third one_third))
 6/9

なお最後の例を見るとわかるが
先の手続きは簡約まではしない
最大公約数gcdを使ってこれを改善する

 (define (make_rat n d)
 	(let ((g (gcd n d)))
 	 (cons (/ n g) (/ d g))))


さてRubyでも有理数を表現してみる
Rubyでは配列を使うのがよさそうだ

 reqire 'rational'
 def make_rat(n, d)
   g = n.gcd(d)
   [n/g, d/g]
 end
 
 def numer(x)
   x[0]
 end
 
 def denom(x)
   x[1]
 end
 
 def print_rat(x)
   puts "#{numer x}/#{denom x}"
 end

gcdを使うのにrationalライブラリをrequireする
演算結果は以下の通り

 def one_half
   make_rat 1, 2
 end
 print_rat one_half
 
 def one_third
   make_rat 1, 3
 end
 print_rat one_third
 
 print_rat add_rat(one_half, one_third)
 
 print_rat mul_rat(one_half, one_third)
 
 print_rat add_rat(one_third, one_third)
 # >> 1/2
 # >> 1/3
 # >> 5/6
 # >> 1/6
 # >> 2/3

クラスによるデータ抽象

でもこれは実にRubyっぽくない
Rubyではデータ抽象にクラスを使うのがよさそうだ
有理数クラスRatを定義してみる

 require "rational"
 class Rat
   attr_reader :numer, :denom
   def initialize(n, d)
     g = n.gcd d
     @numer, @denom = n/g, d/g
   end
   
   def +(other)
    Rat.new(self.numer * self.denom + other.numer * other.denom,
             self.denom * other.denom)
   end
   
   def -(other)
    Rat.new(self.numer * other.denom - other.numer * self.denom,
             self.denom * other.denom)
   end
   
   def *(other)
     Rat.new(self.numer * other.numer,
             self.denom * other.denom)
   end
   
   def /(other)
     Rat.new(self.numer * other.denom,
             self.denom * other.numer)
   end
   
   def ==(other)
     self.numer * other.denom == other.numer * self.denom
   end
   
   def to_s
     "#{self.numer}/#{self.denom}"
   end
 end
 
 one_half = Rat.new(1, 2) # => #<Rat:0x140dc @numer=1, @denom=2>
 one_third = Rat.new(1, 3) # =>#<Rat:0x13ce0 @numer=1, @denom=3>
 
 one_third.denom # => 3
 
 one_half.to_s # => "1/2"
 (one_third + one_third).to_s # => "2/3"
 (one_half * one_third).to_s # => "1/6"
 (one_half / one_third).to_s # => "3/2"
 one_half == one_third # => false

newで渡した引数を分子分母とする
有理数クラスのインスタンスを生成する
分子分母にはnumer、denomメソッドでアクセスできる
各算術演算は整数と同じ記号を使え
算術の結果は有理数クラスのインスタンスで返される


もちろんRubyには標準でRationalクラスがある

 one_half = Rational(1, 2)
 one_third = Rational(1, 3)
 one_half * one_third # => Rational(1, 6)
 one_half /one_third # => Rational(3, 2)


[rakuten:book:10825992:detail]
(追記:2009/2/5) タイトルを「SchemeRubyのデータ抽象を学ぼう」から「SchemeRubyでデータ抽象を学ぼう」に変えました