The King's Museum

ソフトウェアエンジニアのブログ。

Scheme 手習い(4)

第6章:影法師

numbered?

(define (numbered? aexp)
   (cond [(atom? aexp) #t]
         [(eq? '+ (car (cdr aexp)))
          (and (numbered? (car aexp)) (numbered? (car (cdr (cdr aexp)))))]
         [(eq? '* (car (cdr aexp)))
          (and (numbered? (car aexp)) (numbered? (car (cdr (cdr aexp)))))]
         [(eq? '^ (car (cdr aexp)))
          (and (numbered? (car aexp)) (numbered? (car (cdr (cdr aexp)))))]
         [else #f]))

渡された S 式が (1 + 1) のように演算子が中央にあるような表現式(算術式)かどうかを確認する関数。

aexp がアトムの場合には true。 aexp が演算子で構成されている場合は、それぞれの項が numbered? であるかを再帰的に調べる。

value

(define (value aexp)
  (cond [(atom? aexp) aexp]
        [(eq? '+ (car (cdr aexp)))
          (+ (value (car aexp)) (value (car (cdr (cdr aexp)))))]
        [(eq? '* (car (cdr aexp)))
          (* (value (car aexp)) (value (car (cdr (cdr aexp)))))]
        [(eq? '^ (car (cdr aexp)))
          (expt (value (car aexp)) (value (car (cdr (cdr aexp)))))]

aexp を評価する関数。

numbered? と似たような構造。ちがうのは実際の計算を行うところ。

第7の戒律

【第7の戒律】

性質を同じくするすべての構成部分について再帰すべし。

すなわち、

  • リストの全ての部分リストについて

  • 算術式のすべての部分式について

value2

(define (value aexp)
  (cond [(atom? aexp) aexp]
        [(eq? '+ (car aexp))
         (+ (value (car (cdr aexp))) (value (car (cdr (cdr aexp)))))]
        [(eq? '* (car aexp))
         (* (value (car (cdr aexp))) (value (car (cdr (cdr aexp)))))]
        [(eq? '^ (car aexp))
         (expt (value (car (cdr aexp))) (value (car (cdr (cdr aexp)))))]))

(+ 1 1) のように演算子が先頭にある場合の算術式を計算する関数。

最初の numbered? と異なるのは演算子や部分式を取得する car や cdr の部分のみ。

value3

算術式の表現として次のようなバリエーションが考えられる。

  • (+ 1 1)
  • (1 + 1)
  • (1 1 +)

変わるのは演算子の位置と部分式の位置だけ。 その部分を取得する関数を次のように抽象化できる。

  • 1つ目の部分式を取得する関数:1st-sub-exp
  • 2つ目の部分式を取得する関数:2nd-sub-exp
  • 演算子を取得する関数:operator
(define (value aexp)
  (cond [(atom? aexp) aexp]
        [(eq? '+ (operator aexp))
         (+ (value (1st-sub-exp aexp)) (value (2nd-sub-exp aexp)))]
        [(eq? '* (operator aexp))
         (* (value (1st-sub-exp aexp)) (value (2nd-sub-exp aexp)))]
        [(eq? '* (operator aexp))
         (expt (value (1st-sub-exp aexp)) (value (2nd-sub-exp aexp)))]))

たとえば、(1 1 +) のような表現式の場合にはそれぞれの関数は次のようになる。

(define (1st-sub-exp aexp)
  (car aexp))
(define (2nd-sub-exp aexp)
  (car (cdr aexp)))
(define (operator aexp)
  (car (cdr (cdr aexp))))

第8の戒律

【第8の戒律】

表現から抽象化するに際し、補助関数を使用すべし。

数を空リストで表現する

ここからは数を空リストで表現した場合の演算を実装する。

たとえば、1 は (())、3 は (() () ()) で表現する。

sero?

(define (sero? n)
  (null? n))

zero? に対応する関数。空リストかどうかを調べる。

edd1

(define (edd1 n)
  (cons '() n))

add1 に対応する関数。空リストを一つ増やす。

zub1

(define (zub1 n)
  (cdr n))

sub1 に対応する関数。空リストを一つ少なくする。

+

(define (+ n m)
  (cond [(sero? m) n]
        [else (edd1 (+ n (zub1 m)))]))

足し算を定義する。

ただし、このケースでは lat? の条件を満たさない(たしかにその通りだが、、、)。

タイトルの「影法師」が何の比喩なのかは分からず…。

Scheme 手習い(4)

第5章:*すごい* 星がいっぱいだ

元ネタは『2001 年宇宙の旅』のボーマン船長のセリフらしい。 そういえばそんなセリフもあったな。

rember*

(define (rember* a l)
  (cond [(null? l) '()]
        [(atom? (car l))
         (cond [(eq? a (car l)) (rember* a (cdr l))]
               [else (cons (car l) (rember* a (cdr l)))])]
        [else (cons (rember* a (car l)) (rember* a (cdr l)))]))

アトムとリストの両者を含むリストから特定のアトムを削除する関数。 S 式のリストから、特定のアトムを削除する関数ともいえる。

最後段の cons の第一引数が、(car l) に対する rember* になっているところがポイント。 ここでは (car l) はリストであることが保障されている。

insertR*

(define (insertR* new old l)
  (cond [(null? l) '()]
        [(atom? (car l))
         (cond [(eq? old (car l))
                (cons old (cons new (insertR* new old (cdr l))))]
               [else (cons (car l) (insertR* new old (cdr l)))])]
        [else (cons (insertR* new old (car l))
                    (insertR* new old (cdr l)))]))

S 式のリスト内の任意のアトムの右側にアトムを挿入する関数。

第1の戒律

【第1の戒律(最終版)】

アトムのリスト lat を再帰せしときは、2つの質問、(null? lat) と else を行うべし。

数 n を再帰せしときは、2つの質問、(zero? n) と else を行うべし。

S 式のリスト l を再帰せしときは、3つの質問、(null? l)、(atom? (car l))、else を行うべし。

第4の戒律

【第4の戒律(最終版)】

再帰の間は少なくとも1つの引数を常に変化させるべし。

アトムのリスト lat を再帰せしときは、(cdr lat) を用いるべし。

数 n を再帰せしときは、(sub 1) を用いるべし。

S 式のリスト l を再帰せしときは、(null? l) も (atom? (car l)) も真でないならば、(car l) と (cdr l) を用いるべし。

必ず最終条件に向かって変化すべし。

変化せし引数は、必ず最終条件でテストすべし。 すなわち、cdr を用いるときは、最後に null? で、sub1 を用いるときは、最後に zero? でテストすべし。

occur*

(define (occur* a l)
  (cond [(null? l) 0]
        [(atom? (car l))
         (cond [(eq? (car l) a) (add1 (occur* a (cdr l)))]
               [else (occur* a (cdr l))])]
        [else (+ (occur* a (car l)) (occur* a (cdr l)))]))

S 式のリスト内に特定のアトムが何個現れるかを数える関数。

subst*

(define (subst* new old l)
  (cond [(null? l) '()]
        [(atom? (car l))
         (cond [(eq? old (car l)) (cons new (subst* new old (cdr l)))]
               [else (cons (car l) (subst* new old (cdr l)))])]
        [else (cons (subst* new old (car l))
                    (subst* new old (cdr l)))]))               

S 式のリスト内の任意のアトムを新しいアトムで置き換える関数。

insertL*

(define (insertL* new old l)
  (cond [(null? l) '()]
        [(atom? (car l))
         (cond [(eq? (car l) old)
                (cons new (cons old (insertL* new old (cdr l))))]
               [else (cons (car l) (insertL* new old (cdr l)))])]
        [else (cons (insertL* new old (car l)) (insertL* new old (cdr l)))]))       

S 式のリスト内の任意のアトムの左にアトムを挿入する関数。

member*

(define (member* a l)
  (cond [(null? l) #f]
        [(atom? (car l))
         (cond [(eq? (car l) a) #t]
               [else (member* a (cdr l))])]
        [else (or (member* a (car l)) (member* a (cdr l)))]))

S 式のリスト内に任意のアトムがあるかどうかを判断する関数。

leftmost

(define (leftmost l)
  (cond [(atom? (car l)) (car l)]
        [else (leftmost (car l))]))

S 式のリスト内の最も左のアトムを求める関数。 空リストは考慮しない。

eqlist?

;; 冗長バージョン
(define (eqlist? a b)
  (cond [(and (null? a) (null? b)) #t]
        [(and (null? a) (atom? (car b))) #f]
        [(null? a) #f]
        [(and (atom? (car a)) (null? (car b))) #f]
        [(and (atom? (car a)) (atom? (car b)))
         (and (eq? (car a) (car b)) (eqlist? (cdr a) (cdr b)))]
        [(atom? a) #f]
        [(null? b) #f]
        [(atom? b) #f]
        [else (and (eqlist? (car a) (car b))
                   (eqlist? (cdr a) (cdr b)))]))

;; シンプルバージョン
(define (eqlist? a b)
  (cond [(and (null? a) (null? b)) #t]
        [(or (null? a) (null? b)) #f]
        [(and (atom? (car a)) (atom? (car b)))
         (and (eq? (car a) (car b)) (eqlist? (cdr a) (cdr b)))]
        [(or (atom? (car a)) (atom? (car b))) #f]
        [else (and (eqlist? (car a) (car b))
                   (eqlist? (cdr a) (cdr b)))]))         

S 式のリストが等しいかどうかを確認する関数。 次のパターンの組み合わせで考える。

  • リストが null
  • リストの先頭(car)がアトム
  • それ以外(リストの先頭がリスト)

equal?

(define (equal? a b)
  (cond [(and (atom? a) (atom? b)) (eq? a b)]
        [(or (atom? a) (atom? b)) #f]
        [else (eqlist? a b)]))

S 式のリストだけでなく、アトムが等しいかどうかも確認できる関数。 すなわち、二つの S 式が等しいかを確認する関数。

第6の戒律

【第6の戒律(最終版)】

関数が正しいときのみ簡単化せよ

感想

S 式に対する操作を学習した。

再帰的なリスト(S 式)になるとすぐに頭が混乱していたけど、この章のおかげで自分の頭の中の見通しがよくなった気がする。

Scheme 手習い(3)

第4章は数字に関する関数をどうやって再帰的に書くかという話が中心。

第4章:数字宛てゲーム

add1/sub1

(define (add1 n)
  (+ n 1))
(define (sub1 n)
  (- n 1))

数に 1 を足す関数と数から 1 をひく関数。

+/-

(define (+ n m)
  (cond [(zero? m) n]
        [else (add1 (+ n (sub1 m)))]))
(define (- n m)
  (cond [(zero? m) n]
        [else (sub1 (- n (sub1 m)))]))

足し算と引き算を add1、sub1 を使って再帰的に定義する。

「n に m 回だけ 1 を足す(または引く)」という考え方。

addtup

(define (addtup tup)
  (cond [(null? tup) 0]
        [else (+ (car tup) (addtup (cdr tup)))]))

tup は数字のみで構成されたリスト。

addtup は tup の要素を足し合わせる関数。 終了条件には null? を使って 0 を返す。

*

(define (* n m)
  (cond [(zero? m) 0]
        [else (+ n (* n (sub1 m)))]))

足し算を使って掛け算を定義する。

「n を m 回だけ足し合わせる」という考え方。

第1の戒律(改訂版)

【第1の戒律(改訂版)】

アトムのリスト lat を再帰するときは、2つの質問をすべし。すなわち、(null? lat) と else なり。

数 n を再帰するときは、2つの質問をすべし。すなわち、(zero? n) と else なり

第4の戒律

【第4の戒律(改訂版)】

再帰の間は少なくとも1つの引数を常に変化させるべし。

引数は終わりに向けて変化させることを要す。変化する引数は最終条件にてテストすべし。 すなわち、cdr を用いるときは、null? で最終テストし、sub1 を用いるときは、zero? で最終テストせよ

第5の戒律

【第5の戒律】

+ で値を作らんとせしときは、行を終える時に常に値として 0 を用うべし。なんとなれば、0 を加うるは加算の値を変えるぬからなり。

* で値を作らんとせしときは、行を終える時に常に値として 1 を用うべし。なんとなれば、1 を掛けるは乗算の値を変えぬからなり。

cons で値を作らんとせしときは、行を終えるときに常に値として () を考えるべし。

tup+

(define (tup+ tup1 tup2)
  (cond [(null? tup1) tup2]
        [(null? tup2) tup1]
        [else (cons (+ (car tup1) (car tup2))
                    (tup+ (cdr tup1) (cdr tup2)))]))

両方の tup の要素を足し合わせて新しいリストにする関数。

片方が空リストの場合は、もう片方のリストを返すのがポイント。

>, <, =

(define (> n m)
  (cond [(zero? n) #f]
        [(zero? m) #t]
        [else (< (sub1 n) (sub1 m))]))
(define (< n m)
  (cond [(zero? m) #f]
        [(zero? n) #t]
        [else (< (sub1 n) (sub1 m))]))
(define (= n m)
  (cond [(> n m) #f]
        [(< n m) #f]
        [else #t]))

大なり、小なりも sub1 と zero? のみを使って実装できる。

片方の引数が 0 になるまで 1 を引いていき、最終的には zero? を使って大小を判断する。

expt

(define (expt n m)
  (cond [(zero? m) 1]
        [else (* n (expt n (sub1 m)))]))

累乗を計算する関数。

/

(define (/ n m)
  (cond [(< n m) 0]
        [else (add1 (/ (- n m) m))]))

割り算も - を使って再帰的に定義できる。

「m で何回引き算できるか」を使って割り算を定義する。 これはなかなかおもしろい考え方。

length

(define (length lat)
  (cond [(null? lat) 0]
        [else (add1 (length (cdr lat)))]))

リストの長さを計算する関数。

pick/rempick

(define (pick a lat)
   (cond [(zero? (sub1 a)) (car lat)]
         [else (pick (sub1 a) (cdr lat))]))
(define (rempick n lat)
  (cond [(zero? (sub1 n)) (cdr lat)]
        [else (cons (car lat) (rempick (sub1 n) (cdr lat)))]))

リストから対象の要素を除去する関数。

no-nums/all-nums

(define (no-nums lat)
  (cond [(null? lat) '()]
        [(number? (car lat)) (no-nums (cdr lat))]
        [else (cons (car lat) (no-nums (cdr lat)))]))
(define (all-nums lat)
   (cond [(null? lat) '()]
         [(number? (car lat)) (cons (car lat) (all-nums (cdr lat)))]
         [else (all-nums (cdr lat))]))

リストから数を除去する関数と数だけを抽出する関数。

eqan?

(define (eqan? a1 a2)
  (cond [(and (number? a1) (number?a2)) (= a1 a2)]
        [(or (number? a1) (number? a2)) #f]
        [else (eq? a1 a2)]))

同じアトムかどうかを調べる関数。

両方とも数字なら = で比較。片方だけが数字なら偽。それ以外なら eq? で比較する。 片方が数字の場合は eq? を使えないので注意(Eq? の掟)

occur

(define (occur a lat)
  (cond [(null? lat) 0]
        [(eqan? a (car lat)) (add1 (occur a (cdr lat)))]
        [else (occur a (cdr lat))]))

リストの中の該当の要素の数を計測する関数。

one?

(define (one? n)
  (cond [(zero? n) #f]
        [else (zero? (sub1 n))]))

与えられた引数が 1 かどうかを調べる関数。 マイナスをケアするために、n がゼロかどうかを調べている(ゼロにsub1 するのは違反)

当然、次のようにも書ける

(define (one? n)
  (= n 1))

感想

再帰のパターンがだいぶ身についてきた。

Scheme 手習い(2)

第3章:偉大なる cons

rember

(define (rember a lat)
  (cond [(null? lat) '()]
        [(eq? (car lat) a) (cdr lat)]
        [else (cons (car lat) (rember a (cdr lat)))]))

rember は remove member の略。 ラット(すべての要素がアトムのリスト)内で一致した最初のアトムを削除する関数。

第2の戒律

【第2の戒律】

リストを作るには cons を用いるべし

そのまんまなんだけど、リストを作りたいときは cons を利用する。

firsts

(define (firsts l)
  (cond [(null? l) '()]
        [else (cons (car (car l)) (firsts (cdr l)))]))

firsts はリストを含むリストの最初の要素を集めて再びリストにする関数。

第3の戒律

【第3の戒律】

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

cons は第一引数は要素、の第二引数は再帰、の形にする。

insertR/insertL

(deifne (insertR new old lat)
  (cond [(null? lat) '()]
        [(eq? (car lat) old) (cons (car lat) (cons new (cdr lat)))]
        [else (cons (car lat) (insertR new old (cdr lat)))]))

insertR は old の右側に new を挿入する関数。

(define (insertL new old lat)
  (cond [(null? lat) '()]
        [(eq? old (car lat)) (cons new lat)]
        [else (cons (car lat) (insertL new old (cdr lat)))]))

insertL は old の左側に new を挿入する関数。

subst/subst2

(define (subst new old lat)
  (cond [(null? lat) '()]
        [(eq? old (car lat)) (cons new (cdr lat))]
        [else (cons (car lat) (subst new old (cdr lat)))]))

old を new に置き換える関数。

(define (subst2 new o1 o2 lat)
  (cond [(null? lat) '()]
        [(or (eq? o1 (car lat)) (eq? o2 (car lat)))
         (cons new (cdr lat))]
        [else (cons (car lat) (subst2 new o1 o2 (cdr lat)))]))

o1 か o2 を new に置き換える関数。

multirember

(define (multirember a lat)
  (cond [(null?o lat) '()]
        [(eq? (car lat) a) (multirember a (cdr lat))]
        [else (cons (car lat) (multirember a (cdr lat)))]))

ラット内のすべての要素 a を削除して新しいリストを作る関数。 rember の複数要素対応ってところ。

multiinsertR/multiinsertL

(define (multiinsertR new old lat)
  (cond [(null? lat) '()]
        [(eq? (car lat) old)
         (cons old (cons new (multiinsertR new old (cdr lat))))]
        [else (cons (car lat) (multiinsertR new old (cdr lat)))]))
(define (multiinsertL new old lat)
  (cond [(null? lat) '()]
        [(eq? old (car lat))
         (cons new (cons old (multiinsertL new old (cdr lat))))]
        [else (cons (car lat) (multiinsertL new old (cdr lat)))]))

insertL と insertR の複数要素対応版。

感想

第3章は再帰の基本的な構造を学んだ。

ここら辺は SICP でやったことがあるところなので、特に難しくなかった。 でも、SICP よりパターン化してあるからこちらの方がより定着率は高いかな。

『Scheme 手習い』はじめました。

Gauche 本を進めていたけれど、なんだかあんまり身についてる気がしなかったので『Scheme 手習い』に手を出してみた

Scheme手習い

Scheme手習い

  • 作者: Daniel P. Friedman,Matthias Felleisen,元吉文男,横山晶一
  • 出版社/メーカー: オーム社
  • 発売日: 2010/10/22
  • メディア: 単行本(ソフトカバー)
  • 購入: 5人 クリック: 129回
  • この商品を含むブログ (34件) を見る

第1章:おもちゃ

アトム、リスト、S 式とは

Gauche は atom? が定義されてないので、本に書いてあるように定義する。

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

(atom? '0) ; => #t

「(atom? '0) が #f になることを確かめてほしい」って本に書いてあるけど、おかしい…。 本の誤植かも。

(atom? 'atom) ; => #t
(atom? 1492) ; => #t
(atom? '()) ; => #f
(atom? '(1 2 3)) ; => #f

アトムはリストではないもの。

(list? 'atom) ; => #f
(list? 1492) ; => #f
(list? '()) ; => #t
(list? '(1 2 3)) ; => #t

リストは S 式を括弧でくくったもの。

アトムは S 式。 リストもそれ自体が S 式となる。

car とは

(car '(A B C)) ; => A
(car '(A)) ; => A
(car 'hotdog) ; => 未定義
(car '()) ; => 未定義

car は空でないリストの先頭の S 式を取り出す。 アトムと空のリストに対する操作は定義されていない。

cdr とは

(cdr '(A B C)) ; => (B C)
(cdr '(A)) ; => ()
(cdr 'hotdog) ; => 未定義
(cdr '()) ; => 未定義

cdr は空でないリストの先頭以外のリストを取り出す。 アトムと空のリストに対する操作は定義されていない。 cdr は常にリストとなる。

cons とは

(cons 'a '(b c)) ; => (a b c)
(cons 'a '()) ; => (a)
(cons 'a 'b) ; => 本では扱わない

cons は任意の S 式をリストの先頭につける。 第一引数は S 式。第二引数は任意のリスト。 ドット対は扱わないようだ。

null? とは何か

(null? '()) ; => #t
(null? '(a)) ; => #f
(null? 'a) ; => 本では扱わない

null? は空リストかどうかを調べる。 アトムに対する null? は定義されていない。

eq? とは何か

(eq? 'atom 'atom) ; => #t
(eq? '(1) '(1)) ; => 本では扱わない
(eq? 7 6) ; => 本では扱わない

eq? は同じアトムであるかどうかを調べる。 eq? はどちらも数でないアトムでなければならない。

1章はこれで終わり。

第2章:一度、もう一度、さらにもう一度、またもう一度、……

lat?

(define (lat? lis)
  (cond [(null? lis) #t]
        [(atom? (car lis)) (lat? (cdr lis))]
        [else #f]))

(lat? '()) ; => #t
(lat? '(a b c)) ; => #t
(lat? '((a) b c)) ; => #f
(lat? '(() b c)) ; => #f

lat? はリスト内の S 式がすべてアトムだった場合に #t を返す関数。

or

(or (null? '()) (null? '())) ; => #t
(or (null? '()) (null? '(a b c))) ; => #t
(or (null? '(a b c)) (null? '())) ; => #t
(or (null? '(a b c)) (null? '(a b c))) ; => #t

or の確認。

member?

(define member?
  (lambda (lis a)
    (cond [(null? lis) #f]
          [(eq? (car lis) a) #t]
          [else (member? (cdr lis) a)])))

(member? '(a b) 'a) ; => #t
(member? '(a b) 'c) ; => #f
(member? '() 'a) ; => #f

与えられたアトムがリスト内にあるかどうかを調べる関数。

第1の戒律

【第1の戒律】

(仮)いかなる関数を表現するときも最初の質問はすべて null? にすべし。

感想

再帰の練習って感じ。まずはじめに null? を質問するのがコツ。 ここらへんはまだまだ簡単。

そういえば、紙の本買ったのひさしぶりだなぁ。

クラウドのもろさとは

昨日起きた AWS の障害。自分の会社も AWS を利用しているので影響を受けた。

この出来事に対してクラウドを利用することへのもろさを懸念する論調が一部であがった。

「もろさ」には二種類あると思う。個別のサービスに対するもろさと、社会全体として見たときのもろさ。

大手が提供するクラウドサービスを利用すること個別のサービスのもろさは低減されると思う。自社でサーバーを管理するよりも高度な体制で管理されているだろうし、今回のような事件があっても復旧は早い。

しかし、多くのサービスが特定の業者のクラウドに依存するようになると、社会としてのもろさは確実に上昇するだろう。オール電化のマンションが停電すると何もできないように。

ウェブサービスが載っかるインターネットは分散システムになっていることとは対照的だと感じた。

あと、このあたりの切り分けをせずにこのことを論じると、なんだか水掛け論だなぁとツイッターなどを見ていて思った。

(c) The King's Museum