モジュールの作り方と使い方

さて、本を読み進めるうちに書くコードも長くなってきた。たとえば前のエントリで書いた関数 o= は32行ある。だけど、本質的な部分はテスト用コードと空行を含めても11行だ。残りは、o= を作るための補助的というか基本的な関数のコードで占められている。こういうコードは冗長なだけでなく、コードを見難くする。

というわけで、基本的な関数はモジュールにして外に追いやりたいと思ったので、Gauche でのやり方を調べてみた。

 cf. 4.11 モジュール ― Gauche ユーザリファレンス
 cf. 3.1 Gaucheを起動する ― Gauche ユーザリファレンス

簡単な作り方と使い方をまとめると、次のようになる。

  1. define-module を使ってモジュールを定義し、中に関数の定義を書く。
  2. 上記の中で export-all を使って、定義した関数をエクスポートしておく。
  3. 適当なファイル名で保存する。
  4. モジュールを使う側のファイルで、(use モジュール名) とする。
  5. gosh を起動するときに -I. オプションをつける。”.” はモジュールがカレントディレクトリにある場合。

で、今まで書いた基本的な関数をまとめたモジュールがこれ。モジュール名は mymodule とした。

(define-module mymodule

(define atom?
  (lambda (x)
    (and (not (pair? x)) (not (null? x)))))

(define lat?
  (lambda (l)
    (cond
      ((null? l) #t)
      ((atom? (car l)) (lat? (cdr l)))
      (else #f))))

(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)))))))

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

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

(define o>
  (lambda (n m)
    (cond
      ((zero? n) #f)
      ((zero? m) #t)
      (else (o> (sub1 n) (sub1 m))))))

(define o<
  (lambda (n m)
    (cond
      ((zero? m) #f)
      ((zero? n) #t)
      (else (o< (sub1 n) (sub1 m))))))

(define o=
  (lambda (n m)
    (cond
      ((o> n m) #f)
      ((o< n m) #f)
      (else #t))))

(export-all))

テスト用スクリプト:

(use mymodule)

(print (atom? 'a))
(print (lat? '(my name is takatoh)))
(print (lat? '(my name (is) takatoh)))
(print (o+ 2 5))
(print (o- 10 4))
(print (o* 3 3))
(print (o> 12 8))
(print (o< 7 10))
(print (o= 6 6))

試してみよう。

^o^ > gosh -I. test_mymodule.scm
#t
#t
#f
7
6
9
#t
#t
#t

うまくいってるようだ。
このモジュールは、スクリプトモードだけでなくインタラクティブモードでも使える。

^o^ > gosh -I.
gosh> (use mymodule)
#
gosh> (atom? 'a)
#t
gosh> (lat? '(hello scheme))
#t
gosh> (o= 3 7)
#f

>と<と=

大小を判定する関数だ。o> と書くことにしよう。(o> n m) は n>m のときに真を返す。
これはちょっと難しかった、というかややこしくて答を見てもすぐにはわからなかった。まあ、とにかく書いてみた。

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

(define o>
  (lambda (n m)
    (cond
      ((zero? n) #f)
      ((zero? m) #t)
      (else (o> (sub1 n) (sub1 m))))))

(print (o> 12 123))
(print (o> 12 1))
(print (o> 12 12))

ポイントは9-10行目の順番だ。最初、((zero? m) #t) を先に書いてしまったら、n と m が同じときにも #t になってしまった。n のほうが m よりも大きいときだけ真になってほしいのだから、n が先に 0 になってしまってはいけない。これをチェックするには、((zero? n) #f) を先に書くべきなんだ。

ということで、実行:

^o^ > gosh gt.scm
#f
#t
#f

無事、うまくいった。

これは、o< と書くことにしよう。で、>ができれば<は簡単。n と m を入れ替えればいいだけだ。

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

(define o<
  (lambda (n m)
    (cond
      ((zero? m) #f)
      ((zero? n) #t)
      (else (o< (sub1 n) (sub1 m))))))

(print (o< 12 123))
(print (o< 12 1))
(print (o< 12 12)) 

実行:

^o^ > gosh lt.scm
#t
#f
#f

OK!

=は n と m が等しいとき真を返す。o= と書くことにしよう。

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

(define o>
  (lambda (n m)
    (cond
      ((zero? n) #f)
      ((zero? m) #t)
      (else (o> (sub1 n) (sub1 m))))))

(define o<
  (lambda (n m)
    (cond
      ((zero? m) #f)
      ((zero? n) #t)
      (else (o< (sub1 n) (sub1 m))))))

(define o=
  (lambda (n m)
    (cond
      ((o> n m) #f)
      ((o< n m) #f)
      (else #t))))

(print (o= 12 123))
(print (o= 12 1))
(print (o= 12 12))

実行:

^o^ > gosh equal.scm
#f
#f
#t

tup+

tup+ は2つのタップを引数に取り、それぞれの1番目同士、2番目同士・・・を足し合わせたタップを返す。タップというのは、数のリストのことだ。「数のリストのことをタップ(あるいはタプル)と呼びましょう」と書いてあるから、Scheme の用語というよりこの本(「Scheme手習い」)での用語と考えていいのかな。

同じ長さのタップの場合

とにかく書いてみよう。ただし、まずは2つのタップの長さは同じとする。

(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)))))))

(define tup+
  (lambda (tup1 tup2)
    (cond
      ((null? tup1) (quote ()))
      (else (cons (o+ (car tup1) (car tup2)) (tup+ (cdr tup1) (cdr tup2)))))))

(print (tup+ '(1 2 3) '(4 5 6)))

実行:

^o^ > gosh tupplus.scm
(5 7 9)

うまくいったようだ。
でも、答えを見たら、cond のひとつ目の条件(21行目)が (and (null? tup1) (null? tup2)) になっていた。2つのタップの長さが同じという前提なんだから片方だけチェックすればいいと思うんだけどな。

違う長さのタップの場合

(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)))))))

(define tup+
  (lambda (tup1 tup2)
    (cond
      ((null? tup1) tup2)
      ((null? tup2) tup1)
      (else (cons (o+ (car tup1) (car tup2)) (tup+ (cdr tup1) (cdr tup2)))))))

(print (tup+ '(1 2 3) '(10 20 30)))
(print (tup+ '(1 2 3) '(10 20 30 40 50)))
(print (tup+ '(1 2 3 4 5) '(10 20 30)))

実行:

^o^ > gosh tupplus2.scm
(11 22 33)
(11 22 33 40 50)
(11 22 33 4 5)

うまくいった。ポイントは21行目と22行目だ。tup1 と tup2 のどちらかが先に 空になったら、もう一方のタップを返して終了する。もちろん、両方が同時に空になった場合(つまり同じ長さの場合)には (null? tup1) の値 tup2 も空だから、これでうまくいくわけだ。

×

×は掛け算だ。o* と書くことにしよう。n×m を言い換えると、nをm回足したものということができる。だから o* を定義するには、m を0になるまで1ずつ減らしながら、nを足していけばいい。1ずつ減らしていくのは (sub1 m) で、最終条件は (zero? m) だ。

第4の戒律
(改訂版)
再帰の間は少なくとも1つの引数を常に変化させるべし。引数は終わりに向けて変化させることを要す。変化する引数は最終条件にてテストすべし。すなわち、cdr を用いるときは、null? で最終テストし、sub1 を用いるときは、zero? で最終テストせよ。

それじゃ、関数 o* を書いてみよう。

(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)))))))

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

(print (o* 2 3))
(print (o* 10 5))

実行:

^o^ > gosh mul.scm
6
50

21行目、(zero? m) のときの値が 0 なのは、0 が足し算の答に影響しないからだ。もし再帰に掛け算を使うとすれば、0 の換わりに 1 を使えばいい。

第5の戒律
+で値を作らんとせしときは、行を終えるときに常に値として 0 を用うべし。なんとなれば、0 を加うるは加算の値を変えぬからなり。
×で値を作らんとせしときは、行を終えるときに常に値として 1 を用うべし。なんとなれば、1 を掛けるは乗算の値を変えぬからなり。
cons で値を作らんとせしときは、行を終えるときに常に値として () を考えるべし。

(白抜きの)+と-

今日から第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)