もうひとつの Scheme 入門(1) 1-7

SICPを読む上で、最初は「プログラミングGauche」を読んでいたんだけど、構文の話ばかりで練習問題も少なかった。そこでぐぐった所、「もうひとつの Scheme 入門」を見つけた。さらっとやれるかと思っていたけれど、意外に手ごわく、いつになったら読み終わるか分からないので、やり終わった1-7までのメモを公開する。ちなみに、MzSchemeではなくGaucheで実習したので、書いてある内容もGaucheを前提としている。

1. Scheme 処理系のインストール

Windowsの処理系としてMzSchemeのインストール方法とEmacsとの連携方法について記述してある。Gaucheboxと同じ感じになる。あと、GaucheはR5RS準拠だけど、MzSchemeはR6RS準拠で、学習するならR5RSで十分だけど、実用的なプログラムを書くにはR6RSの方がよいそうな。

2. Scheme を電卓代わりに使う

四則演算や算術関数について記述してある。Schemeの処理系は分数を扱うことが出来るので、小数にする場合は exact->inexact を利用する(もしくは引数の一部に小数を含める)。

gosh> (/ 3 5)
3/5
gosh> (exact->inexact (/ 3 5))
0.6
gosh> (/ 3 5.0)
0.6

円周率πを求める練習問題があり、よく分からなかったのでちょっと調べてみたが、奥が深くて理解できなかった。。いずれ再チャレンジしよう。

3. リストを作ろう

  • 再帰的に定義」ってどういう意味の日本語なんだろう?リストを連結したものはリストになるというのが再帰的という意味か?
  • 普通の関数は括弧の中を全て評価して値を外に返す。返さないものを特殊形式というそうだ。quote、if、lamda、defineなど。
  • '() と () の違いは?どちらで入力しても同じ結果になる。処理系によるのかな?

4. 関数を定義しよう

速度と角度から飛距離を求める関数。昔やったんだけど、やっぱり思い出せないね。数学は本腰入れて勉強しなおす必要があるかな。。ちなみに lambda は関数を定義する特殊形式。
あと、可変長引数に関して記述されていたが、「0個以上の引数」に関して記述がなかったためプログラミングGaucheも参照して確認した。

lambda式を使った場合
gosh> (define fixarg (lambda (x) x))
fixarg
gosh> (fixarg 1)
1
gosh> (fixarg 1 2)
*** ERROR: wrong number of arguments for #<closure fixarg> (required 1, got 2)
Stack Trace:
_______________________________________
gosh> (define vararg (lambda x x))
vararg
gosh> (vararg 1)
(1)
gosh> (vararg 1 2)
(1 2)

固定長と可変長の違いは引数の箇所にカッコをつけているかどうか。仮引数に実引数のリストがマッピングされる挙動となっているため、仮引数側がリストである(カッコで囲んである)とリストの要素数が一致しない場合にエラーになる。一方、仮引数がリストでない場合は実引数のリストをそのまま引き受けることが出来る。詳しくはプログラミングGaucheのp72〜p74を参照。

lambdaを省略した場合
gosh> (define (fixarg x) x)
gosh> (define (vararg . x) x)
その他

ちなみにこの調査中に気付いたのだが、前述のコードをprint関数で出力した場合、print関数の戻り値として # が出力される。

gosh> (define vararg (lambda x (print x)))
vararg
gosh> (vararg 1)
(1)
#<undef>
gosh> (vararg 1 2)
(1 2)
#<undef>

5. 分岐

cond式

cond式は条件に対して複数の式を書ける。ifは1つしか書けない。だからといって、複数の式を書くケースがあるのかはナゾだけど。

gosh> (define x 5)
x
gosh> (cond
       ((= x 5) (print (+ x 3)) (print (- x 3)))
       (else 10))
8
2
#<undef>
数値比較関数

数値比較関数に引数を複数指定できるのは直感的でいいな。これってC言語系の言語に導入すると可読性を下げるのかな?

gosh> (define x 5)
x
gosh> (<= 1 x 10)
#t

6. 局所変数

let式を利用することで局所変数を利用できる。処理中に同じ値を利用する場合に、変数として名前をつけて一箇所にまとめられるのがメリットかな。もう少し書けば自然と使えるような気がしてきた。

7. 繰り返し

再帰的プロセス(recursive process)と反復的プロセス(iterative process)。どちらも自身の関数を呼び出す再帰的手続き(recursive procedure)によって実装される*1。ちなみに、反復的プロセスのことを末尾再帰とも言う。
複数の型を返す関数を作るには末尾再帰が必要になる。再帰的プロセスの場合、最終的な戻り値の型を積み上げていく形になるため、型は1種類しか扱えない。一方、末尾再帰の場合は、末端の処理で返す値がそのまま戻り値となるため、複数の型を返すことが出来る。
名前つきletは loop という名前が一般的なようだが、loop 以外も指定できた。

(define (fact-let n)
  (let loop((n1 n) (p n))
    (if (= n1 1)
	p
	(let ((m (- n1 1)))
	  (loop m (* p m))))))

(define (fact-let-hoge n)
  (let hoge((n1 n) (p n))
    (if (= n1 1)
	p
	(let ((m (- n1 1)))
	  (hoge m (* p m))))))
再帰的手続きとリスト

リストを操作する再帰的手続きを実装する場合、再帰と反復で実装方法が異なる。例えば、あるリストから一部の要素を除くような関数を実装する場合で違いが出てくる。
再帰の場合は要素を連結処理を積み上げていく形になるため、もともとの要素の順序を維持できる。

(define (remove-rec ls x)
  (if (null? ls)
    '()
    (let ((e (car ls)))
      (if (eq? e x)
          (remove-rec (cdr ls) x)
          (cons e (remove-rec (cdr ls) x))))))


(remove-rec '(1 2 3 4 5) 3)



(cons 1 (cons 2 (cons 4 (cons 5))))

と処理が進んでいく。

一方、反復の場合は引き回すリストに要素を連結する必要がある。リストに要素を結合する場合、リストの先頭に結合する必要がある。そのため、最後にreverseでリストの順序を反転する必要がある。

(define (remove-itr ls x)
  (define (remove-itr-rec ls0 ls1)
    (if (null? ls0)
    ls1
    (let ((e (car ls0)))
      (if (eq? e x)
          (remove-itr-rec (cdr ls0) ls1)
          (remove-itr-rec (cdr ls0) (cons e ls1))))))
  (reverse (remove-itr-rec ls '())))


(remove-rec-itr '(1 2 3 4 5) 3)



(cons 1 '())
(cons 2 '(1))
(cons 4 '(2 1))
(cons 5 '(4 2 1))

と処理が進んで行くため、最終的に

'(5 4 2 1)

となる。
よって、reverseで反転する必要がある。

ちなみに、反復と名前付きletは関数を内部定義するか名前付きletとして実装するかしか違わない。メリットは名前付きletの場合、初期値と変数名が同じ位置であるため分かりやすい。内部関数の場合は初期値が一番下になるためわかり辛い。つまり、反復を利用する場合は基本的に名前付きletを利用すればよいことになる。

(define (remove-let ls x)
  (reverse
   (let loop((ls0 ls) (ls1 '()))
    (if (null? ls0)
	ls1
	(let ((e (car ls0)))
	  (if (eq? e x)
	      (loop (cdr ls0) ls1)
	      (loop (cdr ls0) (cons e ls1))))))))

一方、letrecがある。ローカルな再帰手続きを書く際に便利とある。ようは反復で実装した際の内部関数remove-itr-recをさらに狭いスコープとして定義できることがメリットみたい。とても複雑な関数を作る場合などには使いたい箇所と定義する箇所が近くてよいのかも。

(define (remove-letrec ls x)
  (letrec ((iter (lambda (ls0 ls1)
		(if (null? ls0)
		    ls1
		    (let ((e (car ls0)))
		      (if (eq? e x)
			  (iter (cdr ls0) ls1)
			  (iter (cdr ls0) (cons e ls1))))))))
  (reverse (iter ls '()))))

正直、名前付きletやletrecの使い分けはイマイチ分からない。まぁ、ある程度の規模のプログラムかかないと分からないのだと思う。そもそも、SICPでは名前つきletはp222、letrecはp232でちょろっと出てくるだけで、SICPそのものを読み進める上ではあまり関係が無いようだ。悩む前にチェックしておけば良かった。。

デバッグ

割とコードの量が増えてきて、実行時エラーが発生することも増えてきた。そこでデバッグとなるわけだけども、Gaucheは #?= を利用することで、S式の結果を出力できる*2