(白抜きの)+と-

今日から第4章だ。
この章の関数名には、白抜きの+、-みたいな、ASCII にはない文字が出てくる。ここは、p.62 の脚注にしたがって、o+ とか o- と書くようにする。o はたぶん operator の o。

下準備

n に1と足す add1 と、n から1を引く sub1 を定義しておく。

(define add1
  (lambda (n)
    (+ n 1)))

(define sub1
  (lambda (n)
    (- n 1)))

(白抜きの)+

まずは、白抜きの+、すなわち o+。これは2つの数を引数にとって1つの数を返す関数で、要するに足し算だ。ヒントには、zoro?、add1 と sub1 を使おうと書いてある。ま、このくらいは簡単に書ける。

(define add1
  (lambda (n)
    (+ n 1)))

(define sub1
  (lambda (n)
    (- n 1)))

(define o+
  (lambda (n m)
    (cond
      ((zero? m) n)
      (else (add1 (o+ n (sub1 m)))))))

(print (o+ 1 3))
(print (o+ 10 5))

試してみよう。

^o^ > gosh add.scm
4
15

OKのようだ。

(白抜きの)-

次は、白抜きの-、すなわち o-。これは引き算だ。これも簡単。

(define add1
  (lambda (n)
    (+ n 1)))

(define sub1
  (lambda (n)
    (- n 1)))

(define o-
  (lambda (n m)
    (cond
      ((zero? m) n)
      (else (sub1 (o+ n (sub1 m)))))))

(print (o+ 3 1 ))
(print (o+ 10 5))

実行:

^o^ > gosh sub.scm
2
5

こっちもOK。

2つの関数に共通すること

o+ と o- の2つの関数に共通するのは、cond の条件が zero? と else であることと、(sub1 m) で再帰していることだ。3章でラットの再帰をしたとき、最終条件が null? だったのと違って、数の再帰では zero? が最終条件となる。また、cdr で再帰する代わりに sub1 で再帰している。ラットの場合は、空になるまでひとつずつ見ていくのに対して、数の場合には 0 になるまで 1 ずつ減らしていくのだな。

第1の戒律
(改訂版)
アトムのリスト lat を再帰するときは、2つの質問をすべし。すなわち、(null? lat) と else なり。
数 n を再帰するときは、2つの質問をすべし。すなわち (zero? n) と else なり。

標準出力に出力する

今まで Gauche のインタラクティブモードで作業をしてきたけど、そろそろコードを打ち込むのが大変になってきた。間違えたりすると最初から打ち直しだし。
なので、これからはスクリプトモードを使うことにする。スクリプトモードでは gosh コマンドの引数にスクリプト名を渡してやると、そのスクリプトを実行してくれる。スクリプトの拡張子は .scm だ。

で、そうすると結果を出力する必要が出てくるんだけど、ちょっと調べたところ、print 関数でいいみたいだ。
試してみよう。

(print "Hello, wold.")

実行:

^o^ > gosh hello.scm
Hello, wold.

multisubst

3章の最後は、multisubst だ。(multisubst new old lat) は lat の中のすべての old を new に置き換える。

(define multisubst
  (lambda (new old lat)
    (cond
      ((null? lat) (quote ()))
      ((eq? (car lat) old) (cons new (multisubst new old (cdr lat))))
      (else (cons (car lat) (multisubst new old (cdr lat)))))))

実行例:

gosh> (multisubst 'A 'a '(a b c a b c))
(A b c A b c)

うまくいった。

multiinsertRとmultiinsertL

multiinsertR

(multiinsertR new old lat) は lat の中のすべての old の右側に new を挿入する。

(define multiinsertR
  (lambda (new old lat)
    (cond
      ((null? lat) (quote ()))
      ((eq? (car lat) old)
        (cons old (cons new (multiinsertR new old (cdr lat)))))
      (else
        (cons (car lat) (multiinsertR new old (cdr lat)))))))

実行例:

gosh> (multiinsertR 'fried 'fish '(chips and fish or fish and chips))
(chips and fish fried or fish fried and chips)

multiinsertL

左側に挿入する multiinsertL。

(define multiinsertL
  (lambda (new old lat)
    (cond
      ((null? lat) (quote ()))
      ((eq? (car lat) old)
        (cons new (cons old (multiinsertL new old (cdr lat)))))
      (else
        (cons (car lat) (multiinsertL new old (cdr lat)))))))

実行例:

gosh> (multiinsertL 'fried 'fish '(chips and fish or fish and chips))
(chips and fried fish or fried fish and chips)

間違った再帰の例

ところで、本文には間違った再帰の例が載っている(p.58)。次の multiinsertL の定義は (eq? (car lat) old) のところの再帰の仕方が間違っている。10行目だ。

(define multiinsertL
  (lambda (new old lat)
    (cond
      ((null? lat) (quote ()))
      (else
        (cond
          ((eq? (car lat) old)
            (cons new
              (cons old
                (multiinsertL new old lat))))
          (else (cons (car lat)
            (multiinsertL new old (cdr lat)))))))))

正しくは lat じゃなくて (cdr lat) を使って再帰しなくちゃいけない。
試しに実行してみよう。

gosh> (multiinsertL 'fried 'fish '(chips and fish or fish and chips))
out of memory (32).  aborting...

メモリーエラーになった。lat で再帰していることで無限に再帰してしまっているのだな。

第4の戒律
(仮)
再帰のときは、常に少なくとも1つの引数を変えるべし。必ず終わりへと近づくこと間違いなし。変えた引数は最終条件で必ずテストすべし。すなわち、cdr を用いるときは、null? でテストすべし。

multirember

multirember はアトム a とラット lat を引数に取り、lat の中のすべての a を取り除く。

(define multirember
  (lambda (a lat)
    (cond
      ((null? lat) (quote ()))
      ((eq? (car lat) a) (multirember a (cdr lat)))
      (else (cons (car lat) (multirember a (cdr lat)))))))

ポイントは、5行目の (eq? (car lat) a) のときにも再帰しているところだな。
実行例:

gosh> (multirember 'cup '(coffee cup tea cup and hick cup))
(coffee tea and hick)

substとsubst2

続いて、subst と subst2。これは答えを見ちゃった。だって隣に書いてあるんだもの。
ただ、二重の cond は冗長だからひとつにまとめたものを書いた。

subst

(subst new old lat) は lat の中の最初の old を new に置き換える。

(define subst
  (lambda (new old lat)
    (cond
      ((null? lat) (quote ()))
      ((eq? (car lat) old) (cons new (cdr lat)))
      (else (cons (car lat) (subst new old (cdr lat)))))))

実行例:

gosh> (subst 'topping 'fudge '(ice cream with fudge for dessert))
(ice cream with topping for dessert)

subst2

(subst2 new o1 o2 lat) は lat の中の最初の o1 または o2 を new に置き換える。

(define subst2
  (lambda (new o1 o2 lat)
    (cond
      ((null? lat) (quote ()))
      ((or (eq? (car lat) o1) (eq? (car lat) o2)) (cons new (cdr lat)))
      (else (cons (car lat) (subst2 new o1 o2 (cdr lat)))))))

実行例:

gosh> (subst2 'vanilla 'chocolate 'banana '(banana ice cream with chocolate topp
ing))
(vanilla ice cream with chocolate topping)

insertRとinsertL

さあ、今日は3章の残りをやっつけるぞ。まずは insertR から。

insertR

insertR はアトム2つ new、old とラット(アトムのリスト)lat を引数に取り、lat の中の old と同じアトムの右側に new を挿入したラットを返す。
もうこのくらいは答えを見ないでも書ける。

(define insertR
  (lambda (new old lat)
    (cond
      ((null? lat) (quote ()))
      ((eq? (car lat) old) (cons old (cons new (cdr lat))))
      (else (cons old (insertR new old (cdr lat)))))))

実行例:

gosh> (insertR 'topping 'fudge '(ice cream with fudge for dessert))
(fudge fudge fudge fudge topping for dessert)

あれ、期待したのと違う。
わかった。間違いは最後の行だ。old を cons してどうする。cons するのは (car lat) だ。

(define insertR
  (lambda (new old lat)
    (cond
      ((null? lat) (quote ()))
      ((eq? (car lat) old) (cons old (cons new (cdr lat))))
      (else (cons (car lat) (insertR new old (cdr lat)))))))

実行例:

gosh> (insertR 'topping 'fudge '(ice cream with fudge for dessert))
(ice cream with fudge topping for dessert)
gosh> (insertR 'jalapeno 'and '(tacos tamales and salsa))
(tacos tamales and jalapeno salsa)

今度はOKだ。jalapeno はハラペーニョかな。

insertL

insertL は insertR と違って new を old の左側に挿入する。これは簡単、new と old の cons する順を入れ替えればいいだけだ。

(define insertL
  (lambda (new old lat)
    (cond
      ((null? lat) (quote ()))
      ((eq? (car lat) old) (cons new (cons old (cdr lat))))
      (else (cons (car lat) (insertL new old (cdr lat)))))))

実行例:

gosh> (insertL 'topping 'fudge '(ice cream with fudge for dessert))
(ice cream with topping fudge for dessert)
gosh> (insertL 'jalapeno 'and '(tacos tamales and salsa))
(tacos tamales jalapeno and salsa)

うまくいった(註:英語の意味は考えないこと)。
と思ったら、5行目の ((eq? (car lat) old) (cons new (cons old (cdr lat))))((eq? (car lat) old) (cons new lat)) とも書けるとのこと。なるほど、そのとおりだ。

firstをmapを使って書いてみた

書いてみたっていうか、内側のリストが空でないのが保証されているのなら、map (と car )を使うほうが素直に感じる。まあ、本(「Scheme手習い」)では順を追って説明していくのだろうけど。

(define first
  (lambda (l)
    (map car l)))

実行例:

gosh> (first '((a b) (c d) (e f)))
(a c e)
gosh> (first '())
()
gosh> (first '((five plums) (four) (eleven green oranges)))
(five four eleven)

もし、内側のリストに空リストが混じっているとうまく動かないはずだ。

gosh> (first '((a b) () (c d) (e f)))
*** ERROR: pair required, but got ()
Stack Trace:
_______________________________________

やっぱり。これは car が空リストにはエラーを起こすからだ。

first

関数 first は空のリストまたはリストのリスト(ただし内側のリストは空ではない)を引数に取り、内側の書くリストの最初のS式からなる新しいリストを返す。
これは答えを見ないで書いてみよう。

(define first
  (lambda (l)
    (cond
      ((null? l) (quote ()))
      (else (cons (car (car l)) (first (cdr l)))))))

さて、うまくいくかな?

gosh> (first '((a b) (c d) (e f)))
(a c e)
gosh> (first '())
()
gosh> (first '((five plums) (four) (eleven green oranges)))
(five four eleven)

うまくいった。

第3の戒律
リストを作らんとせしときは、最初の要素になるものを記述し、しかる後にそれを自然なる再帰に cons すべし。

[追記]
よく読み返したら、関数名が first じゃなくて firsts だった。ま、内容は変わらないからいいか。

rember

rember は remove member の略だそうだ。関数 rember はアトム a とリスト lat を引数に取って、lat の中の一番最初に現れる a と同じアトムを取り除いたリストを返す。
本文 p.35 には次のような定義が載っている。

(define rember
  (lambda (a lat)
    (cond
      ((null? lat) (quote ()))
      (else (cond
              ((eq? (car lat) a) (cdr lat))
              (else (rember a (cdr lat))))))))

試してみよう。

gosh> (rember 'bacon '(bacon lettuce and tomato))
(lettuce and tomato)

うまく動いたようだ。
だけど、次の例では期待どおりにはいかない。

gosh> (rember 'and '(bacon lettuce and tomato))
(tomato)

期待した結果は (bacon lettuce tomato) のはず。おかしいのは7行目の再帰の部分だ。再帰を使ってリスト lat の要素をひとつずつ見ていっていきながら a と同じアトムを探しているんだけど、7行目の (rember a (cdr lat)) は再帰するさいに a と同じでないアトム、つまり取り除くべきでないアトムを捨ててしまっている。
ということは、捨てるべきでないアトムを先頭にくっつけてやればいい。どうやるかっていうと、cons を使えばいい。修正したのがこれ。

(define rember
  (lambda (a lat)
    (cond
      ((null? lat) (quote ()))
      (else (cond
              ((eq? (car lat) a) (cdr lat))
              (else (cons (car lat) (rember a (cdr lat)))))))))

試してみよう。

gosh> (rember 'and '(bacon lettuce and tomato))
(bacon lettuce tomato)

今度は期待どおりに動いた。

第2の戒律
リストを作るには cons を用いるべし。

おまけ。cond は質問とそれに対応する値の組をいくつでもとれるので、それを使って次のように簡単化した定義が載っている(p.41)。

(define rember
  (lambda (a lat)
    (cond
      ((null? lat) (quote ()))
      ((eq? (car lat) a) (cdr lat))
      (else (cons (car lat) (rember a (cdr lat)))))))

実行例:

gosh> (rember 'sauce '(soy sauce and tomato sauce))
(soy and tomato sauce)