前回に引き続き「計算機プログラムの構造と解釈」を使って 今度はSchemeとRubyにおける データ抽象の違いを見ていこうと思います なおSchemeのコードは本書からの抜粋で 説明は自分の要約です

有理数演算手続き

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

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

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

Alt expression

整数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)

計算機プログラムの構造と解釈

(追記:2009/2/5) タイトルを「SchemeでRubyのデータ抽象を学ぼう」から「SchemeとRubyでデータ抽象を学ぼう」に変えました



blog comments powered by Disqus
ruby_pack8

100円〜で好評発売中!
M'ELBORNE BOOKS