引き続き「計算機プログラムの構造と解釈」を使って 今度はSchemeとRubyでのリストの操作を見ていこうと思います なおSchemeのコードは本書からの抜粋で 説明は自分の要約です

リスト要素の参照

Schemeにはデータオブジェクトの並びを表現する リストというデータ構造がある リストはlist手続きで作ることができるが これはconsを入れ子にしたものと等価である

list 1 2 3 4
 (1 2 3 4)
 
 (cons 1 (cons 2 (cons 3 (cons 4 nil))))
 (1 2 3 4)

だからconsを順にcdrダウンしていけば リストの各要素にアクセスできる これを使って リストのn番目の要素を返す手続きlist_refを定義する リストは0番から始まる

(define (list_ref items n)
 	(if (= n 0)
 		(car items)
 		(list_ref (cdr items) (- n 1))))
 
 (define squares (list 1 4 9 16 25))
 
 (list_ref squares 3)
 16

再帰を使ってリストをcdrダウンしていき nが0になったところでその第一要素をcarで返す

Rubyでも同様のことをやってみる まずconsを定義しこれを使ってリストを定義する consの定義にはRubyのArrayクラスを使うのがよさそうだ

def cons(a, b=nil)
   [a, b]
 end
 
 def car(items)
   case items
   when Array
     items[0]
   else
     raise "bad argument type"
   end
 end
 
 def cdr(items)
   case items
   when Array
     items[1]
   else
     raise "bad argument type"
   end
 end
 
 def list(*i)
   if i.empty?
     nil
   else
     cons i.shift, list(*i)
   end
 end
 
 odds = list 1, 3, 5, 7 # => [1, [3, [5, [7, nil]]]]

consに基づいてリストが定義できれば schemeと同じアルゴリズムで list_refが定義できるはずだ

def list_ref(items, n)
   if n == 0
     car items
   else
     list_ref(cdr(items), n-1)
   end
 end
 
 list_ref(odds, 3) # => 7

リストの長さ

schemeに戻って リストの長さ(要素数)を返す手続きlengthを定義する

(define (length items)
 	(if (null? items)
 		0
 		(+ 1 (length (cdr items)))))
 		
 (define odds (list 1 3 5 7 9))		
 (length odds)
 5

cdrダウンするたびに1カウントして 第二要素がnilになったところで終了する

Rubyでもlengthを定義しよう

def length(items)
   if items.nil?
     0
   else
     1 + length(cdr items)
   end
 end
 
 length(odds) # => 4

リストの結合

次に2つのリストを結合する手続きappendを定義する

(define (append list1 list2)
 	(if (null? list1)
 		list2
 		(cons (car list1) (append (cdr list1) list2))))
 
 (append squares odds)
 (1 4 9 16 25 1 3 5 7 9)

list1をcdrダウンしていくたびに list1をcarして新たなpairをconsする list1が空に達したらlist2を繋ぐ

Rubyでもappendを定義しよう

def append(list1, list2)
   if list1.nil?
     list2
   else
     cons car(list1), append(cdr(list1), list2)
   end
 end
 
 squares = list 1, 4, 9, 16, 25
 append(odds, squares) # => [1, [3, [5, [7, [1, [4, [9, [16, [25, nil]]]]]]]]]

リスト要素に手続きを作用させる

今度はリストの各要素に 任意の手続きを作用させたリストを返す手続きmapを定義する

(define (map proc items)
 	(if (null? items)
 		`()
 		(cons (proc (car items))
 			(map proc (cdr items)))))

map手続きにはリストと共に手続きprocを渡し リストをcdrダウンしていくたびに リストの第一要素にprocを作用させた結果を consして新たな要素を作る

mapに渡す演算を変えることによって 多様な結果が得られる

(map abs (list -10 2.5 -11.6 17))
 (10 2.5 11.6 17)
 
 (map (lambda (x) (* x x))
 		(list 1 2 3 4))
 (1 4 9 16)

これを使って新たな演算を定義してもいい

(define (scale_list items)
 	(map (lambda (x) (* x x))
 		 items))
 (square_list (list 1 2 3 4))
 (1 4 9 16)

次にRubyでもmapを定義しよう

def map(proc, items)
   if items.nil?
     nil
   else
     cons proc.call(car items), map(proc, cdr(items))
   end
 end
 
 map(lambda { |x| x.abs }, list(-10, 2.5, -11.6, 17)) # => [10, [2.5, [11.6, [17, nil]]]]
 
 def scale_list(items)
   map(lambda { |x| x**2 }, items)
 end
 
 scale_list odds # => [1, [9, [25, [49, nil]]]]

treeに対する演算

並びはその要素自身が並びである階層構造を許す

(cons (list 1 2) (list 3 4))
 ((1 2) 3 4)

これは再帰的に枝分かれしていくtreeの構造を表現する ここで先のlength手続きを適用した場合 先端の葉の部分までは数え上げてくれない

(define tree (cons (list 1 2) (list 3 4)))
 
 (length tree)
 3

葉を数える手続きcount_leavesを定義しよう

(define (count_leaves tree)
 	(cond ((null? tree) 0)
 		  ((not (pair? tree)) 1)
 		  (else (+ (count_leaves (car tree))
 				   (count_leaves (cdr tree))))))
 
 (count_leaves tree)
 4
 
 ;length手続き
 (define (length items)
 	(if (null? items)
 		0
 		(+ 1 (length (cdr items)))))

length手続きと比べると分かるが treeをcdrダウンするときに 1を足す代わりにtreeをcarダウンして 葉に至ったら1を足すようにする これは要素がpairでないことで判定できる

Rubyでもcount_leavesを定義しよう その前にpair?メソッドを定義する

def pair?(items)
   case items
   when Array
     true
   else
     false
   end
 end
 
 def count_leaves(x)
   case 
   when x.nil? then 0
   when !pair?(x) then 1
   else
     count_leaves(car x) + count_leaves(cdr x)
   end
 end
 
 count_leaves(x) # => 7

うまくいった

schemeに戻ってcount_leaves同様に 先のscale_listもtreeに対応させよう

(define (scale_tree tree factor)
 	(cond ((null? tree) `())
 		  ((not (pair? tree)) (* tree factor))
 		  (else (cons (scale_tree (car tree) factor)
 					  (scale_tree (cdr tree) factor)))))
 					
 (scale_tree (list 1 (list 2 (list 3 4) 5) (list 6 7))
 			   10)
 (10 (20 (30 40) 50) (60 70))
 
 ;scale_list手続き
 (define (scale_list items factor)
 	(if (null? items)
 		`()
 		(cons (* (car items) factor)
 			  (scale_list (cdr items) factor))))

scale_listとの違いはcdrダウンするときに listの第一要素とfactorを直ぐに掛けず carダウンして葉に至ったところでfactorを作用させる点である

Rubyでも同じようにやってみよう

def scale_tree(tree, factor)
   case 
   when tree.nil? then nil
   when !pair?(tree) then tree * factor
   else
     cons scale_tree(car(tree), factor), scale_tree(cdr(tree), factor)
   end
 end
 l = list(1, (list 2, (list 3, 4), 5), (list 6, 7)) # => [1, [[2, [[3, [4, nil]], [5, nil]]], [[6, [7, nil]], nil]]]
 scale_tree l, 10 # => [10, [[20, [[30, [40, nil]], [50, nil]]], [[60, [70, nil]], nil]]]

Listクラスを定義する

Rubyではリストをオブジェクトしたバージョンも書いてみる Arrayクラスを継承してListクラスを定義する

class List < Array
   def list_ref(n)
     self[n]
   end
   
   def append(list)
     self + list
   end
   
   def last_pair
     self[-1]
   end
   
   def scale_list(n)
     self.inject([]) { |arr, e| arr << e * n  }
   end
   
   def map
     self.inject([]) { |arr, e| arr << yield(e) }
   end
   
   def count_leaves
     self.inject(0) do |len, e|
       case e
       when List
         len + e.count_leaves
       else
         len + 1
       end
     end
   end
   
   def map_tree
     self.inject([]) do |arr, e|
       case e
       when List
         arr << e.map_tree{ |x| yield(x) }
       else
         arr << yield(e)
       end
     end
   end
 end
 
 odds = List[1, 3, 5, 7] # => [1, 3, 5, 7]
 
 odds.list_ref(2) # => 5
 
 odds.length # => 4
 
 squares = List[1, 4, 9, 16, 25]
 
 odds + squares # => [1, 3, 5, 7, 1, 4, 9, 16, 25]
 odds.append(squares) # => [1, 3, 5, 7, 1, 4, 9, 16, 25]
 
 odds.last_pair # => 7
 odds.scale_list(5) # => [5, 15, 25, 35]
 
 odds.map { |e| e**2 } # => [1, 9, 25, 49]
 
 tree = List[1, List[2, List[3, 4], 5], List[6, 7]] # => [1, [2, [3, 4], 5], [6, 7]]
 
 tree.count_leaves # => 7
 tree.map_tree { |x| x**2 } # => [1, [4, [9, 16], 25], [36, 49]]

injectを使うのはちょっとずるっぽいけど まあいいか

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

ジェラルド・ジェイ サスマン, ジュリー サスマン, and ハロルド エイブルソン



blog comments powered by Disqus
ruby_pack8

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