The King's Museum

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

『エンジニアのためのマネジメントキャリアパス』を読んで

『エンジニアのためのマネジメントキャリアパス』を読みました。

「今年は毎月本を読む」と決めていたので五月中に読み終えたかったけど、五月中は忙しくて読み切ったのは六月。 けっこうボリュームがあったっていうのも一因だけど。

インターンのメンターから CTO まで

この本はその名の通りエンジニアのマネジメントキャリアパスについて書かれた本。 インターンの監督(メンター)から始まり、CTO に至るまでのエンジニアのマネジメント職について幅広く書いてある。 エンジニアの管理職のキャリアラダーを登り始めた人にはとても役に立つ本だろう。

印象的だったのは、エンジニア求められるスキルとはまったく違うスキルが必要だという点。 言われてみれば当然なのだが、テックリード、技術部長や CTO に至るまで、エンジニアの延長線上にあるように見えていて実はまったく違った役割が求められる。 その上、エンジニアとしての技術力は必要とされており(コードを直接書くことは求められていないが)、そのことがより一掃これらの職位のハードルを高くする。

それぞれの職位の役割がかなり詳細に書かれているので、その職位に就いたときには参考書としてとても役に立つ本だと思う。 仕事についてかなり細かく具体的に書いてあるし、ありがちな失敗や筆者自身の経験などが書かれていてとても親切だ。 たとえば、技術部長になって複数のチームを管理するようになったら「手一杯で何も成果があげられていないように感じるのが普通」というようなことも書かれている。 きっと新米の技術部長はこれを読んで少し安心するのではないだろうか。(逆に手一杯だと感じない場合は、仕事がちゃんとできてない可能性があるとも書かれているが)

いつか CTO になったら

最初に「読み切ったのは六月」と書いたけど、実はこの本全部読んでない。 後半の四割くらいは斜め読みして飛ばしてしまった。

とても細かく仕事について書いてあるけど、通して読むには記述が細かすぎて読み物として飽きてしまった。 翻訳の問題なのか、一部の文がすごく長ったらしいのもあって少し読みにくかった。 もう少しエッセンスを抜き出して書かれているとうれしかった。

より経営に近い職位については今の自分の立場とはレイヤが違うし、あまり頭に入ってこなかった。 それでも、そのレイヤの職位のあるべき姿みたいなのがぼんやりとは見えて勉強にはなった。 加えて、自分の上司やさらにその上の上司がどのような役割で働いて、どういう部下だったら嬉しいか?みたいなところを考えるきっかけになった。

自分がもし CTO になるようなことがあったらまたこの本に手を伸ばそうと思う。 今はそんな予定ぜんぜんないけど。

Cousera: Architecting with Google Kubernetes Engine Specialization の Course 1 & 2 を修了した

Architecting with Google Kubernetes Engine Specialization の前半を修了した。

f:id:hjm333:20200712134852p:plain

f:id:hjm333:20200712134840p:plain

Course 1 は Kubernetes というよりも GCP の概要を演習つきで学習。 Course 2 は Container と Kubernetes の基礎、というか表面だけさらっと流した感じ。

正直、身になってる感じがあまりしないな。 演習もほとんど自分で考える要素がないし。 やっぱり自分でいろいろ試行錯誤して構築する経験を積まないと身につかない気がするな~。

残りは 2 コース。どうなることやら。

Kubernetes を勉強するよ

今更ながら Kubernetes を勉強しようと思う。

とりあえず公式のチュートリアルをやったけど、まだまだ理解できないことばかり。 理解できなくてもどかしい感じは居心地が悪いけど、何か新しいことを学ぶときには必ず通る道だからと自分に言い聞かせる。

チュートリアル終わって何をやろうかなと思っていたところで、Coursera の Specialization を見つけた。

www.coursera.org

Google 公式のコースだしまぁきっと間違いないでしょう。 他にも似たようなコースがあったけど、今まで AWS しか触ったことなくて GCP も少し触ってみたかったし Coursera の方をやろうかな。 有料だけど多少はお金がかかったほうがいいプレッシャーにもなるし。

Coursera のコースはたくさん受講してきたけど、基本的に外れがないので安心感がある。

継続とは何か(2)

継続について考える第二弾。

前回の記事では、足し算と掛け算という単純な例を用いて継続について考えた。

www.thekingsmuseum.info

今回は再帰での継続渡しスタイルについて考えて、継続についてさらに理解を深めたい。

累乗を計算する

再帰を用いて 1 から n までの累乗を計算する関数は次のようになる。

(define (fact n)
  (cond [(= n 1) 1]
        [else (* n (fact (- n 1)))])) 

n=1 の時は 1 を返し、それ以外の時は、n に n - 1 までの累乗を掛け算する。

実行すると次のとおり。

(fact 6) ; => 720

ここまではいつも通りの計算だ。

継続渡しスタイル

累乗の計算を継続渡しスタイルにするとどうなるだろう。

まずは n=1 の時を考える。 1 の累乗は 1 なので、これを関数 col に与えてやればよい。

(define (fact&co n col)
  (cond [(= n 1) (col 1)]
        [else ...TODO...]))

次は n が 1 より大きい時だ。

簡単なところから一歩ずつ考えていこう。

継続スタイルでは col に計算の結果を与えてやる必要があった。 だから、col に対しては n の累乗の結果を与えてやればよい。 x を n-1 までの累乗とすれば、n の累乗は (* n x) だから、col にはそれを与えることにする。

(col (* n x)) ; => ただし、x は n-1 までの累乗

こうすれば継続スタイルの約束を守れる。 問題は x すなわち n-1 までの累乗はどうやって得られるかという点だ。

n-1 の累乗を再帰的に fact&co を使って定義する。 fact&co はそれ自体が累乗を計算して col に計算結果が渡ってくるのだから、次のように計算すればよい。

(fact&co (- n 1) (lambda (x) x)) ; => x は n-1 までの累乗の値が入る

上の lambda の中の x を使えば n-1 までの累乗の値が取得できる。 これを、さきほどのパーツを合わせれば n が 1 より大きいときの計算が得られる。

(fact&co (- n 1) (lambda (x)
                   (col (* n x)))) ; => n の 累乗を計算して、col に与えている

これで else のケースもそろったので fact&co は次のように定義できる。

(define (fact&co n col)
  (cond [(= n 1) (col 1)]
        [else (fact&co (- n 1) (lambda (x)
                                 (col (* n x)))) ]))

実行は次のとおり。

(fact&co 6 (lambda (x) x)) ; => 720

展開してみる

理解を深めるため n=3 として、fact と fact&co の関数適用を展開してみる。

fact は展開すると次のようになる。

(fact 3)
=>
(* 3 (fact 2))
=>
(* 3 (* 2 (fact 1)))
=>
(* 3 (* 2 1))

これは簡単。 ちなみにどの時点でも評価可能で、どの時点の評価も 6 になる。

fact&co は展開すると次のようになる。

(fact&co 3 (lambda (x) x))
=>
(fact&co 2 (lambda (a)
             ((lambda (x) x)
              (* 3 a))))
=>
(fact&co 1 (lambda (b)
             ((lambda (a)
                ((lambda (x) x)
                 (* 3 a)))
              (* 2 b))))
=>
((lambda (b)
   ((lambda (a)
      ((lambda (x) x)
       (* 3 a)))
    (* 2 b)))
 1)

少し複雑だが、順を追って見てみる。

  • (fact&co 3 ... ) は、(fact&co 2 ... ) の結果の a に対して 3 をかけたものを col に渡す。
  • (fact&co 2 ... ) は、(fact&co 1 ... ) の結果の b に対して 2 をかけたものを col に渡す。
  • (fact&co 1 ... ) は、1 を col に渡す。

末尾再帰

こうやって眺めると、その関数の計算が終わったあとにやるべき計算をクロージャにして col に渡していることが分かる。 例えば (fact&co 2 col) には「計算結果に 3 をかけて (lambda (x) x) に渡す」というクロージャを渡している。 これはやはり「これから行われるであろう計算をパッケージ化したもの」だ。

もう一つ気づいたのは、継続スタイルでは関数が末尾再帰になってる。 本来(?)の末尾再帰だと即値で計算した結果を引数に渡してるイメージだけど、継続スタイルではやるべき計算がクロージャとして col に渡されていく。

通常の再帰では「2 までの累乗の結果に 3 をかける」という計算は、関数呼び出しのスタックにつまれて保存されている。

(* 3 (fact 2)) ; → 3 をかけるという計算 (* 3 []) はスタックに積まれて保存

一方、継続渡しスタイルではスタックには積まれずクロージャとして計算を保存している。

(fact&co 2 (lambda (a)
             ((lambda (x) x)
              (* 3 a))))
; => 3 をかける計算はクロージャに保存されている              

また、通常の呼び出しでは見えづらい「3 をかける計算」が継続渡しスタイルでは明示的な計算((* 3 a))になっている。

少し継続がつかめてきた。 次回はさらに複雑な再帰の継続渡しスタイルについて見ていく予定。

『カードミステリー』を読んで

4月の本は『カードミステリー』。珍しく小説を読んだ。

この本は小学生の時に読んだことがある本。 いつかまた読み直したいと思っていたけどずっと本棚で眠っていた。 まぁ、だいぶ前に自炊して PDF にしたから本棚で眠ってたというのは比喩なのだけど。

パパと息子が二人で旅をしながら、家出したママを探しにギリシャのアテネに向かう。 その途中、小さな村で息子は不思議な小さな本(豆本)を受け取る。 豆本にはトランプのカードが人物化した不思議な島の物語が書かれている。 そして、息子はその物語と自分の不思議な関係性に気づいてく。

著者は『ソフィーの世界』を書いたヨースタイン・ゴルデル。 ヨースタイン・ゴルデルは哲学の教師だったこともあって、この本でもよく哲学的な問いが投げかけられる。 パパと息子が旅をしながら哲学の話をするというのは『禅とオートバイ修理技術』の影響を受けているのかな。

思い出の本を読み直すのはとてもノスタルジーな感覚だった(エモい?)。 パパが話す哲学談義や豆本の中の不思議な島の話、人物化したトランプ達やママとの再会、そしてジョーカー。 読み進めていくと忘れていた記憶がすっと蘇ってきたりしてちょっと感動したり。

ただ、パパの小難しい話は今の自分にはあまり響かなくなってしまっていた。 当時はパパの哲学の話にとても影響を受けたんだけど。

子どものうちは、自分のまわりをゆっくり見る能力があった。 けれど、やがて世の中に慣れてしまう。 成長するということは、感覚の経験に酔っ払ってしまうことなのかもしれない。

まさにこのとおり。 もう成長してしまって感覚の経験に酔っ払ってしまったんだろう。 小学生の頃はずいぶんと読むのに時間がかかった気もしたけど、今回はあっという間に読み終わった。 そういう意味でも大人になったんだなぁと感慨にふけった。

そういえば、この本にはトランプの「ジョーカー」が出てくるが、それに影響されて JOKER というハンドルネームを使っていた黒歴史が…。

継続とは何か(1)

『Scheme 手習い』の第8章に「継続」の概念が出てきた。 ただ、説明が少なくいまいち理解できないので、Web ページを漁ってみる。

Gauche の作者 Shiro さん曰く

文献を紐解くと、 継続とは「これから行われるであろう計算をパッケージ化したもの」とある。

「これから行われるであろう計算をパッケージ化したもの」だという。 まったく分からない…。

紫藤さんのページ を見てみる。

継続とはトップレベルに戻ってくるまでにしなければならない計算です。

うーん、やはり分からない。

ただ、継続に関連して「関数の継続渡しスタイル」というものがあるらしい。

継続渡しスタイルとは、関数が、値を返す代わりに、計算した値をどの関数に渡すのかを明示的に指定する方法です。

少しピンときた気がする。

まずは「継続渡しスタイル」を、単純な例からひもといていくことにする。

継続渡しスタイル

はじめに、二つの数を掛け算する関数 mul と足し算する関数 add を定義してみる。

(define (mul x y)
  (* x y))
  
(define (add x y)
  (+ x y))

これらの実行は簡単。次のようにする。

(mul 3 4) ; => 12
(add 1 2) ; => 3

次に継続渡しスタイル。

継続渡しスタイルは「計算した値をどの関数に渡すのかを明示的に指定する方法」だ。 だから、計算の結果を渡す関数を引数として受け取り、計算結果はその関数に渡すようにする。

(define (mul&co x y col)
  (col (* x y)))
 
(define (add&co x y col)
  (col (+ x y)))

これで mul と add は継続渡しになった。 &co は継続渡し形式を意味し、col は結果を渡す関数である。

なんとなく、コールバック関数を渡すのに近い感覚か。

これらの関数を使って実際に計算するにはどのようにすればよいだろう。 add や mul と違って結果を渡す何らかの関数を渡さなければならない。

もし、単に値を返すだけなら、col に値を返すだけの単純な lambda を渡せばよい。

(mul&co 3 4 (lambda (n) n)) ; => 12
(add&co 1 2 (lambda (n) n)) ; => 3

これで最も単純な継続渡しスタイルが理解できた。

少し複雑な例

足し算と掛け算の両方を使った計算はどのようになるだろう。

(mul 3 (add 1 2)) ; => 9

これを継続渡しスタイルで実行すると次のようになる。

(add&co 1 2
  (lambda (x)
    (mul&co 3 x
      (lambda (n)
        n)))) ; => 9

少し複雑になった。add&co には lambda で包んだ mul&co の計算を渡している。

ステップバイステップで関数の適用をひもといていくと次のようになる。

(add&co 1 2
  (lambda (x)
    (mul&co 3 x
      (lambda (n)
        n))))
=>
((lambda (x)
  (mul&co 3 x
    (lambda (n)
      n))) (+ 1 2))
=>
((lambda (x)
  (mul&co 3 x
    (lambda (n)
      n))) 3)
=>
(mul&co 3 3
  (lambda (n)
      n))
=>
((lambda (n)
      n) (* 3 3))
=>
((lambda (n)
      n) 9)
=>
9

通常の計算スタイル((mul 3 (add 1 2)))は内側から外側にむかって計算したが、継続渡しスタイルでは計算が外側から内側に流れていることが分かる。

さらに複雑な例

もう少し複雑な例をとるとどうなるだろう。

(mul (mul 2 3) (add 1 (add 1 2))) => 24

少し長くなるが、これを継続渡しスタイルにすると次のようになる。

(add&co 1 2
  (lambda (x)
    (add&co 1 x
      (lambda (y)
        (mul&co 2 3
          (lambda (z)
            (mul&co y z
              (lambda (n)
                n)))))))) ; => 24

関数の適用は省略するが、継続渡しスタイルでもちゃんと複雑な計算ができる。

ここで、最初の継続の説明に戻ってみる。

継続とは「これから行われるであろう計算をパッケージ化したもの」

さきほどの継続渡しスタイルをもう一度見てみると、一番外側の add&co には次の関数を渡している。

(lambda (x)
    (add&co 1 x
      (lambda (y)
        (mul&co 2 3
          (lambda (z)
            (mul&co y z
              (lambda (n)
                n)))))))

一番外側の add&co にとって、これがまさに「これから行われるであろう計算をパッケージ化したもの」なのではないだろうか。(これが継続?)

通常スタイルの場合、関数の適用は内側から外側に向かって行われるので、あとに続く計算は外側にたどり着くまで分からない。 一方、継続渡しスタイルでは最初の計算を行う時点で、その後に行われる計算が関数として渡されている。

少しだけ理解ができたかもしれない。

今日はここまで。次は継続渡しスタイルの再帰関数について書く予定。

Scheme 手習い(7)

第8章:究極の lambda

rember-f

(define (rember-f test? a l)
  (cond [(null? l) '()]
        [(test? (car l) a) (cdr l)]
        [else (cons (car l) (rember-f test? a (cdr l)))]))

リストから要素を削除する rember の派生版。 要素の一致を判定する関数を引数で与えることができる。

eq-c?

(define eq-c?
  (lambda (a)
    (lambda (x)
      (eq? a x))))

引数 a を与えると、その引数と eq? をとる関数を返す関数。 これはカリー化と呼ばれている。

rember-f

(define rember-f
  (lambda (test?)
    (lambda (a l)
      (cond [(null? l) '()]
            [(test? (car l) a) (cdr l)]
            [else (cons (car l) ((rember-f test?) a (cdr l)))]))))

さきほどと同じ rember-f だが、「関数を適用すると関数が返ってくる」点が異なっている。 この関数を使うためには次のようにする。

((rember-f eq?) 1 '(1 2 3))

rember-f に eq? を与えると test? を eq? で束縛した a と l をとる関数返ってくる。 それに対し、1 と '(1 2 3) を引数として関数を呼び出す。

insertL-f/insertR-f

(define insertL-f
  (lambda (test?)
    (lambda (new old l)
      (cond [(null? l) '()]
            [(test? (car l) old) (cons new l)]
            [else (cons (car l) ((insertL-f test?) new old (cdr l)))]))))
(define insertR-f
  (lambda (test?)
    (lambda (new old l)
      (cond [(null? l) '()]
            [(test? (car l) old)
             (cons old (cons new (cdr l)))]
            [else (cons (car l) ((insertR-f test?) new old (cdr l)))])))) 

リストの特定の要素の左か右に要素を挿入する関数の派生版。 比較関数を引数として与えられるようにしている。

insert-g

(define (seqL new old l)
  (cons new (cons old l)))
  
(define (seqR new old l)
  (cons old (cons new l)))

(define insert-g
  (lambda (seq)
    (lambda (new old l)
      (cond [(null? l) '()]
            [(eq? (car l) old)
             (seq new old (cdr l))]
            [else (cons (car l) ((insert-g seq) new old (cdr l)))]))))  

insertL と insertR を抽象化して、特定の要素のどちら側に要素を挿入するかを関数で与えられるようにしている。

(define insertL
  (insert-g seqL))

(define insertR
  (insert-g seqR))

insert-g に seqL と seqR を渡すと insertL と insertR を得ることができる。

なお、insertL と insertR は seqL と seqR を用意しなくても次のように定義できる。

(deifne insertL
  (insert-g
    (lambda (new old l)
      (cons new (cons old l)))))

(deifne insertR
  (insert-g
    (lambda (new old l)
      (cons old (cons new l)))))

また、要素を違う要素に置き換える subst も次のように定義できる。

(define subst
  (insert-g
    (lambda (new old l)
      (cons new l))))

さらに、なじみの深い rember も次のように定義できる。

(define rember
  (insert-g
    (lambda (new old l) l)))

第9の戒律

【第9の戒律】

新しき関数においては共通のパターンを抽象化すべし。

multirember-f

(define multirember-f
  (lambda (test?)
    (lambda (a lat)
      (cond [(null? lat) '()]
            [(test? (car lat) a)
             ((multirember-f test?) a (cdr lat))]
            [else (cons (car lat)
                        ((multirember-f test?) a (cdr lat)))]))))

multirember の比較関数を引数で与えられるようにしたバージョン。

multirember-T

(define (multirember-T test? lat)
  (cond [(null? lat) '()]
        [(test? (car lat))
         (multirember-T test? (cdr lat))]
        [else (cons (car lat)
                    (multirember-T test? (cdr lat)))]))

multirember に特定の値と比較する比較関数を与えられるようにしたバージョン。 次のような関数を定義し、multirember-T に与える。

(define (eq?-tuna k)
  (eq? 'tuna k))

(multirember-T eq?-tuna '(tuna shrimp salad))

multirember&co

(define (multirember&co a lat col)
  (cond [(null? lat) (col '() '())]
        [(eq? (car lat) a)
         (multirember&co a (cdr lat)
           (lambda (newlat seen)
             (col newlat (cons a seen))))]
        [else
         (mutltirember&co a (cdr lat)
           (lambda (newlat seen)
             (col (cons (car lat) newlat) seen)))]))          

multirember を継続渡しスタイル (continuation passing style) と呼ばれるやり方に変更したバージョン。

ちなみにこの例では継続を使って、さらに再帰しているので非常に分かりづらい。 継続を説明せずにいきなりこの例を出されるのはけっこう辛いものがある。

multiinsertLR

(define (multiinsertLR oldL oldR new lat)
  (cond [(null? lat) '()]
        [(eq? oldL (car lat))
         (cons new (cons oldL
                    (multiinsertLR oldL oldR new (cdr lat))))]
        [(eq? oldR (car lat))
         (cons oldR (cons new
                    (multiinsertLR oldL oldR new (cdr lat))))]
        [else (cons (car lat)
                    (multiinsertLR oldL oldR new (cdr lat)))]))

oldL の要素の左に new を、oldR の要素の右に new を挿入する関数。 これ自体はそれほど難しくない。

multiinsertLR&co

(define (multiinsertLR&co oldL oldR new lat col)
  (cond [(null? lat) (col 0 0 '())]
        [(eq? oldL (car lat))
         (multiinsertLR&co oldL oldR new (cdr lat)
           (lambda (l r newlat)
             (col (+ 1 l) r (cons new (cons oldL newlat)))))]
        [(eq? oldR (car lat))
         (multiinsertLR&co oldL oldR new (cdr lat)
           (lambda (l r newlat)
             (col l (+ r 1) (cons oldR (cons new newlat)))))]
        [else
         (multiinsertLR&co oldL oldR new (cdr lat)
           (lambda (l r newlat)
             (col l r (cons (car l) newlat))))]))             

multiinsertLR を継続渡しスタイルにしたバージョン。 この例はぎりぎり理解できている(気がする)。

外側からどんどん関数を適用(ひも解いていく)するイメージかな…。

evens-only*

(define (evens-only* lat)
  (cond [(null? lat) '()]
        [(atom? (car lat))
         (cond [(even? (car lat)) (cons (car lat) (evens-only (cdr lat)))]
               [else (evens-only (cdr lat))])]
        [else (cons (evens-only (car lat)) (evens-only (cdr lat)))]))                

入れ子になったリストから奇数を除去する関数。 入れ子を走査するために、(car lat) が atom? かどうかで分岐しているのがポイント。

evens-only*&co

(define (evens-only*&co lat col)
  (cond [(null? lat) (col '() 1 0)]
        [(atom? (car lat))
         (cond [(even? (car lat))
                (evens-only*&co (cdr lat)
                  (lambda (newlat p s)
                    (col (cons (car lat) newlat) (* p (car lat)) s)))]                    
               [else
                (evens-only*&co (cdr lat)
                  (lambda (newlat p s)
                    (col newlat p (+ (car lat) s))))])]
        [else
         (evens-only*&co (car lat)
           (lambda (newlat p s)
             (evens-only*&co (cdr lat)
               (lambda (dnewlat dp ds)
                 (col (cons newlat dnewlat) (* dp p) (+ ds s))))))]))

evens-only* を継続渡しスタイルにしたバージョン。 ここで自分の理解能力の限界を超えた。

(evens-only*&co (car lat)
  (lambda (newlat p s)
    (evens-only*&co (cdr lat)
      (lambda (dnewlat dp ds)
        (col (cons newlat dnewlat) (* dp p) (+ ds s))))))

この部分がなぜ、こうなるのかまだきちんと理解できていない。

本には、

ひえー。頭がこんがらがりそうですね。

と書かれていたが、ほんとにその通り。

自分の理解のために次の章に進む前に継続について一つの記事を書こうかな。

(c) The King's Museum