やろーじだい

ブログです

Aizu SICP 第12回

 やりました。範囲は P99 練習問題 2.6 ~ P101 練習問題 2.10 です。ここから読書会参加者で scheme のテストを利用するようにしました。(gauche 利用者は gauche.test を、 racket 利用者は rackunit を利用しています。) 今後は利用したテストケース (ただし gauche の方だけ) も記述しておきます。

練習問題 2.6

 しっかりチャーチ数について調べていないが、 id 関数に対して何回関数 f を適用するかによって数字というものを表現しているようだ。zero というものは f を受けとるが適用せずに捨てていることに注意したい。

(define zero
  (lambda (f) (lambda (x) x)))

(define (add-1 n)
  (lambda (f) (lambda (x) (f ((n f) x)))))

(add-1 zero)
;; ↓
(add-1 (lambda (f) (lambda (x) x)))
;; ↓
(lambda (f) (lambda (x) (f (((lambda (f) (lambda (x) x)) f) x))))
                             ---------------------------
                                  ;; ここが n
;; ↓
(lambda (f) (lambda (x) (f (((lambda (f) (lambda (x) x)) f) x))))
                            -------------------------------
                            ;; f を適用するが f は使われずに内側の (lambda (x) x) だけが残る
;; ↓
(lambda (f) (lambda (x) (f ((lambda (x) x) x))))
                           ------------------
                           ;; (lambda (x) x) に x を渡すと x になる
;; ↓
(lambda (f) (lambda (x) (f x)))
;; これが one

(define one
  (lambda (f) (lambda (x) (f x))))

;; 同様に one に add-1 を適用すれば two ができる
(define two
  (lambda (f) (lambda (x) (f (f x)))))

これはつまり、ある x に対して Haskellsucc 関数のようなものを適用することで次のものにするということを表現していると考えられるので、f として (+1) を、x として 0 を渡せば整数に変換できる。

 ちなみに hoogle:succ にあるように succHaskellEnum 型が持つ method の一つで、『後継 (successor)』を意味する関数になる。(数字だけではなく、順序が定義されていれば渡された値の次の値を返す)

 Scheme では (+1) として inc を定義する。

(define (inc x)
  (+ 1 x))

よって以下のように、zerotwo、またはそれらに add-1 したものに inc0 を渡すことによって整数に変換できる。

((zero inc) 0)
;=> 0
((two inc) 0)
;=> 2
(((add-1 (add-1 two)) inc) 0)
;=> 4

 また、例えば zero として #\afnext-char という手続きを定義して渡せば文字にも変換できる。

(define (next-char x)
  (integer->char (+ 1 (char->integer x))))

(((add-1 two) next-char) #\a)
;=> #\d

 次に加算手続き (plusとする) だが、考え方としては、まず最終的には inc0 を受けとれるような形である必要があるので以下のような形になる (... は未定義な場所とする)。

(define (plus a b)
  (lambda (f) (lambda (x) ...)))

次に、ab に入ってくるのは上で定義した onetwo であるので、例えば (plus one two) と呼ばれた場合の結果を考えると以下のようになる。

(lambda (f) (lambda (x) (f (f (f x)))))

これを踏まえると x に対して onetwo によって plus が受けとる f を適用すれば良いことがわかったので、結果は以下のようになる。

(define (plus a b)
  (lambda (f) (lambda (x) ((a f) ((b f) x)))))
;; Test
(test-section "practice 2.6")

(test "zero" 0
      (^[] ((zero inc) 0)))

(test "one" 1
      (^[] ((one inc) 0)))

(test "two" 2
      (^[] ((two inc) 0)))

(test "add-1" 3
      (^[] (((add-1 (add-1 one)) inc) 0)))

(test "plus" 3
      (^[] (((plus two one) inc) 0)))

(test "plus" 5
      (^[] (((plus two three) inc) 0)))

(test "character-church" #\d
      (^[] (((add-1 two) next-char) #\a)))

2.1.4 発展問題: 区間演算

二つ区間の割り算は、⼀つ⽬に⼆つ⽬の逆数をかけることにします。区間の逆数の下限と上限は、上限の逆数と下限の逆数という順番になることに気をつけてください。

(Page 100).

これに関しては、以下の練習問題 2.7 で気にしなくて良いようにした。

練習問題 2.7

 区間のオブジェクトは端点を二つ持ち、upper-bound (lower-bound) によって大きい方 (小さい方) を返すようにした。

(define (upper-bound interval)
  (max (car interval) (cdr interval)))

(define (lower-bound interval)
  (min (car interval) (cdr interval)))

計算量的な問題で以下のように make-intervalcar 部を小さい方、cdr 部を大きい方とするべきだろうと思う。と言うのも upper-boundlower-boundmul-interval などで同じオブジェクトに対して複数回呼ばれるため無駄な計算が増えてしまう。

(define (make-interval a b)
 (if (< a b)
     (cons a b)
     (cons b a)))
(test-section "practice 2.7")

(test "upper-bound" 3
      (^[] (upper-bound (make-interval 3 0))))

(test "lower-bound" 0
      (^[] (lower-bound (make-interval 3 0))))

練習問題 2.8

 差の場合は、引かれる方の最大値から引く方の最小値が、新しい区間の最大に、引かれる方の最小値から引く方の最大値が新しい区間の最小値になる。よって定義は以下のようになる。

(define (sub-interval x y)
  (make-interval (- (upper-bound x) (lower-bound y))
                 (- (lower-bound x) (upper-bound y))))
(test-section "practice 2.8")

(test "sub-interval"
      (cons 8 -2)
      (^[] (sub-interval (make-interval 10 3)
                         (make-interval 5 2))))

(test "sub-interval"
      '(100 . 0)
      (^[] (sub-interval (make-interval 100 25)
                         (make-interval 25 0))))

練習問題 2.9

区間の幅 (width) は、上限と下限の差の半分である。 幅は、区間によって規定される数値の不確かさの程度である。

(Page 101).

幅という単語からは上限と下限の差であることはイメージできるが、半分ということなので精度に近いものだろうということで理解した。

 まず具体例から考えてみる。以下では width という幅を求める関数には区間が渡され、区間[2,5] のように表現することにする。

// 加算
width([2, 5]) = 3.0/2 = 1.5
width([3, 8]) = 5.0/2 = 2.5
width([2, 5]) + width([3, 8]) = 1.5 + 2.5 = 4.0

add-interval([2,5], [3,8]) = [5, 13]
width([5, 13]) = 8.0/2 = 4.0

//減算
width([3, 8]) = 5.0/2 = 2.5
width([2, 5]) = 3.0/2 = 1.5
width([3, 8]) + width([2, 5]) = 2.5 + 1.5 = 4.0

sub-interval([3,8], [2,5]) = [-2, 6]
width([-2, 6]) = 8.0/2 = 4.0

よって、加算と減算に関しては width の結果の関数として表現可能できそうである。関数であるので width の結果の演算になっていれば良く、。これを示すと。

// 加算
// a < b, c < d とする
width([a, b]) = (b-a)/2 = t
width([c, d]) = (d-c)/2 = s
width([a+c, b+d]) = {b+d-(a+c)}/2 = {(b-a)/2} + {(d-c)/2} = t + s

// 減算
// 同じく a < b, c < d とする
width([a, b]) = (b-a)/2 = t
width([c, d]) =  (d-c)/2 = s
width([a-d, b-c]) = {b-c-(a-d)}/2 = {(b-a)/2} + {(d-c)/2} = t + s

よって関数となることを示せた。

 次に積算と除算については具体例で考えてみる。

//積算1
width([3,8]) = (8-3)/2 = 2.5 = t1
width([2,5]) = (5-2)/2 = 1.5 = s1
// それぞれの要素間の積は 3*2 = 6, 3*5 = 15, 8\*2 = 16, 8\*5 = 40 であるので
width([6,40]) = (40-6)/2 = 17 

//積算2
width([4,10]) = (10-4)/2 = 3.0 = t2
width([0,5]) = (5-0)/2 = 2.5 = s2
// それぞれの要素間の積は 0, 20, 0, 50 となるので
width([0,50]) = (50-0)/0 = 25

// 以上の二つを同じ関数では表現できないので、関数化不可能である。除算も同様だと思う。

練習問題 2.10

 ゼロを跨ぐ区間で除算を行うと問題となる理由は y=1/x のグラフを見てみるとわかりやすい (グラフ:wolframalpha)。

 0 での除算は未定義である上、 0 付近での除算が大きい数字になるにも関わらず、現在の div-interval では区間の両端でのみ考えているため誤った結果を返してしまう。

(define (div-interval x y)
  (mul-interval
   x
   (make-interval (/ 1.0 (upper-bound y))
                   (/ 1.0 (lower-bound y)))))

gosh> (div-interval (make-interval 10 20) (make-interval 5 -5))
(-4.0 . 4.0)

分母となる区間の両端の積の符号が負である場合はエラーを返すようにする。

(define (div-interval x y)
  (mul-interval
   x
   (if (>= (* (upper-bound y) (lower-bound y)) 0)
       (make-interval (/ 1.0 (upper-bound y))
                      (/ 1.0 (lower-bound y)))
       (error "ゼロを跨ぐ区間です"))))

gosh> (div-interval (make-interval 10 20) (make-interval 5 -5))
*** ERROR: ゼロを跨ぐ区間です

Aizu SICP 第10,11回

 一回一回の読書会が短めだったため二つ分です。

 第 10 回から 2 章に入りました。2 章は P231 までと長めですがここも面白そうな話題が多いです。

 P84 2 章 データを用いた抽象化の構築 ~ P 99 練習問題 2.6 までやりました。

読書会について

 読書会にあたって slack の Thread を利用しているのだが、コードなどを共有しようとするとかなり場所を取る上、コードのハイライトなどができない (そういう意図で使うことを想定していないと思うので当たり前なのだが) ため、コードの共有には Gist などを利用するべきだろうかと考え中。

2 データを用いた抽象化の構築

 ここからは焦点が手続きからデータに移って行く。

なぜプログラミング言語に複合データが必要なのでしょうか。その理由は、複合手続きが必要である理由と同じです。プログラムを設計する概念レベルを引き上げ、設計のモジュール性を高め、言語の表現力を強くしたいからです。 手続きを定義できることによって基本演算よりも高い概念レベルで手続きを扱 えるようになるように、複合データオブジェクトを構築できることによって、言語の基本データオブジェクトよりも高い概念レベルでデータを扱えるようになります。

(Page 85).

今更だが、この本では、抽象性が高いものが扱えるほど良いプログラミング言語であるという前提がある。少しそれについて考えてみると、ここでも言っている通り、それによって単純に言語の表現力が強くなるという点では良いのだが、抽象性が上がることによって発生する弊害というものありはする。

 例えば単純な例として、抽象度が高い言語を使っていると言語の裏でどのようなことが行われているのかというのが見えにくくなることがある。ただ、自分としても基本的には抽象度は高い方が良いだろうということには賛成である。というのも抽象度が高い言語というのは抽象度をあえて低くした状態で書ける。つまり大は小を兼るということであり、まずはとりあえず抽象度が低い状態で書いていき、抽象度を上げた方が良いと思ったタイミングで抽象度を上げることができる。最初から抽象度が高い状態でプログラミングをすることは (特に、書きながら考えているような時などのプログラムの仕様が明確でない場合) 難しい上に無駄になって辛いことが多い。

 したがって両方を兼るという意味で、プログラミング言語は抽象度が高い (表現力が強い) 方が良いと言って良さそうだと思った。


プログラムの中のデータオブジェクトをどうやって表すかを扱う部分と、データオブジェクトをどうやって使う かを扱う部分とを分離するという汎用的なテクニックは、データ抽象化 (data abstraction) と呼ばれる強力な設計手法です。

(Page 85~86).

 抽象化という単語についてだが、データをどのように表現するかとそれをどう使うかを分離すること、ここを指してデータ抽象化と呼ぶらしい。そう言われると、手続きの抽象化というのも、それがどう実装されているかとそれをどう使うかを分離することを指していることがわかった (しかし P88 の 2.1 ですぐにこれについて言及されているが、そこでも「手続き抽象化」というような単語は表れない。そもそもそれこそが手続きであるとも言えるかもしれない)。また、

ここでは、データ抽象化によって、プログラムの部品間に適切な抽象化の壁 (abstraction barrier) を建てられるようになるというこ とを見ていきます。

(Page 87).

この抽象化の壁というのも、データ抽象化が指す境界線を指しているように考えられる。


具体的には、データ主導プログラミング (data-directed programming) というテクニックを導入します。これは、それぞれのデータ表現を独立して設計し、それらを加法的 (additively) に (つまり、修正なしに) 組み合わせられるようにするものです。

(Page 88).

 最初は総称関数をイメージしたが多分それだろう。2 章の後半で表れるようだ。


2.1 データ抽象化入門

 データ抽象化についてもう少し詳しく書かれている。

2.1.1 例: 有理数の数値演算

まずは、分子と分母から有理数を構築する方法はすでに持っていると仮定しましょう。

(Page 89).

さらっと書かれていたが、これができるかどうかが抽象化が上手く働いているかの基準になりそうだ。まだ実装が無い状態でも、それがどのように実装されているかという詳細を気にする必要はないという抽象化の特性により、そもそもそれが存在していなくとも、それを使う手続きの実装時に (実際には動かないという以外の) 不都合は生じない。そしてこれを指して

ここでは、プログラムを合成していくうえでの強力な戦略である希望的思考 (wishful thinking) を使っています。有理数をどうやって表現するのか、numer, denom, make-rat という手続きをどう実装するのか、まだ何も言っていません。 それでも、もしこれらの手続きを持っていたとすると、足し算、引き算、かけ 算、割り算、等価性のテストは、次のような関係を使って実行できます。

(Page 89).

というように希望的思考 (wishful thinking) と呼ぶようだ。

練習問題 2.1

(define (make-rat n d)
  (let* ([abs-n (abs n)]
         [abs-d (abs d)]
         [g (gcd abs-n abs-d)]
         [sign (if (> (* n d) 0) 1 -1)])
    (cons (* sign (/ abs-n g)) (/ abs-d g))))

if は式なので (Lisp では基本的に全てが式であるので) どこでも使える。


 読書会の疑問に condif の使い分けについて疑問があがった。基本的に三分岐以上の場合は cond を、二分岐の場合は if、一分岐 (真また偽の時だけある処理をすること) の場合は whenunless を用いる。

2.1.2 抽象化の壁

一般的に、データ抽象化の底にある考え方は、それぞれのデータオブ ジェクトの型に対して、それさえあればその型に対するどんな演算も行えるような基本演算セットを特定し、その後はデータを操作するのにそれらの演算し か使わないようにするというものです。

(Page 93).

上の単純な例を引き続き使って、有理数パッケージを設計するにあたって、gcd を実行するタイミングを構築時にするか選択時にするかを最初の段階で決められないとします。この場合、データ抽象化という方法を使うことによって、その決定を後回しにしつつ、システムのほかの部分の開発を進めるということができるようになります。

(Page 95).

ここでも基本的に抽象化は良いものとして言われているが、問題が起こることを予期して抽象化を行うことのコストと、その問題が実際に起こることによるコストを考えると、どちらを取ればよいのかというのは本当に難しいように思う。

 例えば、この後の cons の実装例のように、有理数のデータを lambda を使うことになるかもしれない、ということを想定して、現在のペアで実装されている有理数へのアクセッサーとして carcdr の別名としてのアクセッサーを用意することは、明かに過剰な心配に思える。なので単に carcdr をそのままアクセッサーとして使っても良いのではないかとも考えられるだろうと思ってしまう。しかし、それとは逆に単に別名を与えるだけで抽象化の壁を構築できるならばしておくべきではないかとも考えられるというのもある。

 このようなバランスを考慮することは現在あたっている問題によって変わり、経験や勘が占める部分が大きいように思え、とても難しいところだなあと思った。

練習問題 2.2

(define (make-point x y)
  (cons x y))

(define (point-x point)
  (car point))

(define (point-y point)
  (cdr point))

(define (make-segment start-point end-point)
  (cons start-point end-point))

(define (start-segment seg)
  (car seg))

(define (end-segment seg)
  (cdr seg))

(define (midpoint access-f point1 point2)
  (/ (+ (access-f point1) (access-f point2)) 2))

(define (midpoint-segment seg)
  (let* ([start-point (start-segment seg)]
         [end-point   (end-segment seg)]
         [mid-x (midpoint point-x start-point end-point)]
         [mid-y (midpoint point-y start-point end-point)])
    (make-point mid-x mid-y)))

練習問題 2.3

矩形を表すために二点によって長方形が構成されているとした。

(define (make-rec point-a point-b)
  (cons point-a point-b))

(define (start-point rect)
  (car rect))

(define (end-point rect)
  (cdr rect))

(define (points-diff x-or-y p1 p2)
  (abs (- (x-or-y p1) (x-or-y p2))))

(define (periphery rect)
  (let ([sp (start-point rect)]
        [ep (end-point   rect)])
    (* 2 (+ (points-diff point-x sp ep) (points-diff point-y sp ep)))))

(define (area rect)
  (let ([sp (start-point rect)]
        [ep (end-point   rect)])
    (* (points-diff point-x sp ep) (points-diff point-y sp ep))))

2.1.3 データとは何か

一般的に、データというものは、何らかのセレクタとコンストラクタの集合に加え、それらが有効な表現となるために満たさなければならないと規定された条件によって定義されるものと考えることができます。5

(Page 96).

 つまりデータというのはこれさえ満たせればなんでも良いということになる。条件によって定義されるものとのことだが、この定義というものは基本的にセレクタとコンストラクタの定義に対する条件であるように思える。そうであるならばデータというのは条件付き手続きの集合というように見ていいのだろうか。

5 意外なことに、この考え方は厳密に定式化することが非常に難しいものです。

(Page 96).

面白そうな話題であるので今度ちゃんと調べてみたい。


(define (cons- x y)
  (define (dispatch m)
    (cond ((= m 0) x)
          ((= m 1) y)
          (else (error "Argument not 0 or 1: CONS" m))))
  dispatch)

;; これは以下のようにした方がわかりやすい気がする。

(define (cons- x y)
  (lambda (m)
    (cond ((= m 0) x)
          ((= m 1) y)
          (else (error "Argument not 0 or 1: CONS" m)))))

これは、今の段階ではただの面白い考え方のように思えるかもしれませんが、手続きによるデータの表現というものは、私たちのプログラミングのレパートリーの中で中心的な役割を果たしています。このプログラミングスタイルはよくメッセージパッシング (message passing) と呼ばれ、第 3 章でモデル化とシミュレーションの問題を扱う際には、これを基本的な道具として使うことになります。

(Page 98).

 自信が無いのだが (cons 3 2) という手続きによって作られる以下のようなデータがあるとき、

(lambda (m)
  (cond ((= m 0) 3)
        ((= m 1) 2)
        (else (error "Argument not 0 or 1: CONS" m))))

これに対してメッセージ 0 を送ると 3 が返り、メッセージ 1 を送ると 2 が返ってくる。これを指してメッセージパッシングと呼べると考えていいのだろうか。

追記:

(define (cons- x y)
  (lambda (m)
    (cond ((equal? m :car) x)
          ((equal? m :cdr) y)
          (else (error "Argument not 0 or 1: CONS" m)))))

gosh> ((cons- 3 4) :car)
3
gosh> ((cons- 3 4) :cdr)
4

こうすると Smalltalk っぽくなってよりメッセージパッシング感が出た。


練習問題 2.4

(define (cons- x y)
  (lambda (m) (m x y)))

(define (car- z)
  (z (lambda (p q) p)))

展開してみる。

(car- (cons- 2 3))
;;
(car- (lambda (m) (m x y)))
;;
((lambda (m) (m x y)) (lambda (p q) p))
;;
((lambda (p q) p) x y)
;;
x

正しく x が取り出せる。cdr については car と同じようにすればよい。

(define (cdr- z)
  (z (lambda (p q) q)))

練習問題 2.5

(define (cons- x y)
  (* (expt 2 x) (expt 3 y)))

(define (car-cdr pair base)
  (let loop [(pair pair) (count 0)]
    (if (= (modulo pair base) 0)
        (loop (/ pair base) (+ count 1))
        count)))

(define (car- pair)
  (car-cdr pair 2))

(define (cdr- pair)
  (car-cdr pair 3))

car-cdr では名前付き let と呼ばれるものを利用している。

練習問題 2.6

頭がごちゃごちゃになっていたので宿題

継続を用いた割り込み処理の実装 (のための継続の学習)

やりたいこと

 sleep などで入力した値だけ処理を停止させておきながら、キーボードが入力された時にそれを見て、それによって停止を中断したりするような割り込みのような処理をしたい。継続使えそうじゃないかと思ったので gauche で実装を試みた。

読む人の時間を奪わないためにまず結論

 最終的に、継続を使う以前に、sleep している処理と並列してキーボード入力を監視、ある入力を受け取ったら sleep から抜けるという方法はわからなかったが、I/O (今回はキーボード入力) に対してタイムアウトを実装すれば間接的に n 秒 sleep しつつ、入力を待つということが実装できることに気付いた。その点でググったら都合良く gauche を使って実装している例があったので解決した。長時間かかる処理にタイムアウトをつける

 継続についての理解が進んだので良かった。最終的に、最初に引数 n を渡して継続を利用した関数を作りそれを呼び出す度に毎回 1 秒スリープして n 回呼び出したら以降は sleep しないような関数、というものを作成したところで解決策がみつかったので終了した。

 以下は継続を勉強した時のメモになる。

継続

 以前から継続という概念については読んだりしていたが、実際に使用して何かをしたこともなく理解しているのかわからないのでまず継続について学びなおした。

まず適当に検索して見つけたページを読んだ。 なんでも継続

処理をひとつづきのパイプのようなものだと考えて、 どこでも良いがどこかをすぱっと切ってみる。その切り口の下側が上側の継続である(図3)。

概念の説明としてはここが一番しっくりきた。

 [末尾再帰と継続]で使われている木の葉の数を数えるプログラムを見てみる。

(define (leaf-count/cps tree cont)
  (if (pair? tree)
      (leaf-count/cps (car tree)
        (lambda (n)
          (leaf-count/cps (cdr tree)
            (lambda (m) (cont (+ n m))))))
      (cont 1)))

(define tree '((a . b) (c . d) . e))

gosh> (leaf-count/cps tree values)
5

(values は多値を返すための関数)

上のプログラムがパッと見難しいので順を追って評価した。lambda 式内で入れ子に表れる nm については区別のために便宜上 - を付与した。

(leaf-count/cps '((a . b) (c . d) . e) values)

;; ↓

(let ((tree '((a . b) (c . d) . e))
      (cont values))
  (leaf-count/cps (car tree)
                  (lambda (n)
                    (leaf-count/cps (cdr tree)
                                    (lambda (m)
                                      (cont (+ n m)))))))

;; ↓

(leaf-count/cps '(a . b)
                (lambda (n)
                  (leaf-count/cps '((c . d) . e)
                                  (lambda (m)
                                    (values (+ n m))))))

;; ↓

(let ((tree '(a . b))
      (cont (lambda (n)
              (leaf-count/cps '((c . d) . e)
                              (lambda (m)
                                (values (+ n m)))))))
  (leaf-count/cps (car tree)
                  (lambda (n-)
                    (leaf-count/cps (cdr tree)
                                    (lambda (m-)
                                      (cont (+ n- m-)))))))

;; ↓

(leaf-count/cps 'a
                (lambda (n-)
                  (leaf-count/cps 'b
                                  (lambda (m-)
                                    ((lambda (n)
                                       (leaf-count/cps '((c . d) . e)
                                                       (lambda (m)
                                                         (values (+ n m)))))
                                     (+ n- m-))))))

;; ↓

((lambda (n-)
   (leaf-count/cps 'b
                   (lambda (m-)
                     ((lambda (n)
                        (leaf-count/cps '((c . d) . e)
                                        (lambda (m)
                                          (values (+ n m)))))
                      (+ n- m-)))))
 1)

;; ↓

(leaf-count/cps 'b
                (lambda (m-)
                  ((lambda (n)
                     (leaf-count/cps '((c . d) . e)
                                     (lambda (m)
                                       (values (+ n m)))))
                   (+ 1 m-))))

;; ↓

((lambda (m-)
   ((lambda (n)
      (leaf-count/cps '((c . d) . e)
                      (lambda (m)
                        (values (+ n m)))))
    (+ 1 m-)))
 1)

;; ↓

((lambda (n)
   (leaf-count/cps '((c . d) . e)
                   (lambda (m)
                     (values (+ n m)))))
 (+ 1 1))

;; ↓

((lambda (n)
   (leaf-count/cps '((c . d) . e)
                   (lambda (m)
                     (values (+ n m)))))
 2)

;; ↓

(leaf-count/cps '((c . d) . e)
                (lambda (m)
                  (values (+ 2 m))))

というように評価されていく。このように見ていくと、cont にうまいこと以降に行われるべき処理が入っており、関数の呼び出し元に戻ることなく処理が最後まで行われることが伺える。

呼び出し側は、leaf-countの戻り値を受け取って処理を続けるかわりに、 leaf-countを呼び出す時点の継続を関数の形にしてcontとして渡すのだ。

というのはこれを言っているのがわかった。

継続渡し形式では、関数からのreturnという概念が無くなる。呼び出し元が指定した継続contを結果の値を引数として呼び出すことが、returnと等価になるわけだ。

これも上の評価で実際にそうなっているのがよくわかった。


 次に実際に Scheme 上で継続を使って実装する方法を探して、[お手元マルチスレッド]という項で継続を用いたマルチスレッド(もどき)を実装しているのでこれを読んでみた。使いたい人のための継続入門

Schemeにはcall/ccという呪文があって、 大域脱出だの例外機構だのマルチスレッドさえ実装できる。 しかし、call/ccは決して怪しい動作をするものなんかじゃなく、 ある意味、とても普通の関数である。 このことを理解してもらうために、call/ccなどと省略せずに、 call-with-current-continuationという正式名称に注目してみよう。

(fact/cps 10 (lambda (a) (* 2 a)))

fact/cps は上は[なんでも継続]であった reaf-count/cps と同じようなものなので、 (fact 10) を計算のあとの継続として (lambda (a) (* 2 a)) があるものだということになる。これを call/cc を使って書くと以下のようになる。

( 2 (call/cc (lambda (cont) (cont (fact 10)))))
contには(lambda (a) (
2 a))が渡ってくると考えられそうだ。 fooの素に相当する現在の継続の素を改めて書く必要はない。

ただし以下の話が続く。

;; call/ccの継続渡し形式への書き換えを試みてみる
((lambda (cont) (cont (fact 10)))
(lambda (a) ( 2 a)))
しかし、よくよく考えると、(
2 …)式の続きの計算はどうなるんだろう?
厳密に考えると、read-eval-print-loopがあるはずで、 次のread-eval-printが延々待ってるハズである。 これは以降の説明でも重要な点で、無視できないので仕方がない。 PRINT-AND-NEXT-REPLというものを便宜的に持ち込もう。 というわけで、次のようにするとしよう。
;; call/ccの継続渡し形式への書き換えを試みてみる(PRINT-AND-NEXT-REPL導入版)
( 2 ((lambda (cont) (cont (fact 10)))
(lambda (a) (PRINT-AND-NEXT-REPL (
2 a)))))

call/cc式の外側にある2倍を行う処理もそのまま残留している。 もし、
;; 継続渡し形式への変換と混同しちゃった版
((lambda (cont) (cont (fact 10)))
(lambda (a) (PRINT-AND-NEXT-REPL (* 2 a))))
という風な継続渡し形式への変換を予想された方は注意。 call/ccは継続渡し形式に変換する関数ではなく、 あくまで、call/cc式自身の継続(自分の値を待っている計算)を自動的に作り、 それをcall/ccの引数であるprocedureに渡して、 そのprocedureをcallするという関数だ。

見事に勘違いしていた。call/cc は外側の継続を持っているが、call/cc 継続自体の存在が call/cc の外側から無くなるわけではない。


保存した継続kは今までの説明のまま機械的に変換すると、 (lambda (a) (PRINT-AND-NEXT-REPL (+ x a z)))になるはずだが、 これは正確ではなかったということだ。
;; 実は不正確だった継続
(lambda (a) (PRINT-AND-NEXT-REPL (+ x a z)))
正しくは、(今のGaucheでは)継続kは (lambda (a) (PRINT-AND-NEXT-REPL (+ 1 a z)))だったのだ。 なぜなら、第二引数であるcall/cc式を評価する前に 第一引数であるxは評価されて1を得ていたからだ。

;; より正確な継続
(lambda (a) (PRINT-AND-NEXT-REPL (+ 1 a z)))
つまり、Gaucheでは左から順に引数を評価するために、

;; 引数の評価順とcall/ccの位置
;; (1) xが評価される
;; (2) call/cc式が評価される
;; (3) zが評価される
;; (4) +が3つの値に適用される

(+ x call/cc式 z)
この式の第二引数のcall/cc式を評価する前に、 すでにxの値が環境から取り出されて1になっており、 call/cc式の継続(続きの計算)には 「環境からxの値を取り出す」という計算は含まれてないのだ。

逆に言えば評価すらも継続の処理一部として考えられるということになる。


もっと細かいことを言えば、 (lambda (a) (PRINT-AND-NEXT-REPL (# 1 a z)))であり、 もし、継続を保存後に(set! + *)とか(set! + max)などとしても、 やはり加算を行なうのである。

Common Lisp ではリストの先頭は関数とみなしてまず引数を評価していたような気がするので確かめたところやはりそうだった。

CL-USER> (undef-func (print 3))

; in: UNDEF-FUNC (PRINT 3)
;     (UNDEF-FUNC (PRINT 3))
;
; caught STYLE-WARNING:
;   undefined function: UNDEF-FUNC
;
; compilation unit finished
;   Undefined function:
;     UNDEF-FUNC
;   caught 1 STYLE-WARNING condition

3 ; Evaluation aborted on #<UNDEFINED-FUNCTION UNDEF-FUNC {1003B1DCB3}>.
CL-USER>

コンパイル時の warning が出ているが、実行時には 3 と表示された後にエラーが出ている。つまり関数の評価の前に引数から評価されている。しかし以下のような話が続いた。

実は今のGaucheでは (lambda (a) (PRINT-AND-NEXT-REPL ((identity +) 1 a x)))なので 破壊後の+の値である#が計算されて(# 1 2 10)から10が返る。 最初の(identity +)が引数の評価より後なのだ。 実は+などの様な組込み関数を使わずにユーザ定義関数を使う場合にも最後に評価される。 というか、むしろ逆で組込み関数の場合に最適化のために 引数が評価されるより前に#に評価済みになっているのだ。

一応試してみる。

(define adding
  (lambda a
    (apply + a)))

gosh> (define k #f)
k
gosh> (define x 1)
x
gosh> (adding x (call/cc (lambda (cont) (set! k cont) 2)) x)
4
gosh> (set! x 10)
10
gosh> (k 2)
13
gosh> (set! adding max)
#<subr (max arg0 :rest args)>
gosh> (k 2)
10

確かに継続 k の実行時に addingmax として振る舞っている。 Common Lisp ではどうなってるのか (組み込み関数とユーザー定義関数を区別しているのか) を調べる手段がわからなかった。


 本題のお手元マルチスレッド (お手軽マルチスレッド?) の部分と部分継続の項の前例をやっていて継続についてある程度理解が進んだ気がした。次に 「sleep 中にキーボード入力で任意のタイミングで sleep を解除する」というものを実装しようとした。

 考えた方法は、継続を利用して呼び出す度に 1 秒 sleep する関数を実装し、それを呼ぶ度にキーボード入力を見る処理を挟むというもの。とりあえず sleep の部分を実装した。

(define cont-sleep #f) ; 継続保存用

(define (sleep/cps n)
  (let ([n-rest n])
    (call/cc (^[cc] (set! cont-sleep cc)))
    (if (not (= n-rest 0))
        (begin
          (sys-sleep 1)
          (set! n-rest (- n-rest 1))
          #t)
        #f)))

gosh> (sleep/cps 3)
#t
gosh> (cont-sleep)
#t
gosh> (cont-sleep)
#t
gosh> (cont-sleep)
#f

期待通りの動作をした。しかし、この間にキーボード入力を見に行くということをするとしても、このキー入力自体にタイムアウトなどがなければ、正しい時間スリープすることができないことに気がついた。ここで read に対してタイムアウトが実装できれば良いということに気付いたので検索したところみつかった。

 長時間かかる処理にタイムアウトをつける。詳細についてこのページで解説されているが、この関数では、継続を脱出のために利用している。

おわり

(16. 継続)http://www.shido.info/lisp/scheme_cc.htmlleaf-generator が全く理解できていない。

Aizu-SICP 第九回

やりました。今回の範囲は P76 1.3.4 ~ P81 1.3.4 終わりまでです。

1.3.4 返り値としての手続き

ここまでの例では、手続きを引数として渡す能力が私たちのプログラミング言 語の表現力を大幅に拡張するということを見てきました。この表現力は、返り 値自身が手続きであるような手続きを作成することによって、さらに強力にで きます。

(Page 76).

 例が少し複雑であるため目的を見失いがちであるが、ここでの重要なこと上の文の通り (SICPにおけるプログラミング言語の具体例としての) Scheme が手続きを受けとって新しい手続きを返すといった高い表現力を持っているということであり、その強力さを示すための例としてニュートン法などが利用されている。


(define (average-damp f)
  (lambda (x) (average x (f x))))
;; (Page 76). 

少しわかりづらかったが、練習問題 1.36 で使った式を使って実際にあてはめてみるとそのままであることがわかる。

f:id:lhcpr:20170705172841p:plain:w300


この定式化は、この手法の三つの考え方を明確に示しています。不動点探索、平均緩和法、そして関数 y → x/y です。この平方根の手法の定式化を1.1.7 節の 元のバージョンと比べると学ぶところがあるでしょう。これらの手続きは同じ プロセスを表現しているということに気をつけて、これらの抽象化を使って表現することでどれだけ考え方が明確になっているかを見てください。

(Page 77).

比べてみる。

;; 1.1.7

(define (improve guess x)
  (average guess (/ x guess)))

(define (good-enough? guess x)
  (< (abs (- (square guess) x)) 0.001))

(define (sqrt-iter guess x)
  (if (good-enough? guess x)
      guess
      (sqrt-iter (improve guess x) x)))

(define (sqrt x)
  (sqrt-iter 1.0 x))

;; 本節

(define tolerance 0.00001)

(define (fixed-point f first-guess)
  (define count 0)
  (define (close-enough? v1 v2)
    (< (abs (- v1 v2))
       tolerance))
  (define (try guess)
    (set! count (+ count 1))
    (let ([next #?=(f guess)])
      (if (close-enough? guess next)
          next
          (try next))))
  (try first-guess))

(define (average-damp f)
  (lambda (x) (average x (f x))))

(define (sqrt x)
  (fixed-point (average-damp (^[y] (/ x y)))
  1.0))

 最初に見た時は関数全体で見てしまったため、そこまで違うだろうか?と思ったが、1.1.7節では停止条件が「二乗すると x に近くなったら」という固有のものであった。しかし新しい方では不動点を求めるという一般化された形である。また1.1.7節の方では improve 内に (average guess (/ x guess)) が埋め込まれており値の更新式が確認しづらい。しかし新しい方では fixed-point にその関数を渡すようになっている。これらの二つは sqrt の定義を確認することですぐにわかるようになっている。これが「これらの抽象化を使って表現することでどれだけ考え方が明確になっているか」を表していると考えられる。

ニュートン法

 ニュートン法についてはここがわかりやすかった。


61 微積分の入門書では、ニュートン法は普通、xn+1 = xn − g(xn)/Dg(xn) という近似列として記述されています。私たちはプロセスに関して語る言語を持っているため、不動点の考えを使って手法の記述を簡単にできます。

(Page 78 注釈).

この文が読書会中参加者全員がいまいちピンと来ていなかったが、P78 の一番上の式というのは通常のニュートン法の式では無く、左辺が f(x) になっている。これはニュートン法を繰り返していった時に、最終的な結果として求まる x というものを代入した時に、f(x) = 0 となる (グラフとx軸が交差する) 点を表している。すなわちこの関数の不動点を求めることがニュートン法を用いて関数の解 x を求めることと同じになるのでこのような式で表現できる。注釈はそれを表していると考えられるが、読みとれなかった。

追記  「f(x) = 0 となる (グラフとx軸が交差する) 点を表している」と書いてしまったが、実際には勾配が 0 となる点でならば停止しうる。


多くの関数 g と、十分によい初期推測値 x に対して、ニュートン法は g(x) = 0 の解に急速に収束します。62

(Page 78).

「十分によい初期推測値」とはいったい何なのか。アマゾンの奥地へ飛ぶ必要がある。

 ただ、この後で定義する newtons-method を利用すると、第八回のまとめで書いたような方程式の場合でも、適当な値を入力しても基本的に解が求まるようになるのでそこまで気にしなくてもいいのかもしれない。しかしなぜニュートン法を利用すると解が求まりやすいのかを理解できていない。

追記  機械学習における重みの更新と同様に、勾配の逆方向に x は更新されていくので初期値から最も近い解の近付いていくのだろうと考えた。解に近づくにつれて勾配は小さくなって行くので基本的に収束するということもわかった。

 以下は x^2 + 5x + 6 を初期値 35ニュートン法を使った例。

gosh> (newtons-method (^[x] (+ (* x x) (* 5 x) 6)) 35)
#?-    16.253335843501315
#?-    6.883335900113309
#?-    2.2049919310939874
#?-    -0.12093403572980543
#?-    -1.2579229998223325
#?-    -1.77832152189135
#?-    -1.965952134171057
#?-    -1.9989143418927326
#?-    -1.9999988130790058
#?-    -1.9999999999867224
-1.9999999999867224

抽象化とファーストクラス手続き

プログラマである私たちは、プログラムに潜んでいる抽象化を見つけ、それらの抽象化を使ってプログラムを構築し、さらに強力な抽象化を生み出すた めにそれらを一般化する機会を見逃さないようにしなければいけません。これは、いつでも可能な限り抽象的にプログラムを書かなければいけないということではありません。熟練プログラマであれば、タスクに対して適切な抽象レベルを選べるものです。しかし、新しい状況でも適用できるように、このような抽象化によって考えられるようになっておくことは重要です。高階手続きが重要なのは、これらの抽象化を明示的に私たちのプログラミング言語で表現することを可能にし、ほかの計算の要素と同じように扱えるようにしてくれるからです。

(Page 80).

この項のまとめ。

一章を終えて

 ここまでで練習問題の一部 (過半数) を除く一章を読み終えました。数学の話が非常に多くなる場所であることは聞いており不安な場所だったが、読書会という形で参加者で集まって考え合うことができたおかげで楽しく読むことができた。一人では多分読み飛ばしてしまっていただろうと思うので、ここを読書会で読めたのは非常に助かりました。参加者も数学的な場所に対して自分が怖じ気ついている時に「やりましょう!」というような形で後押ししてくれたのでありがたかったです。

 謝辞のようになりました。

 また本来この Aizu-SICP の記事は参加できない方向けに書くというモチベーションで始めましたが、現在は読書会の後で再び一人で読みなおし、そのまとめだけでなく、議論などをふまえて理解をさらに進めるために書くようになりました。実際に読書会の時には少し曖昧だったものがこの記事をまとめている内に考えが深まる経験をしているのでまとめを書くようにして良かったと思います。

 少し問題があるとすれば、参加者のひらめきも自分が思いついたかのように書いてしまっている箇所があるので、この SICP に関する記事は参加者達のおかげで書けているという点も、もし参加者以外に読んでいる方がいればご留意頂きたいです。

 やはり謝辞のようになりました。

 次回は飛ばした練習問題を解く予定です。

Aizu-SICP 第八回

やりました

1.3.3 汎用手法としての手続き

区間二分法によって方程式の根を求める

 区間二分法についてはグラフを適当に書いてみるとわかりやすい。ここ のように、プログラミングを用いた方程式の解の導出の題材として多く使われるようだ。

search を直接使うと、ちょっと面倒なことがあります。うっかり f の値が符号の条件を満たさないような点を与えてしまうかもしれないということです。その場合、間違った答えになってしまいます。そうする代わりに、以下に示す手 続きを経由して search を使うことにしましょう。これは、どちらの端点が負の関数値を持ち、どちらが正の関数値を持つかを調べ、それに応じて search 手続きを呼ぶというものです。

(Page 71~72).

 起こりうるミスを未然に防ぐための関数。こういう関数を表す名前があると良いと思った。

関数の不動点を求める

関数を受け取ってそれの不動点を求める関数 (この本における fixed-point という関数) は不動点コンビネータと呼ばれるものである。

すなわち高階関数g が不動点コンビネータであるとは、 任意の関数 f に対し、p = g(f) とすると, f(p) = p が成立する 参考:wiki

 方程式を解くのにも応用できる。例えば単純な三角関数 sin(x) の解を求めてみる。既に私達は ±nπ (n ⊂ N) が解であることを知っているので、結果がこれの何かになればよい。

 sin(x)0 となるところが解であるので sin(x) = 0. 次にこの式の両辺に x を加えて sin(x) + x = x のようにすることで不動点が求められるかどうかを調べることができる形になる。というのはこの式の左辺の sin(x)0 になるような x 、つまり sin(x) の解であるときに限り x = x という関数と見ることができ、それの不動点は明かに x となる。以下で fixed-point を使って実際に調べている (また毎回の関数適用時の値を出力している)。

(fixed-point (^[x] (+ (sin x) x)) 1.0)
#?-    1.8414709848078965
#?-    2.80506170934973
#?-    3.135276332899716
#?-    3.141592611590653
#?-    3.141592653589793
3.141592653589793


gosh> (fixed-point (^[x] (+ (sin x) x)) -1.0)
#?-    -1.8414709848078965
#?-    -2.80506170934973
#?-    -3.135276332899716
#?-    -3.141592611590653
#?-    -3.141592653589793
-3.141592653589793

1.0 が初期値である例の実際の計算は以下のようになってる

f(x) = sin(x) + x
のとき
x = 1.0
f(x) = 0.8414709848078965 + 1.0                 = 1.8414709848078965

x = 1.8414709848078965
f(x) = 0.9635907245418334 + 1.8414709848078965  = 2.80506170934973

x = 2.80506170934973
f(x) = 0.3302146235499861 + 2.80506170934973    = 3.135276332899716

x = 3.135276332899716
f(x) = 0.006316278690937182 + 3.135276332899716 = 3.141592611590653

x = 3.141592611590653
f(x) = 4.19991402881363e-8 + 3.141592611590653  = 3.141592653589793

このように不動点が求められ、同時に sin(x) の一つの解の近似が (誤差は tolerance の値に依存) 3.141592653589793 であることがわかった。

 しかし不動点を求めるにあたって、初期値 (fixed-point における first-guess) をどのように決めるかが問題になる。本の中ではこれに関して言及されていないため謎だった。今回の場合は sin(x) が周期関数であるため多くの入力に対して解をみつけることができるが、単純な方程式 (例えば x^2 + 5x + 6 のようなもの) は入力に対して出力が大きすぎる上に解が -2, -3 だけであるため収束するのが難しい。

 上記の対策として挙げられているのが平均緩和法であるが、結局のところ良い入力というのが何であるかの判断というのは、関数が複雑になるにつれて非常に難しくなってしまう。初期値を決める問題はアルゴリズムなどのパラメータの決定と同じような問題になるのだろうか。

練習問題 1.35

黄金比 φ(1.2.2 節) が x → 1 + 1/x という変形の不動点であることを示し、そのことを利用して φ を fixed-point 手続きによって求めよ。

(Page 74).

 まず φ = (1 + √5) / 2x → 1 + 1/x不動点であることを示す。

f:id:lhcpr:20170705120257p:plain:w300

次に fixed-point へは単に 1 + 1/x を与えればよい。

gosh> (fixed-point (^[x] (+ 1 (/ 1 x))) 1.0)
#?-    2.0
#?-    1.5
#?-    1.6666666666666665
#?-    1.6
#?-    1.625
#?-    1.6153846153846154
#?-    1.619047619047619
#?-    1.6176470588235294
#?-    1.6181818181818182
#?-    1.6179775280898876
#?-    1.6180555555555556
#?-    1.6180257510729614
#?-    1.6180371352785146
#?-    1.6180327868852458
1.6180327868852458

練習問題 1.36

 既に表示はしているので log の計算のみ。

gosh> (fixed-point (^[x] (/ (log 1000) (log x))) 1.1)
#?-    72.47657378429035
#?-    1.6127318474109593
#?-    14.45350138636525
#?-    2.5862669415385087
#?-    7.269672273367045
#?-    3.4822383620848467
#?-    5.536500810236703
#?-    4.036406406288111
#?-    4.95053682041456
#?-    4.318707390180805
#?-    4.721778787145103
#?-    4.450341068884912
#?-    4.626821434106115
#?-    4.509360945293209
#?-    4.586349500915509
#?-    4.535372639594589
#?-    4.568901484845316
#?-    4.546751100777536
#?-    4.561341971741742
#?-    4.551712230641226
#?-    4.558059671677587
#?-    4.55387226495538
#?-    4.556633177654167
#?-    4.554812144696459
#?-    4.556012967736543
#?-    4.555220997683307
#?-    4.555743265552239
#?-    4.555398830243649
#?-    4.555625974816275
#?-    4.555476175432173
#?-    4.555574964557791
#?-    4.555509814636753
#?-    4.555552779647764
#?-    4.555524444961165
#?-    4.555543131130589
#?-    4.555530807938518
#?-    4.555538934848503
4.555538934848503

36ステップだった。

 次に平均緩和法を利用した式の変形と変形後の結果は以下のようになる。

f:id:lhcpr:20170705123143p:plain:w300

gosh> (fixed-point (^[x] (/ (+ (/ (log 1000) (log x)) x) 2)) 1.1)
#?-    36.78828689214517
#?-    19.352175531882512
#?-    10.84183367957568
#?-    6.870048352141772
#?-    5.227224961967156
#?-    4.701960195159289
#?-    4.582196773201124
#?-    4.560134229703681
#?-    4.5563204194309606
#?-    4.555669361784037
#?-    4.555558462975639
#?-    4.55553957996306
#?-    4.555536364911781
4.555536364911781

13 ステップだった。もとの式によるだろうが少なくとも今回の場合はステップ数が半分以下になり効果が大きいことがわかった。

 練習1.37 ~ 1.39 は一旦飛ばした。

イギリスに行った

イギリスに行ってきた。エクスター (Exeter) という町でCYBCONFという学会があり、論文が無事受理されたので発表した。海外の学会に論文が受理されたのは初めて (提出は2回目) だったので家で小踊りした。以下箇条書きでの記録と写真など。普段あまり写真を取るタイプでは無いけどさすがにテンションが上がっていたのでパシャパシャ撮りまくってしまった。

初日

  • 電車で財布を紛失した (寝てる隙 or 置き引き or エスカレーターの後ろ)
    • 電車を降りて 30 秒で財布が無いことに気付き、終点だったので電車に戻って探したりしたが無かった
    • 別な財布にお金とクレカを入れていたのである程度被害は少なくて済んだ (被害額3万程度)
    • 海外でのいざという時のための準備が日本で役に立つとは思わなかった
    • 帰国後、駅のゴミ箱の中で発見されたが現金は抜かれていた
  • 日本から香港経由でロンドンへ 待ち時間含めれば 20 時間近くかかった

f:id:lhcpr:20170628104515j:plain:w300 f:id:lhcpr:20170628104520j:plain:w300 f:id:lhcpr:20170628104525j:plain:w300

  • ロンドンの空港からエクスターへ電車で4時間ほど
    • 電車の中に売店があったのでサンドイッチを食べた
    • ロンドンとエクスターの間は平野が延々と続いていて良かった

f:id:lhcpr:20170628105138j:plain:w300 f:id:lhcpr:20170628105146j:plain:w300

  • エクスター到着

f:id:lhcpr:20170628105142j:plain:w300

  • かもめがそこら中にいた
    • 現地ではカラスみたいな扱いらしい

f:id:lhcpr:20170628105327j:plain:w300

  • 謎すぎる広告

f:id:lhcpr:20170628105418j:plain:w300

  • 寿司屋さん ポルトガル行ったときに入った寿司屋がえらく不味かったので入らないことにした

f:id:lhcpr:20170628105532j:plain:w300

  • 夏至前日で日没時間が 21:30 というのを知る
  • 夕飯は近くで見付けたフランス料理店に入る

    • 物価が少し高く一品 £9~15 (\1260~2100) くらいした
    • イギリスではどこもこの程度でなかなかしんどかった
    • 頼んだ飲みものがカップルが頼むようなやつだったらしく恥かしい思いをした (おいしかった)

    f:id:lhcpr:20170628105632j:plain:w300 f:id:lhcpr:20170628105712j:plain:w300

  • はちゃめちゃに疲れていたので現地時間の 19 時くらいに寝た

二日目

  • ホテルの朝食はビュッフェ形式のイングリッシュブレックファストで肉やチーズなどかなりおいしかった

    • これを毎日食べたが、イギリスで食べた料理で一番おいしかった

    f:id:lhcpr:20170628105750j:plain:w300

  • 発表当日だった徒歩で会場に向う

    • 泊まった所から10分ほどのホテルで行われた

    f:id:lhcpr:20170628105827j:plain:w300 f:id:lhcpr:20170628105840j:plain:w300

  • 発表は初日の昼頃だった

  • 部屋にあまり人が居なかったので緊張感は意外と無かった
  • 質問も予期していたような内容だったのであまり焦らずに答えられた
  • 昼食はサンドイッチ
  • 昼食後は一度帰りエクスター大学がJ・K・ローリングの母校ということで拝みに行った
  • 町並みもおしゃれだった

f:id:lhcpr:20170628110453j:plain:w300 f:id:lhcpr:20170628110457j:plain:w300 f:id:lhcpr:20170628110501j:plain:w300 f:id:lhcpr:20170628110504j:plain:w300 f:id:lhcpr:20170628110508j:plain:w300 f:id:lhcpr:20170628110512j:plain:w300 f:id:lhcpr:20170628110516j:plain:w300 f:id:lhcpr:20170628110636j:plain:w300

  • エクスター大学

f:id:lhcpr:20170628110640j:plain:w300 f:id:lhcpr:20170628110643j:plain:w300 f:id:lhcpr:20170628110647j:plain:w300

  • 煉瓦作りで結構ボロいのと合わせて異様にゴミ箱があるので外からは生活感が希薄でなんとなく不気味なところが多かった

f:id:lhcpr:20170628110830j:plain:w300

  • 城の跡地や大聖堂を見た

f:id:lhcpr:20170628111001j:plain:w300 f:id:lhcpr:20170628111006j:plain:w300 f:id:lhcpr:20170628111009j:plain:w300 f:id:lhcpr:20170628111013j:plain:w300 f:id:lhcpr:20170628111016j:plain:w300 f:id:lhcpr:20170628111020j:plain:w300 f:id:lhcpr:20170628111024j:plain:w300 f:id:lhcpr:20170628111027j:plain:w300

f:id:lhcpr:20170628111329j:plain:w300 f:id:lhcpr:20170628111332j:plain:w300 f:id:lhcpr:20170628111336j:plain:w300 f:id:lhcpr:20170628111340j:plain:w300 f:id:lhcpr:20170628111345j:plain:w300

f:id:lhcpr:20170628111614j:plain:w300 f:id:lhcpr:20170628111618j:plain:w300 f:id:lhcpr:20170628111621j:plain:w300 f:id:lhcpr:20170628111625j:plain:w300 f:id:lhcpr:20170628111630j:plain:w300

  • 結構早めに帰って同行者と遊んで寝た

三日目

  • 学会の他の発表を聞いたりした
  • 昼飯も学会で食べたが立食だったのでゆっくりできなかった
  • Panel session (教授が集まって議論のようなものをする) がスピード感があって良かった
    • タイトルは Cyber-X (Cyber-security などの一般化) について今後の展望とかでおもしろかった
    • よく考えたらネイティブの英語に触れる機会が全然無かった (大学も留学もアメリカ・イギリス系の人と話すことがなかった) ので初めて長い間その英語に触れた
    • 勉強して流暢になった人の英語とは違い生まれた時からほぼ英語だけを使っている人達の英語は、そこの国の人達が把握できる程度に単語や文・発音が最小限になっているようで (要出典) 全然聞きとれない・補完できないで絶望した
      • 単に英語力が無いだけかもしれない
  • 午後は観光した博物館のような場所を観光した

f:id:lhcpr:20170628113708j:plain:w300 f:id:lhcpr:20170628113711j:plain:w300 f:id:lhcpr:20170628113715j:plain:w300 f:id:lhcpr:20170628113718j:plain:w300 f:id:lhcpr:20170628113722j:plain:w300 f:id:lhcpr:20170628113725j:plain:w300 f:id:lhcpr:20170628113729j:plain:w300 f:id:lhcpr:20170628113732j:plain:w300 f:id:lhcpr:20170628113736j:plain:w300

  • 自動ドアの開き方に感動して動画を取った

  • 学会の夕食会に参加した
  • テーブルの隣りに中国人の人が座ったが、指導教員が会津大学の10期生 (中国人で現在はイギリスの大学に所属) らしくびっくりした
    • その教授が日本語を使えるので、隣りに座った人も日本語が使えていろいろ喋り Line 交換したりした
  • 10人くらいの席だったので、他の人とも英語で喋ったが、中国人が異様に多く基本的中国語で話しておりあまり会話には混ざれなかった
  • 英語は聞きとりやすかったので助かった
  • 帰って同行者と遊んだ
  • 意外と空き時間があったので Smalltalk をやりはじめた

四日目

  • 観光した
    • 近くに賑っている川があったのでブラブラしたりハンバーガー食べたりした
    • 天気が悪かったので早めに帰ってゲームしたり Smalltalk やったりして早めに寝た

f:id:lhcpr:20170628114856j:plain:w300 f:id:lhcpr:20170628114859j:plain:w300 f:id:lhcpr:20170628114903j:plain:w300 f:id:lhcpr:20170628114908j:plain:w300 f:id:lhcpr:20170628114911j:plain:w300 f:id:lhcpr:20170628114915j:plain:w300 f:id:lhcpr:20170628114919j:plain:w300 f:id:lhcpr:20170628114922j:plain:w300 f:id:lhcpr:20170628114926j:plain:w300 f:id:lhcpr:20170628114929j:plain:w300 f:id:lhcpr:20170628114933j:plain:w300 f:id:lhcpr:20170628114936j:plain:w300

最終日

  • 相変わらずホテルの朝食はおいしかった
  • 電車と飛行機を乗り継いで延々と揺られていた
  • 日本に着いたのが23時くらいだったので空港近くのホテルで寝た
    • 寝坊してチェックアウトの時間過ぎてからのフロントからの電話で起きた

おわり

想像以上に時間に余裕があってゆっくり観光できたので良かった。財布の盗難のせいで海外で金銭的余裕が無かったのはちょっと辛かったが、免許証など戻ってきたのであまり気にしないことにした (可能なら取り戻したいが難しそうなので)。郡山駅で先週多発していたらしいので、利用する方はお気を付けください。

Smalltalk で遊び初めたので MacOS における処理系の選択肢についてと数日触った所感

 自由自在 Squeak プログラミング(PDF版) を読んでいます。2年前くらいにいつか Smalltalk 触ってみたいとつぶやいていましたが、最近 Smalltalk が好きな方 (id:phaendal) とお話しする機会があり,その時簡単に教えてもらったこともあり触ってみています。

処理系の選択肢

 OS X El Capitan version 10.11.6 情報です。バージョンが違うと状況が変わる可能性があります。近い内アップデートするつもりなのでその後の状況についても更新します。

 自分がインストールして試したのは Pharo 5.0, 6.0, Squeak 5.1 の三つで,Pharo は最初からコードの色付け・補完などが実装されており整った環境がすぐに得られたのでこちらがおすすめな気がします。

 さらに、簡単に日本語を表示できるもの (http://qiita.com/newapplesho/items/d541bfdc1c54f273cea9 この記事での通りに日本語を表示できるもの) は現状 5.0 だけのようなので,日本語を表示したい場合は現在は 5.0 をインストールするべきかもしれません。ただ,6.0 ではシステムフォントが update を押しても入ってこないという問題であるため,単に日本語の表示の問題ではないです。うまくパスなどを通し日本語フォントが選択できれば 6.0 でも問題なくできる可能性があります。

 読んでいる書籍 (自由自在 Squeak プログラミング) は Squeak の処理系を使っていることが前提であるため,ところどころ違う点があります。例えば Halo が無い?ことや名前の違い (Squeak: Workspace, PopUpMenu, Pharo: Playground, basicMenuPopUp)、メニューから Morph 作成方法が見当たらないなどなど。これは Playground で Morph new openInWorld で作成可能。Transcript は Spotter (Shit + Enter) から見つかります。ただ基本的なキーバインドは同じなので 1, 2章を読んだ時点では少し調べれば解決できる程度にしか問題はおきていません。

 こちらのブログ で書かれているチュートリアルや公開されている Pharo の入門書の日本語訳 が参考になりそうです。

はまったこと

 上記の処理系全てをインストールした状態で open コマンドなどでイメージを起動するとどれで起動しているのかわからなくなります。前述の通り 5.0 しかフォントを上手く指定できなかったので最初はできていたことが突然できなくなるなどして困りました。現在は Pharo 5.0 以外をアンインストールして対応しました。

楽しさ

 Smalltalk によって構築された世界 (仮想環境) で開発を楽しめます。言語固有のOSが提供されている感覚で,文字通り Smalltalk の世界でプログラミングを行うので通常のプログラミング言語を学ぶ時とは全く違っています。言語仕様を見るだけではわからない感覚だと思うので気になっている人はひとまず触ってみるのがおすすめです。

 独特な世界ですが楽しめています。Emacs の中だけでプログラミングをしたりすることが気持ち良く感じる人は多分同じく楽しめるかと思います。ただ、グラフィカルなものを扱う経験があまりなかったので、単にそういった映像的なフィードバックを受けることの楽しさと混ざっている可能性もあります。

困っている点

 日本語の入力時に変換候補が Pharo 上で表示されず、なぜかデスクトップの左下の方に表示されます。AquaSKK では画面外に表示されてしまう (変換候補の辞書情報だけ見える) ので、通常のように変換が行えません。ことえりや GoogleIME では場所がおかしいだけで表示はされるのでこちらを使っています。

GoogleIME

f:id:lhcpr:20170627123525p:plain

AquaSKK

f:id:lhcpr:20170627123532p:plain