The King's Museum

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

『More Effective Agile』を読んで

『More Effective Agile』を読みました。10 月の読書。

『More Effective Agile』 は『CODE COMPLETE』で有名なスティーブ・マコネル氏の著書。 ソフトウェア開発のトレーニングやコンサルティングを行う会社を経営しているマコネル氏の豊富な経験を基にした、モダンなアジャイルについて書かれた本。

アジャイルマニフェストが世に出て約 20 年。 その間、業界には様々な経験が蓄積され、効果のあるプラクティスとそうではないものが分かるようになってきた。 この本はその違いに焦点があてられていて、その名のとおり「より効果を出すためのアジャイル(More Effective Agile)」のプラクティスを紹介している。

「アジャイル」は、多くのプラクティス・原則・ルールの総称となっている。 代表的なプラクティスの一つである「スクラム」から始まり、自分の知らないプラクティスもたくさん紹介されていた。 最近のアジャイルを俯瞰的に眺めるにはもってこいの一冊だと思う。 著者の長年の経験をふまえた説明もとても納得感があってよい。

ちなみに、代表的なうまくいかないプラクティスの一つとして「スクラムバット」が紹介されている。 「スクラム」はすでに最小セットのプラクティスであり、省いてもよいプラクティスは存在しない。 スクラムのプラクティスを省いてしまうことでプロジェクトに内在している問題が見えにくくなり、結果としてうまくいっていない状態をスクラムバットと呼ぶ。

スクラムバットの名前の由来は、このような状況でよく挙げられる台詞から来ている。

A ScrumBut has a particular syntax: (ScrumBut)(Reason)(Workaround)

ScrumBut Examples:

"(We use Scrum, but) (having a Daily Scrum every day is too much overhead,) (so we only have one per week.)"

...

「スクラムはやってるよ。でも、〇〇だから、××はやってないんだ。」ってやつ。よく聞くセリフだ。

スクラムバットは特に陥りがちな状態だと思うけど、それ以外にも気をつけなきゃいけないなーと思うことが多くて身になる一冊だった。

Scheme 手習い(10)~ eval と apply ~

第10章:このすべての値は何だ

Scheme 手習い第 10 章。

いわゆる eval を実装する。この本では value という関数名になっているけど。

準備

リストを操作するための便利関数を事前に定義しておく。

(define (build a b)
  (cons a (cons b '())))

(define (first x)
  (car x))

(define (second x)
  (car (cdr x)))

(define (third x)
  (car (cdr (cdr x))))

エントリーとテーブル(環境)

new-entry

(define new-entry build)

(new-entry '(appetizer entree beverage) '(patee boeuf vin))
; => ((appetizer entree beverage) (patee boeuf vin))

エントリーとは2つの要素からなるリストである。 第1要素が集合のリスト、第2要素がその値であるリスト。

変数名とそれらに対応する値のリストである。

lookup-in-entry

(define (lookup-in-entry name entry entry-f)
  (lookup-in-entry-helper
    name
    (first entry)
    (second entry)
    entry-f))

(define (lookup-in-entry-helper name names values entry-f)
  (cond [(null? names) (entry-f name)]
        [(eq? name (car names)) (car values)]
        [else (lookup-in-entry-helper name (cdr names) (cdr values) entry-f)]))

entry から name に対応する値を探す関数。

指定された entry 内に name が見つからなかった場合には、entry-f を呼び出す点が特徴的。 これをどうやって利用するかはあとで分かる。

extend-table

テーブルというのはエントリーのリスト。テーブルは環境とも呼ぶ。

テーブルを拡張するには、単にリストの先頭にエントリーを追加すればよい。

(define extend-table cons)

(define entry-1 '((a b c) (1 2 3)))
(define entry-2 '((d e f) (4 5 6)))
(define entry-3 '((g h i) (7 8 9)))

(extend-table entry-1
  (extend-table entry-2
    (extend-table entry-3 '())))
;; => (((a b c) (1 2 3)) ((d e f) (4 5 6)) ((g h i) (7 8 9)))

lookup-in-table

(define (lookup-in-table name table table-f)
  (cond [(null? table) (table-f name)]
        [else (lookup-in-entry name (car table)
                (lambda (name)
                  (lookup-in-table name (cdr table) table-f)))]))                

テーブルから任意の名前に対応する値を探す関数。

それぞれのエントリには同じ名前が含まれいてる可能性があるが、そういう場合は先頭にあるエントリの値を優先する。

テーブルの先頭にあるエントリーに対し lookup-in-entry を行う。 もし値が見つからなかった場合は entry-f として再帰する処理を lambda で渡している。

こうやって再帰を渡す方法もあるんだなぁと感心。継続に近いのかな。

value

さて、ここからは value を実装する長い旅にでる。

value は「渡された S 式の自然な値を返す(評価する)関数」である。 いわゆる eval と同等のものになる。

value の仕様

まず、value の返すべき値について本に書かれていた実例を挙げておく。

(value '(car (quote (a b c))))
; => a

(value '(quote (car (quote (a b c)))))
; => (car (quote (a b c)))

(value '(add1 6))
; => 7

(value '6)
; => 6

(value 'car)
; => (pritmive car)

(value '(quote nothing))
; => nothing

(value 'nothing)
; => エラー。対応する変数が定義されていないため

(value '((lambda (nothing)
           (cons nothing (quote ())))
         (quote
           (from nothing comes something))))
; => ((from nothing comes something))

(value '((lambda (nothing)
           (cond [nothing (quote something)]
                 [else (quote nothing)]))
         #t))
; => something

(primitive car) はプリミティブな基本関数であること示している。 これについては後述。

タイプ

value にはさまざまな S 式が渡されるので、まずその種類に応じてタイプを定義する。 type はタイプを取得する関数だと仮定する。

(type '6)
; => *const(定数)

(type '#f)
; => *const(定数)

(type 'cons)
; => *const(定数)

(type '(quote nothing)
; => *quote(クオート)

(type 'nothing)
; => *identifier(識別子)

(type '(lambda (x y) (cons x y)))
; => *lambda(ラムダ)

(type '((lambda (nothing)
           (cond [nothing (quote something)]
                 [else (quote nothing)]))
        #f))         
; => *application(適用)

(type '(cond [(nothing (quote something))]
             [(else (quote nothing))]))
; => *cond(条件)

これらのタイプそれぞれに対しての評価する処理を実装していく。

アクション

与えられた S 式に対して、それぞれのタイプに対するアクションを実装する。

(define (expression-to-action e)
  (cond [(atom? e) (atom-to-action e)]
        [else (list-to-action e)]))

e に対して atom かリストであるかを判別してそれぞれに対して正しいアクションへの変換を定義する。 atom-to-action は次のとおり。

(define (atom-to-action e)
  (cond [(number? e) *const]
        [(eq? e #t) *const]
        [(eq? e #f) *const]
        [(eq? e (quote cons)) *const]
        [(eq? e (quote car)) *const]
        [(eq? e (quote cdr)) *const]
        [(eq? e (quote null?)) *const]
        [(eq? e (quote eq?)) *const]
        [(eq? e (quote atom?)) *const]
        [(eq? e (quote zero?)) *const]
        [(eq? e (quote add1)) *const]
        [(eq? e (quote sub1)) *const]
        [(eq? e (quote number?)) *const]
        [else *identifier]))

list-to-action は次の通り。

(define (list-to-action e)
  (cond [(atom? (car e))
         (cond [(eq? (car e) (quote cond)) *quote]
               [(eq? (car e) (quote cond)) *cond]
               [(eq? (car e) (quote lambda)) *lambda]
               [else *application])]             
        [else *application]))

アクションである *const、*identifier、*cond などはこのあと定義していく。

expression-to-action を使うと value は次のように定義できる。

(define (value e)
  (meaning e '()))

(define (meaning e table)
  ((expression-to-action e) e table))

value は空のテーブル(環境)を定義し、meaning を呼ぶ。 meaning は与えられた S 式からアクションを生成して、そのアクションを S 式と与えられた環境で評価する。

アクション定義

アクションは S 式と環境を受け取って評価する関数。 すべてのアクションは S 式である e と環境である table を引数として受け取る。

*const

(define (*const e table)
  (cond [(number? e) e]
        [(eq? #t e) #t]
        [(eq? #f e) #f]
        [else (build (quote primitive) e)]))

(*const '1 '()) ; => 1
(*const '#t '()) ; => #t
(*const 'cons '()) ; => (primitive cons)

定数に対するアクション。

S 式が数字や真偽値ならそのまま S 式を返す。 それ以外の場合はプリミティブな基本関数であることを示す (primitive e) というリストを返す。

プリミティブな基本関数とはシステムに組み込まれた何をすればいいかすでに分かっている関数を指す。

*quote

(define (*quote e table)
  (second e))

(*quote '(quote hoge) ()) ; => hoge

クオートに対するアクション。

渡される S 式は (quote hoge) のような形になっているので、単に第2要素を返せばよい。

この挙動を考えると、そもそも Scheme の quote が何をするものかより明確に理解できる。 quote を使うと S 式を評価をせずそのまま返すことができる。

*identifier

(define (*identifier e table)
  (lookup-in-table e table initial-table))
  
(define (initial-table name)
  (car '()))

(*identifier 'x '(((x y) (1 2)))) ;=> 1
(*identifier 'z '(((x y) (1 2)) ((z) (0)))) ;=> 0

識別子に対するアクション。

テーブル(環境)から、名前に対応する値を返す関数。 テーブルから名前がひけない場合はエラー(空リストに対する car)となるようになっている。

*cond

次は *cond を実装する。

cond は次のような構文である。

<cond-form> := (cond cond-line cond-line cond-line ...)
<cond-line> := (question answer)

これを踏まえて *cond を実装する。

; 式が else かどうかを調べる関数
(define (else? e)
  (cond [(atom? e) (eq? e 'else)]
        [else #f]))

; cond-line から question 式をとる関数
(define question-of first)
; cond-line から answer 式をとる関数
(define answer-of second)

; cond-line のリストを評価していく関数
(define (evcon lines table)
  (cond [(else? (question-of (car lines)))
         (meaning (answer-of (car lines)) table)]
        [(meaning (question-of (car lines)) table)
         (meaning (answer-of (car lines)) table)]
        [else (evcon (cdr lines) table)]))

(define (*cond e table)
  (evcon (cdr e) table))

(*cond '(cond (#t 1) (#f 2) (else 3)) '()) ; => 1
(*cond '(cond (#f 1) (#f 2) (else 3)) '()) ; => 3
(*cond '(cond (val1 1) (val2 2) (else 3)) '(((val1 val2) (#f #t)))) ; => 3

evcon は cond-line を先頭から見ていく。 まず question 式が else の場合には #t だと見なすので、対応する answer 式を評価して返す。 評価には meaning を用いる。

そうでない場合、question 式を評価して #t であれば answer 式を評価して返す。 それ以外の場合は、残りの cond-line について再度 evcon を再帰的に評価する。

*lambda

*lambda アクションではノンプリミティブな非基本関数であることを示す (non-primitve ...) というリストを返す。 次の3要素を覚えておくようにしておく。

  • その時点の環境
  • 仮引数リスト
  • 関数本体
(define (*lambda e table)
  (build (quote non-primitive)
     (cons table (cdr e))))

(*lambda '(lambda (x y) (cons x y)) '(((y) (1))))
; => (non-pritmive (((y) (1)) (x y) (cons x y))

これらを踏まえると *lambda は (non-primitive (テーブル 仮引数リスト 関数定義)) というリストを返す。

それぞれを取得できるように次の便利関数も定義しておく。

(define table-of first)
(define formals-of second)
(define body-of third)

ちなみに *lambda のアクションで環境を保存しておくのは Scheme の静的(レキシカル)スコープの特徴。 動的スコープならここでテーブルを覚えておく必要はないのかな?

*application

最後は関数適用アクション。 関数適用は (fun arg arg arg arg ...) という形になるのでそれを踏まえて順にアクションを定義していく。

evlis

関数を適用する際、まずは引数をすべて評価する必要がある。 そこで、関数適用の実引数のリストをすべて評価してリスト化する関数 evlis を定義する。

(define (evlis args table)
  (cond [(null? args) '()]
        [else (cons (meaning (car args) table)
                    (evlis (cdr args) table))]))

処理としては与えられた実引数のリストを先頭要素を meaning で評価して、再帰して cons していく。

apply

次に関数を適用する apply メソッドを実装する。

;; プリミティブかノンプリミティブかを判定する関数
(define (primitive? fun)
  (eq? (first fun) (quote primitive)))  
(define (non-primitive? fun)
  (eq? (first fun) (quote non-primitive)))

(define (apply fun vals)
  (cond [(primitive? fun) (apply-primitive (second fun) vals)]
        [(non-primitive? fun) (apply-closure (second fun) vals)]))

apply メソッドは関数本体と実引数のリストを受け取る。 そして、関数本体がプリミティブかノンプリミティブに応じて別の関数を呼び出す。

apply-primitive と apply-closure はこの後定義する。

apply-primitive

(define (apply-primitive name args)
  (cond [(eq? name (quote cons)) (cons (first vals) (second vals))]
        [(eq? name (quote car)) (car (first vals))]
        [(eq? name (quote cdr)) (cdr (first vals))]
        [(eq? name (quote null?)) (null? (first vals))]
        [(eq? name (quote eq?)) (eq? (first vals) (second vals))]
        [(eq? name (quote atom?)) (atom?? (first vals) (second vals))]
        [(eq? name (quote zero?)) (zero? (first vals))]
        [(eq? name (quote add1)) (add1 (first vals))]
        [(eq? name (quote sub1)) (sub1 (first vals))]
        [(eq? name (quote number?)) (number? (first vals))]))

関数が primitive だった場合は、関数名を調べて正しい関数を適用するだけの処理。

apply-closure

最後に non-primitve な関数、すなわちクロージャーを適用する。

(define (apply-closure fun args)
  (meaning (body-of fun)
           (extend-table
             (new-entry (formals-of fun) args)
             (table-of closure))))

処理としてはけっこうシンプル。 やっている内容は分かりやすい文が載っていたのでそのまま転載する。

非基本関数(クロージャー)を値のリストに適用することは、(formals values) の形のエントリで拡張したテーブルのもとでクロージャーの本体の意味を求めることと同じです。

レキシカルスコープ

この時、クロージャーに含まれいてる環境を meaning に与えていることがポイントだと思う。 次のようにクロージャーのテーブルではなく評価時のテーブルを次のように与えることもできる。

(define (apply-closure fun args)
  (meaning (body-of fun)
           (extend-table
             (new-entry (formals-of fun) args)
             table))) ;; ← (table-of closure) ではなく table を与える

この違いは、レキシカルスコープか動的スコープかの違いになるのではないだろうか(たぶん)。

レキシカルスコープは (lambda ...) が評価された場所の環境が使えるためコード上で環境が見えやすい。 一方、動的スコープだと関数が呼び出された場所の環境に依存するためコードが分かりづらくなりそうだ。

動的スコープの言語はあまり触ったことがないのであまり感覚をつかめていない。 ただ、オブジェクト指向の this だけは動的スコープ的な評価になるというのは、どこかに書いてあって確かに「言われてみれば〜」という感じ。

*application

apply と evlis ができたので *application が作れるようになった。

(define (*application e table)
  (apply (meaning (function-of e) table)
         (evlis (cdr e) table)))

*application は、e の関数部分を評価し、引数リストを評価してから、apply する。

実例

ようやくすべてのアクションがそろったので、value が機能するようになった。

下記の式を例にして value の適用を順を追って見てみることにする。

((((lambda (y)
     (lambda (x)
       (lambda (f)
         (f x y))))
   2)
  1)
 (lambda (a b)
   (cons a b)))

適用は長くなるのでこちらに。

この関数の適用では、値の束縛が引数の呼び出し(スタック)に積まれていくのが分かる。

((lambda (f)
  ((lambda (x)
    ((lambda (y)
       (f x y))
     2))
   1))
  (lambda (a b)
    (cons a b)))

適用は長くなるのでこちらに。

こちらの関数の適用では値の束縛がクロージャの環境として保持されているのが分かる。

同じような関数呼び出しだが値の束縛を保持する位置が違うのが興味深い。

define は作っていないけど、Y コンビネータを使えば再帰が使えるので define は必須ではないことが分かる。

(value
 '(((lambda (le)
     ((lambda (f)
        (f f))
      (lambda (f)
        (le (lambda (x)
              ((f f) x))))))
    (lambda (length)
      (lambda (l)
        (cond
         ((null? l) 0)
         (else (add1 (length (cdr l))))))))
   (quote (1 2 3 4 5))))

さらに

さらに次のような課題が。

≪Y コンビネータによる変形を行うと、インタプリタ上でインタプリタを走らせることが可能であるということですか。≫

はい。でもそんなに悩まないでください。

Y コンビネーターを使えば作った value 上でさらにインタプリタを走らせることができるらしい。

こちらの課題に関してはこちらのブログがとてもよくまとまってたので、それを見て満足したのでやってない。 というか、本書全体を通してこちらのブログがとても細かく書かれていて参考になった。

kb84tkhr.hatenablog.com

おわりに

これで Scheme 手習いは終わり。 途中、半年くらいさぼってしまって終わるのに一年くらいかかった。

最初の7割と最後の3割くらいで、難易度がずいぶんと違う気がする。 継続や Y コンビネーター、eval について簡単にだけど学ぶことができたので満足。 それぞれが何を意味しているかくらいは理解した。

やっぱりこうやってブログにしながら学習すると理解がはかどるな。 時間はかかるけど。

気が向いたら Scheme 修行やるかな。なんか絶版っぽいけど。。。

Scheme 手習い(9)~ Y コンビネータ~

第9章:…… もう一度、もう一度、もう一度、……(続き)

第9章の最後は Y コンビネータについて。 リストの要素数を数える length を題材にして Y コンビネータを学ぶ。

length

リストの要素数を数えるなんの変哲もない関数 length を定義する。

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

length を define を使わずに定義できるだろうか?

define がなければ、関数名を定義できないので定義内で自身を参照できない。 これでは再帰ができないので length は定義できなくなってしまう。

でも、方法はある。それが Y コンビネータだ。 順を追って考えよう。

length-0, length-1, ..., length-n

まずは空リストの要素数を数えることができる関数を定義しよう。 ここで重要なのは define を使わず、lambda だけを使うことだ。

(lambda (l)
  (cond [(null? l) 0]
        [else (add1 (eternity (cdr l)))]))

空リスト以外だと無限ループするが、これで空リストの要素数は数えられるようになった。 これを length-0 と呼ぶことにする。

では、次に要素 1 以下のリストを数えられる関数を定義してみる。

(lambda (l)
  (cond [(null? l) 0]
        [else (add1 (length-0 (cdr l)))]))

define がないのだから length-0 は参照できない。 そこで、length-0 の定義をそのまま lambda を使った表記に置き換えることにする。

(lambda (l)
  (cond [(null? l) 0]
        [else (add1
                ((lambda (l)
                  (cond [(null? l) 0]
                        [else (add1 (eternity (cdr l)))]))
                 (cdr l)))]))

これを length-1 と呼ぼう。

同じようにしていけば length-2、length-3、...、length-n を定義できるはずだ。

ただ、見ての通りどんどん関数定義が長くなってしまう。 このままでは length は定義できない。

mk-length

そこで共通のパターンを抽象化して、同じ関数定義を繰り返し書かなくてもよいようにしたい。 さきほどの length-n の次の部分を共通化することを考える。

(lambda (l)
  (cond [(null? l) 0]
        [else (add1 (length (cdr l)))]))

定義内の length はどこにも存在しないため、このままでは参照できない。 そこで引数として length を外から与えられるようにする。

(lambda (length)
  (lambda (l)
    (cond [(null? l) 0]
          [else (add1 (length (cdr l)))])))

こうすれば length-n を定義できる。 例えば length-0 を作りたいなら eternity を渡せばよい。

((lambda (length)
  (lambda (l)
    (cond [(null? l) 0]
          [else (add1 (length (cdr l)))])))
 eternity) ; = length0

length-1 なら次のように length-0 を与えれば良い。

((lambda (length)
  (lambda (l)
    (cond [(null? l) 0]
          [else (add1 (length (cdr l)))])))
 length-0)

ただ、ここでも length-0 は参照できないので lambda を使った表記に置き換える必要がある。

((lambda (length)
  (lambda (l)
    (cond [(null? l) 0]
          [else (add1 (length (cdr l)))])))
 ((lambda (length)
    (lambda (l)
      (cond [(null? l) 0]
            [else (add1 (length (cdr l)))])))
   eternity)) ; = length1

さらに length-2 は次のようになる。

((lambda (length)
  (lambda (l)
    (cond [(null? l) 0]
          [else (add1 (length (cdr l)))])))
 ((lambda (length)
   (lambda (l)
     (cond [(null? l) 0]
           [else (add1 (length (cdr l)))])))
  ((lambda (length)
     (lambda (l)
       (cond [(null? l) 0]
             [else (add1 (length (cdr l)))])))
   eternity)))

ここまでの変形で同じような部分が連続するようになった。

共通する部分 (lambda (length) ... が何をしているかといえば、「外から次の再帰呼び出しの関数を受け取り length 関数を生成する」という処理だ。

(lambda (length)
  (lambda (l)
    (cond [(null? l) 0]
          [else (add1 (length (cdr l)))])))
; => これに名前を付けたい

そこでこの関数に make length を意味する mk-length という名前を付けることにする。

名前を付けるにはどうしたらよいか。 lambda を用いて引数としてさきほどの関数を与え、引数に mk-length と名前をつけることにする。

そうすると length-0 は次のようになる。

((lambda (mk-length)
   (mk-length eternity))
 (lambda (length)
   (lambda (l)
     (cond [(null? l) 0]
           [else (add1 (length (cdr l)))]))))

length-1 は次のようになる。

((lambda (mk-length)
   (mk-length
     (mk-length eternity)))
 (lambda (length)
   (lambda (l)
     (cond [(null? l) 0]
           [else (add1 (length (cdr l)))]))))

length-2 は次のとおり。

((lambda (mk-length)
   (mk-length 
     (mk-length
       (mk-length eternity))))
 (lambda (length)
   (lambda (l)
     (cond [(null? l) 0]
           [else (add1 (length (cdr l)))]))))

ここまでの変形でだいぶシンプルになった。念のため実行してみる。

(((lambda (mk-length)
    (mk-length 
      (mk-length
        (mk-length eternity))))
  (lambda (length)
    (lambda (l)
      (cond [(null? l) 0]
            [else (add1 (length (cdr l)))]))))
  '(1 2)) ; => 2

ただしく実行することができた。 しかし、まだ要素数が多くなるほど関数定義も増えてしまう点に変わりはなく、このままでは length は定義できない。

(mk-length mk-length)

今のままでは mk-length を何度も書かないと length を定義できない。 あともう少し、関数を変形すればこれを回避することができる。

まず、eternity について。 eternity は無限ループする関数なので、呼び出した時点で処理は終了しなくなる。 どうせ、eternity を呼んでしまったらそこで終わりなのだから、eternity ではなく何か別の関数、たとえば mk-length 自体を渡すように変更してみる。

((lambda (mk-length)
   (mk-length mk-length))
 (lambda (length)
   (lambda (l)
     (cond [(null? l) 0]
           [else (add1 (length (cdr l)))]))))

こうすると mk-length には length ではなく、mk-length が渡ってくるようになる。 そこで引数名を length から mk-length に変更する。

((lambda (mk-length)
   (mk-length mk-length))
 (lambda (mk-length)
   (lambda (l)
     (cond [(null? l) 0]
           [else (add1 (mk-length (cdr l)))]))))

この時点では mk-length に (cdr l) を渡してしまっている。 mk-length は関数を引数に受け取る関数なので、mk-length を渡す必要がある。

((lambda (mk-length)
   (mk-length mk-length))
 (lambda (mk-length)
   (lambda (l)
     (cond [(null? l) 0]
           [else (add1 ((mk-length mk-length) (cdr l)))]))))

(mk-length mk-length) は length を生成する呼び出しであり length 関数を返してくれる。 mk-length 関数に自分自身(mk-length)を渡すようにしたおかげで、再帰してリストを数える length が定義できるようになった。

これで define を使わずに length を定義できた。

(((lambda (mk-length)
    (mk-length mk-length))
  (lambda (mk-length)
    (lambda (l)
      (cond [(null? l) 0]
            [else (add1 ((mk-length mk-length) (cdr l)))]))))
 '(1 2 3 4)) ; => 4

これで一応、「define を使わずに length を定義する」という目的は達成することができた。

Y コンビネータ

ここまで define を使わずに length を定義できた。 さて、これを一般化して length に限らず、任意の再帰関数を define を使わずに定義できるか考えてみよう。

任意の再帰関数を定義するためには「再帰を行うための関数部分」と「再帰関数自体の定義部分」に分けることを目標とする。

さきほどの length から一つずつ関数を変形していこう。

((lambda (mk-length)
    (mk-length mk-length))
  (lambda (mk-length)
    (lambda (l)
      (cond [(null? l) 0]
            [else (add1 ((mk-length mk-length) (cdr l)))]))))

(add1 ...) の中にある (mk-length mk-length) は、length を生成する関数呼び出しになっている。 そこで、この関数を呼び出した結果を length という名前をつけて扱うことにする。

例によって、lambda の引数を使って、length という名前を付けることにする。

((lambda (mk-length)
  (mk-length mk-length))
 (lambda (mk-length)
    ((lambda (length)
       (lambda (l)
         (cond [(null? l) 0]
               [else (add1 (length (cdr l)))])))
     (mk-length mk-length))))

(mk-length mk-length) を事前に評価し、length という名前の引数に渡す形に変えた。 しかし、この定義では定義を評価しようとすると無限ループしてしまう。 引数の (mk-length mk-length) を評価する段階で、無限に展開しようとしてしまうためだ。

(mk-length mk-length) は必要な時に一度だけ評価されるようにしたい。 そこで、遅延評価されるように lambda を使って (mk-length mk-length) を呼び出せるようにする。

((lambda (mk-length)
  (mk-length mk-length))
 (lambda (mk-length)
    ((lambda (length)
       (lambda (l)
         (cond [(null? l) 0]
               [else (add1 (length (cdr l)))])))
     (lambda (x) ((mk-length mk-length) x)))))

これで無限ループすることなく length が定義できた。あともう少しだ。

次に、内部のまさに length 特有の処理を定義している部分を le という引数名にして、一番外側にくくり出す。

(lambda (le)
  ;; 再帰を行うための関数部分
  ((lambda (mk-length)
     (mk-length mk-length))
   (lambda (mk-length)
     (le (lambda (x) ((mk-length mk-length) x)))))
  ;; 再帰関数自体の定義部分。le という名前になる
  (lambda (length)
    (lambda (l)
      (cond [(null? l) 0]
            [else (add1 (length (cdr l)))]))))

これで「再帰を行うための関数部分」の部分と「再帰関数自体の定義部分」に分けることができた。

一般的にこの「再帰を行うための関数部分」を 適用順 Y コンビネータ と呼ぶ。 引数を mk-length のような名前ではなく、より一般的な f とすると次のようになる。

(define Y
  (lambda (le)
    ((lambda (f)
       (f f))
     (lambda (f)
       (le (lambda (x) ((f f) x)))))))

ついによく見かける Y コンビネータにたどり着いた。

これを使って階乗 fact は次のように定義できる。

(define fact
   (Y (lambda (f)
        (lambda (n)
          (cond [(= n 1) 1]
                [else (* n (f (- n 1)))])))))
(fac 5) ; => 120

どうやら正しく Y コンビネータを定義できたようだ。

ずいぶんと長くなってしまったが、一応 Y コンビネータについて理解した。 一通り書いてあることは理解したつもりだけど、本質的に理解できてはいないなという感じ。 もっと深い理解にたどり着くためにはもっと努力が必要なようだ。

これで Scheme 手習いもあと1章を残すのみとなった。

Scheme 手習い(8)

第9章:…… もう一度、もう一度、もう一度、……

keep-looking

(define (pick to lat)
  (cond [(= to 0) (car lat)]
        [else (pick (- to 1) (cdr lat))]))
        
(define (keep-looking a to lat)
  (cond [(number? to)
         (keep-looking a (pick to lat) lat)]
        [else (eq? a to)]))

引数のインデックスの要素が数字かシンボルかを判定する。 数字だった場合にはさらにその数字をインデックスとして要素を取得して同じ事を繰り返す。もし、シンボルだったら渡された a と一致するかを調べる関数。

この再帰は常にリストが短くなっていくわけではないので、無限ループする可能性がある。

これを「不自然な再帰」と呼んでいる。

eternity

(define (eternity x)
  (eternity x))

値を返さない関数。このような関数を部分関数というらしい。 入力に対して一部しか値が定義されないから部分関数と呼ぶのか?

shift

(define (build a b)
  (cons a (cons b '()))) 
(define (first a)
  (car a))
(define (second a)
  (car (cdr a)))
 
(define (shift x)
  (build (first (first x))
    (build (second (first x))
      (second x))))

第1要素がペアであるペアをとり、第2要素に第1要素の先頭をつけてペアを作る関数

align

(define (align para)
  (cond [(atom? para) para]
        [(a-pair? (first para))
         (align (shift para))]
        [else (build (first para)
                (align (second para)))]))

ペアで構成されたリストを整列させる関数。 たとえば ((a b) ((c d) (e f))) という入力の場合、出力は (a (b (c (d (e f))))) となる。

length* と weight*

(define (length* para)
  (cond [(atom? para) 1]
        [else (+ (length* (first para))
                 (length* (second para)))]))
(define (weight* para)
  (cond [(atom? para) 1]
        [else (+ (* (length* (first para)) 2)
                 (length* (second para)))]))

ペアで構成されたリストの要素を数える関数。 また、第一要素に重みをつけたのが weight* 関数。

weight* を使うと align が再帰するたびに、重みが軽くなっていることが分かるので、再帰が収束することが分かるらしい。 なんとなく理屈は理解できるけど、すべてのケースに置いて本当にそうなのかは不明。

C

(define (C n)
  (cond [(one? n) 1]
        [(even? n) (C (/ n 2))]
        [else (C (add1 (* 3 n)))]))

いわゆるコラッツ問題の関数。 この関数はすべての引数に対して値を返すかどうかはまだ分かっていない。

A

(define (A n m)
  (cond [(zero? n) (add1 m)]
        [(zero? m) (A (sub1 n) 1)]
        [else (A (sub1 n)
                 (A n (sub1 m)))]))

アッカーマン関数。 コラッツ問題と違いこの関数は全関数(必ず値を返す)ことが分かっているらしい。 ただし、計算量が膨大なため計算機では計算に時間がかかり現実的な時間で計算できないことが多い。

will-stop?

与えられた関数が停止するかどうかを調べられる関数 will-stop? が存在するかどうかを問う思考実験。

(define (last-try x)
  (and (will-stop? last-try) (eternity x)))

will-stop? が存在すると仮定して、上記のような関数を定義する。

last-try が停止すると仮定した場合、(will-stop? last-try) は true となる。 すると、(eternity x) が評価され、結果として (last-try x) は (eternity x) によって停止しない関数となってしまい、仮定と矛盾する。

一方、last-try が停止しないと仮定した場合、(will-stop? last-try) は false となる。 (last-try x) は false となるため (eternity x) は評価されずに停止してしまうので、仮定と矛盾する。

結果として、will-stop? が存在するという仮定が間違いだったことが分かる。

いわゆる停止性問題

Y コンビネータ

この章の残りは、リストの要素数を数える length を使って Y コンビネーターについて説明している。 長くなるので、それは次回に。

『超予測力』を読んで

『超予測力』を読みました。9 月の読書。

超予測者

『超予測力』は未来の予測についての本。

  • 「ロシアは今後三ヶ月以内に新たなウクライナ領土を正式に併合するか」
  • 「来年ユーロ圏を離脱する国はあるか」
  • 「北朝鮮は今年度中に核兵器を使用するか」

このような未来の問いに対する「予測の正しさ」についての研究成果がまとめられている。 著者はこのような問いに対して予測をしてくれるボランティアを広く集め大規模な実験を行った。 その結果、一部の予測者は統計的にも有意に優れた予測をし、CIA などの諜報機関で働くプロの情報分析官すら上回る成績を残した。

筆者は彼等を「超予測者」と呼ぶ。

彼らは以下のような特徴を持っていた。

  • 確率的にものごとを考える
  • 予測をサブ問題に分割して考える
  • 外側の視点と内側の視点を持っている
  • 常に予測を検証し必要に応じてアップデートする
  • 知的な誠実性を持っている

彼等の職業はばらばらで、専門分野も異なった。 筆者は超予測力は生まれつきの才能ではなく、その「やり方」や「考え方」に秘密があると述べる。 その方法を模倣することで、誰でも優れた予測力を身につけることができるのだ。

“専門家”と我々

一方、世間で予測を商売にしているアナリストや評論家などのいわゆる“専門家”は、超予測者とはならなかった。 予測の成績が振るわなかったのだ。 筆者がいうところによると「平均的な専門家の正確さは、チンパンジーが投げるダーツとだいたい同じぐらいである」ということだ。

新聞やテレビのニュース番組を見れば、専門家が今後どうなるか予測している。 ただほぼ例外なく、彼等がカメラの前にあっているのは予測力に優れているという何らかのお墨付きを得ているためではない。 予測の正確さが問題になることなどほとんどない。

そもそも評論家やコメンテーターに求められていることは、その正確性ではなく「予測に説得力があるかどうか」だと述べる。 人は説得力やつじつまが合うことを求める気質があり、正確性などは二の次になる場合が多い。

過去の予測は過去のニュースと同じで、すぐに忘れ去られ、評論家が過去の予測と現実に起きたこととを照合するよう求められることはまずない。 コメンテーターが明らかに持っておいる才能は、説得力ある節を確信持って語る能力であり、それだけで十分なのだ。

予測の消費者となる我々が予測の結果に対して無頓着であることに著者は警笛をならす。 経営者や政治家から我々に至るまで、有効性の確認されない薬は飲まないが、予測に関しては行商人が出してくる不老不死の薬と同じくらい怪しい者でもお金を払ってしまう。

予測が企業や政府など、社会にとって重要な位置付けであることは間違いない。 だからこそ、エビデンスを重視する科学的プロセスを取り入れて検証を行うことが社会にとって必要である。

余談

「人は説得力やつじつまが合うことを求める気質がある」という話は、カーネマンの『ファスト&スロー』のシステム1とシステム2の話に繋がっていて、この本でもよく引用されている。

www.thekingsmuseum.info

以前、読んだことがある本がいくつか引用されていてこの分野への興味がまた沸いてきた。

『ワークシフト』を読んで

ワークシフトを読みました。8月の読書。

ワークシフトは2011年に書かれた本。 未来の働き方、具体的には 2025 年の働き方について予測し、そこで起こっているであろう<シフト>について書かれている。 筆者は働き方において、これから 3 つのシフトが起こるだろうと述べている。

  1. ゼネラリストが評価される時代が終わり、高い専門性を持つスペシャリストが求められるようになる
  2. 人的関係に基づいたコラボレーションによるイノベーションが重要になる
  3. 経済的な成功と消費を求める画一的なキャリアゴールから、やりがいを求めるキャリアを築く必要がある

これら 3 つのシフトの具体例として筆者が想像した悲観的なストーリーと楽観的なストーリーが語られる。 悲観的なストーリーはこれらのシフトにうまく適応できなかった人々のストーリー、楽観的なストーリーはこれらのシフトにうまく適応できた人々のストーリだ。 そして、これらの想像上のストーリーを踏まえて 3 つのシフトをうまく乗りこなす術が書かれている。

ゼネラリストエンジニア

3 つのシフトはそれぞれ「確かにその通りだ」と感じたが、中でも特に気になったのが 1 つ目のシフト。

第一に、ゼネラリスト的な技能を尊ぶ常識を問い直すべきだ。 世界の50億人がインターネットにアクセスし、つながり合う世界が出現すれば、ゼネラリストの時代が幕を下ろすことは明らかだと、私には思える。

どうやらゼネラリストの価値は下がっていく一方のようだ。 反面、高い専門性を持つスペシャリストが求められるようになる。

省みて自分のキャリアはどうだろうか。 自分はプログラマー/エンジニアだと思っているが、世間的には技術職にあたるわけだしある程度の専門性がある職業だと思う。

でも、ソフトウェアエンジニアとして専門性の高いスペシャリストではないと最近感じる。 職業人生はそろそろ 10 年を迎えるが、何か一つの領域を長くやってきたわけではない。 Java の GUI を書き、C++ で HPC のミドルウェアを書き、Android もやったし Web のフロントエンドもそこそこやった。 サーバーサイドもそれなりにいじれるし、インフラ構築もそれなりにできる。 フルスタックエンジニアとポジティブに呼ぶこともできるが、端的にいえばソフトウェアエンジニアの世界でのゼネラリストなのかもしれない。

ゼネラリストの価値は下がっていく。 さて、自分のキャリアは大丈夫だろうか? でも、この本にはこんなことも書かれている。

新しい時代には、本書で提唱する「専門技能の連続的習得」を通じて、自分の価値を高めていかなくてはならない。 未来にどういう技能と能力が評価されるかを知り、その分野で高度な技術を磨くと同時に状況に応じて柔軟に専門分野を変えることが求められるのだ。

専門性は高めていかなければならないが、専門領域も時代に合わせて変化させていく必要がある。 なるほど。専門性を維持しつつ求められている専門領域にキャリアを常にシフトさせていく、というのは説得力がある。

自分のキャリアをポジティブに捉えるのならば、ずっと「ソフトウェア開発」には携わってきたわけで、それを軸にして時代に合わせた技術を習得すればよいのではないか。 勉強を続けなければいけないことは大変といえば大変だけど、この仕事は今でも楽しいと思えるのでそれほど苦にはならなさそうだ(能力的な限界が先にくるかもしれないが)。 ただ、どの能力・技術が評価されるかを見極めるのはとても難しく、常にアンテナを高く立てておく必要はあるだろうし、経験を思い切って捨てるようなキャリアを選ぶ柔軟性も必要になってくるだろう。

そうやって、自分のキャリアを見つめ直した一冊だった。

『リーダブルコード』を読んで

『リーダブルコード』を読みました。7月の読書。

内容はかなり初歩的で特に目新しいことはなかったかな。 ただ、これを実践できてるかは別で、書かれていることを意識してコード書いていきたいと思った。

日々、コードはシンプルで分かりやすくなるように気をつけているけど、他者からの視点を意識するというのは少し抜け落ちてた気がする。

最近、まとまったコード書いてなくてあまり良くないな〜と思う今日この頃…

(c) The King's Museum