The King's Museum

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

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))))))

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

本には、

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

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

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

道路を舗装する

仕事をしていると、アウトプットがあった日とそうでない日がある。

プログラマのアウトプットはコードだと思っている。 だから、コードが書けない日はアウトプットがなかったと感じる。 もちろん、コードの「量」だけがアウトプットだとは思っていない。 書いたコードの影響力とか、そのコードが与えたインパクトの大きさが大事だ。

ただ、やはりコードがアウトプットであることには変わりはないと思う。

コードを書かない日

コードを書かない日は何をしてるかというと、打ち合わせをしたり、メールをしたり、チケットにコメントを書いたり、開発環境を整えるのに苦労したりしている。 こういう日が増えてくると、目に見えるアウトプットが減っていき、仕事での達成感を失っていく。

ただ、少し見方を変えてみると違う結果が得られる。

事前に打ち合わせやメールで仕様を調整する。 インタフェースについて合意を取るためにチケットにコメントを書く。 開発環境の構築に時間を費やす。

一見、コードを書くことと関係なさそうに見えるこれらの仕事はうまくいっているのならコードを書くための準備運動だとみることもできる (ただ、「うまくいっているのなら」が重要なポイントで、意味のない打ち合わせなどは本当に何も生み出さないので注意)。 そう考えるとこれらの活動は、コード、すなわちアウトプットを直接生み出してないようにみえて、実はそれを生み出すための下地を整えているともいえる。

これを自分は「道路を舗装する」と呼んでいる。

アウトプットのために

快適にそして全速力でコーディングするために道路を舗装してるんだと思うと、打ち合わせやメール、めげそうになる Yak Shaving も少しだけポジティブに取り組むことができる。アウトプットを感じられなかった日も、自分は明日のために道路を舗装してたんだと思えばいくらか気分はましになる。

もちろん、もとから道路が舗装されていればベストだし、他人が舗装してくれることを期待してもよいだろう。 ただ、この職を続けていれば自分が舗装しなければならない場面に必ず遭遇するし、職位が上がると自分だけでなくチームのために道路を舗装しなければならないことも増えてくる。

そういう時に無駄に達成感をなくしてしまうのではなく、解釈を変えることで自分のモチベーションをうまく維持することができる。 そして、こういう自分を扱い方というものは身につけるべき一種のテクニックだなぁ感じた職業プログラマー10年目の春でした。

『イノベーション・オブ・ライフ』を読んで

今年は毎月一冊本を読もうと思い立って三ヶ月。

今月は『イノベーション・オブ・ライフ』を読んだ。

『イノベーションのジレンマ』で有名なクレイトン・クリステンセンが幸せな人生を送るための理論について語った本。

この本では企業経営に用いる様々なマネジメント理論を人生に応用する。 「〜をすれば幸せな人生を送れる」という知識を提供するのではなく、幸せな人生を分析するための理論を提供することを目指している。 言ってみれば「魚を与えるのではなく魚の釣り方を教える」というところだろう。理論によって対象を分析することをクリステンセンは『理論のレンズを通して見る』と表現する。

人生を理論のレンズを通して見てみよう、というわけだ。

キャリアについて

キャリアでありがちなミスとして報酬や地位をゴールにしてしまうことを挙げている。

報酬や地位は<衛生要因>であり、それらを満たすことで不満をなくすことはできるが仕事の満足には繋がらない。 「衛生状態が悪ければ健康を害するが、衛生状態が良くても健康を増進するわけではない」ことから付けられた名前だ。 「仕事に不満がある」の反対は「仕事に満足している」ではなく、単に「仕事に不満がない」という状態である、と述べている。

これは「動機付け理論」という理論のレンズを通して見ることで明らかになる。

また、キャリアについてのすべてをあらかじめ計画しておくことはできないし、そうするべきではないと説く。 事前に計画した戦略を意図的戦略と呼ぶが、企業経営と同様に物事が計画通りいくことはほぼありえないし、予期されないチャンスを逃す可能性が高い。 予期されないチャンスに対して扉をオープンにしておくことを創発的戦略と呼び、企業経営と同じくキャリアについても意図的戦略と創発的戦略のミックスが必要だという。

その他にも様々な理論を用い、そのレンズを通してみることでキャリアについて分析している。

感想

キャリアについては自分にもあてはまるなぁ、と思う部分も多々あり、考え方が整理されてとてもよかった。

一方、本書の残りの半分以上が人生の『プライベート』の部分(家族や地域コミュニティなど)に関して書かれているのだが、正直いまいちピンとこなかった。 どちらかといえばこの『プライベート』の部分が本書の主軸のような気もするけど、悪くいえば少し説教くさかったかし、賛同できない点もあった。

でも、キャリアについてはとても役に立ったのでその部分だけでも読むことをおすすめしたい。 今回、友達がおすすめしてくれたのでこの本を読んだのだけど、紹介してくれた友達には感謝したい。

ちなみに、原題は "How will you measure your life?" なんだけど、邦題に無理矢理「イノベーション」をいれているあたりはお約束といった感じだろうか。 「人生にイノベーションを!」みたいなイケイケな本ではないことは付け加えておきたい。

そういえば『イノベーションのジレンマ』は数年前にブログ記事にしたなと思っていたが、もう5年以上も前だった…。

www.thekingsmuseum.info

(c) The King's Museum