ボルダリングをやってきた(3)

今日の午後は、ボルダリングをやってきた。今年になって初めてだ。年末年始の連休を別にすれば、相変わらず週一ペースで続いている。
で、どうだったかというと、割と幸先よくできたと思う。手足限定課題のオレンジB、C、青B と足自由の水色6 をクリアできた。一方で、ずっとクリアできないでいる水色4 は今日もダメだった。何でできないんだ。

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

さて、本を読み進めるうちに書くコードも長くなってきた。たとえば前のエントリで書いた関数 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 なり。