The King's Museum

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

『デジタル・ミニマリスト』を読んで

『デジタル・ミニマリスト』を読みました。今年最後の読書。

『デジタル・ミニマリスト』は『DEEP WORK - 大事なことに集中する』などの著者、カル・ニューポートの本。 人々の生活に入り込み、多くの時間を支配するようになった SNS などのオンラインコンテンツについて警笛を鳴らしている。 人々の注意を引き、それを広告主売ることで利益を得る行為を「アテンション・エコノミー」と呼び、それと決別する実践的な方法を述べている。

筆者はアテンションエコノミーと決別するために、一ヶ月の「デジタル片付け」を行うことを推奨する。 そして、そのあと、本当に必要なデジタルツールやサービスを、それらに支配されることなくうまく利用するべきだと説く。 数日間デジタルツールから離れる程度では意味がなく、「デジタルツールとの根本的な関わり方を変える必要がある」と述べる。

ただ、決してデジタルツールを使うなということではなく「デジタルツールに生活を支配されることから逃れ、それらを能動的に使うことで幸せになれる」という姿勢だ。 すなわち、意図をもって必要最小限のデジタルツールを使う『デジタル・ミニマリスト』になることで、より充実した人生が過ごせるはずだと述べる。

--

去年、発売した時に読み始めたけど途中で辞めてしまった本。 筆者の著作である『DEEP WORK - 大事なことに集中する』は読んだことあって、考え方がけっこう好きなのでメルマガもフォローしていた。 数年前、この本を書くきっかけになる「デジタル・片づけ」をメルマガで募集していて、興味があったけど結局参加しなかった。 その後、この本が出たので読みたいと思っていた。

前々から少しずつ SNS は絶つようにしていて、最後に残っていたのはツイッターだった。 フォロー数も多くなくて、それほど多くの時間を使っている感覚はなかったけど、なんとなく頻繁にチェックしてしまう習慣は残っていた。 フォローしている人の近況を知るのはまぁいいとして、トレンドを見たりすると炎上しているニュースなどを目にしてしまい時間が奪われている感じはあった。 それにもやもやした気分になったりすることもあったので、時間だけでなく全体として自分のリソースを奪われていたと思う。

「デジタル・片づけ」を始めた当初は無意識にツイッターを開いてリロードをしてしまったりしていたけど(ちなみにこの行為をスロットを回すのと同じらしい)、1週間くらいたつとそれもなくなってほぼ気にならなくなった。 その後、夜に一度だけチェックするようになったがそれ以上にはなってないかな。 フォロー数も多くないから夜に一度チェックすれば数分でその日のツイートを見られるしね。

ただ、まぁそうして余った時間で何をするかっていうのは結構悩んでいる。 自分は基本的にあまり趣味と呼べるものを持っていない。 著者はなるべく手を動かす趣味を見つけろ、みたいなこと書いてたけど、わざわざそういう趣味を新たに見つけようという気も起きないし。 SNS は確かに注意を奪っているのでそれは納得なんだけど、その時間の使い方にあまり説得力はなかったかな。 地域社会に貢献しようとか、より素朴でリアルなゲーム(ボードゲームとか)がいいってのはあまり納得できなかった。

--

年初に毎月一冊本を読んで感想をブログに書くという目標をかかげたけど、一応達成できたな。 来年もこの習慣は続けていきたい。

今年のブログはこれで最後かな。 良いお年を。

転職。

転職しました。 12月1日から新しい会社で働いています。

前職は7年ほど勤めました。 修士卒で入社した一社目は2年7ヶ月で辞めてしまったので、それと比較するとずいぶん長く勤めたと思う。

転職の動機は「新しい環境でチャレンジしてみたくなった」というよくあるやつ。正直、前職の職場環境は良かったので特に不満はなかった。ただ、ハーズバーグのモチベーション理論で「仕事に不満がないことと、仕事に満足していることは異なる」という言葉があるように、不満はなくてもどこか満足してない部分があったのは事実かな。

--

それは、慣れからくる刺激のなさと自分の成長が感じられないところからきていたと思う。俗にいうキャリアプラトーというものだったのかもしれない。もちろん、転職せずとも刺激を見つけたり、自分次第でいくらでも成長できたのだろうけど、環境を大きく変えるのが手っ取り早いなと思って転職活動を始めた。

ここ数年は転職活動をしようと思っても、学生の頃に描いたキャリアイメージに縛られてしまい、今の自分とそのイメージの差から尻込んでしまっていた。でも、今年に入ってから発想を変えることができて、自由に考えられるようになった。これは『イノベーションオブライフ』を読んだ影響も大きい。

そんな状態でずいぶんと気楽に転職活動を始めてみると、よさそうな会社を見つけて面接を受け、縁があって内定をもらったのでそこに決めた。自分のキャリアを見つめ直す機会にもなったし、いい転職活動ができたかな。

--

前職では結構のんびり仕事してきたから、他の会社でやっていけるかな〜という不安がある。まずは試用期間を乗り越えないと。もちろん、そんな低いところに目標を置いていてはいけないが。

いろいろな事情で仕事に費やす時間は20代の頃と比べて減っているけど、そんな環境でも昔よりも結果を出していきたいし、そうしなければと思う。これまで働いてきて、そういう環境でも成果を出すやり方を多少なりとも磨いてきたつもりだし。

--

大学で新しく講義が始まった時、研究室に最初に配属された時、短期のインターンに挑戦した時、会社で働き始めた時、新しいプロジェクトに配属された時。そんな時に感じていたワクワク感みたいなものを久しぶりに味わっている。いつまでもこのワクワク感を感じながらキャリアを歩んでいけるといいんじゃないかと思った。

そのために、決して「定期的に転職する」ということではなく、いつでも新しい環境でチャレンジできる状態に自分を準備しておくことが必要かな。そのためのスキルを身につけることも必要だし、仕事以外の環境もうまく調整しておく必要があるかなと思う。

仕事だけが人生じゃないけど、仕事には人生の多くの時間を費やすのだから、満足できる時間を過ごせるようにしたいですね。

『財務3表一体理解法』と『財務3表図解分析法』を読んで

『財務3表一体理解法』と『財務3表図解分析法』を読みました。11 月の読書。

増補版 財務3表一体理解法 (朝日新書)

増補版 財務3表一体理解法 (朝日新書)

財務3表図解分析法 (朝日新書)

財務3表図解分析法 (朝日新書)

友達がおすすめしていた本で、前々から財務諸表を読めるようになりたいと思っていたのでちょうどよかったので購入。

財務3表である、貸借対照表(BL)、損益計算書(PL)、キャッシュフロー計算書(CS)を解説している本(株主資本等変動計算書の解説もある)。 その名の通り、財務諸表を個別に理解しようとするのではなく、3表のつながりを意識しながら説明している。

基本的な表の読み方は『財務3表一体理解法』の方で解説されているのだけど、この本だけだと数字の意味が分かるだけで「この数字をどう使えばいいの?」という状態になる。 その疑問に対して『財務3表図解分析法』の方では、書かれた数字から企業の状況や経営姿勢を読み取る方法について説明している。 この時に、実際の企業の財務諸表を使って解説しているので、親近感が持てるので理解しやすい。

世の中には財務諸表の解説本なんてたくさん出ているし、もっと分かりやすい本はあるのかもしれないけど、この本は自分にとってまさに必要十分という感じだった。 分量もそれほど多くなくてさくっと読めるので、二冊合わせてささっと読むのがおすすめです。

財務諸表の意義

財務諸表を読めるようにはなりたいと思っていたけど、企業の出す財務諸表にどれほどの意味があるのだろうと思っている。 企業の財務諸表は多かれ少なかれ他者に見られることが意識されていて、数字は意図的に操作されているだろうから。 規則に違反するほどの数字の操作は粉飾になるわけだけど、著者も述べているようにそのレベルの操作である粉飾さえ見抜くことは不可能に近い。

ただ、予め申し上げておきたいのは、財務諸表から粉飾を見抜くのは非常に難しいということです。 特に大企業の粉飾は、財務諸表を作る前の取引の認識の操作や、社外の仕掛けを使うなどして巧妙に仕組まれています。 財務諸表だけから粉飾を見抜くのは、極めて困難であると言わざるを得ません。

ということは、企業が思う「見られたい姿」を読み取ることはできても、企業の「本当の状況」を読み取ることは不可能に近いのではないだろうか。

あと、今の財務諸表ってソフトウェア産業のような業界の実情とは合わないという印象を受けた。ソフトウェア業界で固定資産割合とか重要じゃないだろうし、、、。 筆者も似たようなことを考えていて、ドラッカーの「資本主義社会のあとに訪れる知識社会」を引用して次のように述べている。

企業の情報を知るために、いまは財務諸表を使っていますが、これは資本主義社会に適応しているツールです。 しかし、資本にさほど意味がなくなる知識社会のビジネスでは、現在の財務諸表があまり大きな意味を持たなくなるでしょう。 なぜなら、意味のあるものは人間の知恵であり、その知恵を持つ人間は、自由に会社を移動するからです。 知識社会においては、高い値段で会社を買収しても、そこに知恵のあるキーパーソンがいなくなれば、いくら資産があっても会社としての価値はなくなってしまうのです。

でも、じゃあまったく意味がないかというとそうではなくて、少なくとも企業の一側面を捉える道具としては便利なツールだと思う。 それに、中身を知っているのと知らないのでは大違いだろうから、この本を読んで少しでも理解できるようになってよかった。

この著者、ドラッカーが好きらしく随所随所で言及されていて少し興味がわいてきたけど、この前読んだドラッカーの本がイマイチだったからどうしようかな。。。

ネクスト・ソサエティ

ネクスト・ソサエティ

『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 コンビネーターについて説明している。 長くなるので、それは次回に。

(c) The King's Museum