ugo's 読書感想文

読んだ本のまとめや学んだことを書いていきます。

プログラマー脳

概要

プログラマーとしての考え方を学ぶ上で、どういう思考をすればいいのかなどはプログラマー歴が浅い人にとっては気になるところ。

僕自身もそれは気になっていて、プログラムを書くのは好きだけど、無闇に書いていてもよくなく

しっかりとしたプログラムを書くために身につけないといけないスキルというのを考える必要があった。

そこで、評判が非常に高かった本書を手に取る事にした。

コードをよりよく読むために

混乱タイプ

新しいコードを読んでいて混乱を招く3つの要因がある。

  1. 知識不足
  2. 情報不足
  3. 処理能力の不足

知識不足は長期記憶に関わる。

情報不足は短期記憶に関わる。

そして、ワーキングメモリに負荷がかかる。

長期記憶は、我々の行動全てに関係する。

Javaのコードを読む時は、短期記憶を使用し、変数や関数を覚えておくことができるが、

それも長期記憶からも読み解いて、短期記憶に保存しておくといったプロセスを踏んでいる。

認知プロセスの相互作用を利用し、プログラムを読み解くことを行っている。

プロセスは以下

  1. 長期記憶から情報を取得する
  2. 今向き合ってるプログラムに関する情報を保持する短期記憶
  3. コードが何をやっているかなどのワーキングメモリで、コードを判断する

コードの速読

プログラマーの仕事の60%が、読む事に時間を使っている。

人はプログラムを学ぶ時、書く事にばかり集中する。

しかし実際の仕事では、読むことの方が多い。

例えば、新機能の開発で関数の適切な配置場所はどこかなどは、読むところから始まる。

そのため、読む速度を上げることがプログラムを書くことの向上にもつながり、生産性の向上につながる。

人間の短期記憶の容量

人間の短期記憶の容量は、保持するのに30秒が限界な上に、入る容量も少ない。

そのため、変数や長期記憶にない処理が入っていると、その分短期記憶の容量を圧迫し、読むことも理解することも大変になる。

そのため、無意味な変数名や説明のない関数を作るのは避けましょうという話。

記憶のサイズ制限を克服する

チェスの例

チェスの上級者と初級者を集めて実験があった。

ある時の試合の途中の局面を両チームに短時間見せて、再現してもらうというもの。

上級者の方が多くのコマを再現することができた。

これは、上級者の中に定石などがあり、コマの意味を把握しながら覚えることができたということと、覚えなくても決まりごととして理解していたことで再現できたということ。

今度はランダムにコマを配置した局面を見せて、同じように再現してもらった結果

上級者でも初級者でも結果はほぼ変わらなかったそう。

これは、コードにも同じことが言える。

上級プログラマーが同じコードを見たときと、初級者が見た時とでは頭で処理する量が異なるということになる。

その結果、上級プログラマーには例えばデザインパターンなどが頭に入っていることによって、短期記憶の容量を超えず、無理なく短いスパンで再現することができるということになる。

このように長期記憶の量を増やすことで短期記憶しておく容量をセーブし、より少ないワーキングメモリーでコードを読むことが可能になる。

コメント

また、人はコードを読む時、コードがある時とない時とで集中力やコードの読み方に変化がある。

もちろん、コメントに関数の概要が書いてあればそれだけで理解できるだろうし、コードがあると、意外と人はコメントを読むということをしているそう。

そして、ビーコンを立てることが重要。

上記の再現実験を自分で行う時、自分が詰まったところにビーコンを立てて行うことで、自分に必要なもの、自分が短期記憶しなくてはいけないところが明確になり、より対処をしやすくなるというもの。

言語の関数を覚えるのが難しい理由と覚える必要がある理由

割り込み

プログラマーがプログラムを書いてる途中に、メールや別の調べ物で気が取られることがある。

調査によるとその作業からプログラムを書く作業に戻るのにほとんどの場合、15分以上が経っていて、再開した時には自分が何をしていたかを思い出す作業から始まる。

その時、文法を覚えているかどうかだけで思い出す時の労力が著しく異なる。

文法の覚え方

フラッシュカードを複数人で作る。(自分だけがこの問題に直面しているわけではないというのを知るため)

フラッシュカードを最短時間を何日もかけて覚える。

期間を空けて復習することが有効。

期間が短いグループと長い期間を空けたチームとでは、長く空けたチームの方が定着率は高かったそう。

記憶定着させるテクニック

  • 想起練習
  • 推敲

貯蔵強度と検索強度という二つのメカニズムがある。

貯蔵強度とは

特定の何かが長期記憶にどれだけきちんと保持されているかを示している。

九九の掛け算なんかは忘れることができないレベルまで定着しているもの。

検索強度とは

特定の何かを思い出すのがいかに簡単かを示している。

知っているはずなのに、なかなか思い出せないもの。

電話番号や、名前など。

つまり、

長期記憶に保持するだけではなく、簡単に取り出せるようにする必要がある。

そのためには、毎回関数を調べていたのでは、脳はその関数を覚える必要がないと認識する。

そうすると悪循環がうまれ、毎回調べることで向上することがない。

意識的に覚えることが大事になる。

精緻化

学んだばかりのことについて考えるプロセスのこと

スキーマ

脳の記憶は、他の記憶や事実との関係性を持ったネットワークを形成した形で保存される。

この関係を頭の中で整理したものがスキーマと呼ばれる。

既存スキーマを新しい記憶と結びつけることで長期記憶に適合させる方法。

記憶を長く保つためのコツのまとめ

  1. プログラミングの文法を覚える
  2. フラッシュカードを使う
  3. 定期的に練習する
  4. 調べる前に頭の中の記憶検索を行う練習をする
  5. 時間(期間)をかけて練習する
  6. 精緻化を行う

複雑なコードの読み方

認知負荷

・課題内在性負荷 ・課題外在性負荷 ・学習関連負荷

課題内在性負荷

問題そのものに難しくさせる要因が入っている状態

三角形の2辺の長さが分かってて、残り1辺を求めるような問題は

どのようにしてもそれ以上簡略化することができない。

このような問題のことを、「固有複雑性」ともいう。

課題外在性負荷

先ほどの三角形について

2辺の長さが a=8, b=6 のように別に定義されている場合など

その数値を辺の長さと繋ぎ合わせる必要がある。

そのような問題には、短期記憶を使う必要があり、問題が必要以上に難しくなる。

このようなことを課題外在性負荷という。

リファクタリング

リファクタリングをするのは、コードを共通化したりする時に行うが

通化することで参照先が増え、読む時により負荷の高いものになる、逆リファクタになる可能性がある。

しかし、それでもその時の読み手のためにリファクタをして読みやすくするということをすることをしてもよい。

例えばバージョン管理で、ブランチを切って、自分が理解しやすいようにリファクタをすることはコードを理解することの助けになる。

認知度を下げる

コードの中のつながりを可視化することで頭に留めておく時間や労力を下げるということを行う。

具体的には、同じ変数同士や、使っている関数を線で繋ぐなどをすることで、俯瞰してコードを見ることで一つ一つの認知度を下げるというもの。

そして、その変数がどのように変化するかなどの、状態遷移表を書くとよりコードの理解が深まるものだ。

ワーキングメモリーの負荷をいかに減らすかというところで、サポートしてくれるアプリがある。

PythonTutor は上記のような変数を丸で囲ったり、共通部分を線で繋ぐなどを視覚的に行ってくれ、訓練を行うことができるアプリである。

変数が持つ役割

変数をカバーできる11の役割がある。

  • 固定値 その名の通り、変化しない変数のこと。 定数として扱うことができる。

  • ステッパー ループ処理を行う時に、ループのたびに値が変更される変数

for文などで使われる i のような変数のこと

  • フラグ 何かが発生したことを示したり、何かの情報が含まれていることを表す変数。

is_setやis_available、is_errorなど。

  • ウォーカー ループ処理を開始する前には、どのように走査を行うのかが未知のケースで利用されます。

プログラミング言語の仕様によって、ウォーカーは、ポインタ変数であったり、整数のインデックスであったりします。

  • 直近の値の保持者 一連の値を純に処理していく際に、もっとも最新の値を保持する変数のこと。

配列要素のコピーなど(element = list[i] のような)

  • 最も重要な値の保持者 ある特定の値を探すために、値の一覧に対して反復処理を行うが、その過程で重要な値を保持するための変数のこと。

最小値や最大値、特定の条件を満たす最初の変数など。

  • 収集者 データを集めて、1つの変数に集約させているときの変数

配列の数値を足していく、最後のsumのような変数のこと。

  • コンテナ 複数の要素を内包し、追加や削除が可能なデータ構造のこと。

リストや、配列、スタック、ツリーなど。

  • フォロワー 前の値や、次の値を保持して参照するための変数。

常に他の変数とセットで使われる。

  • オーガナイザー 処理を進めるために変数を何らかの方法で変換する。

例えば、リストを並べ直して保存したい時など。

その時に形式を揃えるために使われる変数のこと。

  • テンポラリ 短期間だけ使われる変数。 tempやtなどがその代表例。

ハンガリアン記法

型がない場合に、名前自体に型の命名を含める記法。 strNameなど。

モデル

モデルとは、現実の具体を抽象化してコードに落とし込んだもの。

コードのモデルとは、バーで飲んでる際にコースターの裏に書いたような計算式もモデルと呼べる。

しかし、全てのモデルが便利というわけではない。

例えば、言語ごとに得意とする領域が異なる場合がある。

Pythonだと機械学習に関するライブラリが豊富であったりと、物事に対するモデルの最善手というのは異なる。

メンタルモデル

実際に手を動かすわけではなく、頭の中で考え、利用するモデルのこと。

これは、結構日常的に考えてるものである。

例えば、パソコンを開いて、IDEを立ち上げ、コードを開く時、パソコンの中ではバイナリという実行ファイルを開いているだけで、もっと細かく見れば、0と1を実行しているだけにすぎないのだが、人間の頭の中にはどこにどのファイルがあって、どれを開こうというファイル構造が存在している。

このように、自分の頭の中で抽象化されているものをメンタルモデルという。

しかし、これには欠点も多い。

メンタルモデルは、対象のシステムを完全に表現しているわけではないので、かなり不完全なものが混じることになる。

また変化も多く、常に同じということはない。

脳の労力を多く使うため、人は頭脳労働ではなく、デバッグなどもコードを微調整し実行することを繰り返すような筋肉で解決することに走ることもある。

想定マシン

実際のコンピュータがどのように機能するかを抽象化したもの。

プログラミングの概念を説明したり、プログラミングについて理解を深める際に利用される。

スキーマの重要性

変数を「箱」と表現することは多くの人にとって認知負荷は低い。

しかし、変数を「一輪車」と喩えた時、多くの人は一輪車で何ができるかなどを考える必要があり、認知負荷が高くなる。

つまり、人々にとってどれだけ慣れ親しんだものであるかというインターフェースが重要になるのである。

二つ目のプログラミング言語を学ぶことは一つ目より簡単

転移といわれる、すでに知っていることが新しいことを知ることに役立つ作用によるもの。

以下が大きく影響を与えるもの。

  • 習熟度
  • 類似度
  • コンテキスト
    • 二つのタスクの環境がどれだけ似ているか(IDEなどの環境が例)
  • 重要な性質に関する知識
  • 関連性
  • 感情
    • タスクに対してどのような感情を持つか。二分木を使った処理を調べるのが楽しかったと感じるとして、新しいタスクに対しても楽しいという記憶が蘇ることがある。

デメリット

すでにある知識が、新しいものを学ぶ時に弊害になる時がある。

例えば、Javaのチェック例外などは、Java特有の言語機能なので

C#などを学ぶ際には邪魔になってしまう。

概念変化

すでにある知識を新しい知識を学ぶ際にリセットし、概念理解を改変させる必要がある。

これにはただ、説明を受けただけでは書き変わらない。

長期記憶にある、すでにある知識を変化させるのは、一体化したものを分解する必要があるので通常の学習より大変になる。

以下のようなデメリットも存在する。

例えば

雪だるまにセーターを着せるとどうなるかという質問に対し

人々の持つ、「セーターを着ると暖かい」という印象があるということを考えると

雪だるまにセーターを着せると溶けてしまうという発想になるが

セーターの性能は、「その温度を内部で留める」というものであるため

雪だるまにセーターを着せても、溶けにくくなるということになる。

このように概念変化とは、新たな概念に対して、既知の概念が入ることで誤認識を正していく必要がある。

防ぎ方

プログラミングにおける防ぎ方として

自分が正しいと確信していることでも、間違っている可能性があると認識することが大事。

また誤認識に陥らないように常に意識して勉強しておく。

誤認識をまとめたチェックリストを使うのが良い。

そして、同じプログラミング言語を使っている人からアドバイスをもらうことで、誤認識を防ぐことができる。

これには、モブプロなどを行うことで、お互いの認識を合わせることができるのでとても有効。

また、誤認識があった場合は、テストを追加するだけでなく、同じ過ちを繰り返さないようにドキュメントを残しておくのが重要。

ルール

命名のルールを決めることが重要

ルールを決めることで、命名の一貫性を保つことができる。

初期の命名の慣習は永続的な影響を与える。

この場合、Camel Caseでの命名GitHubでも一番多く、そして一番多くの人に受け入れられている。

しかし、プログラマーではない人も含めた結果ではCamel Caseでは精度は高まるが、認識に少し時間がかかることが分かった。

プログラマーのみの場合は、認識する速度はむしろ上がったので、Camel Caseが良いと言える。

三つのステップモデル

  • 名前にどんな概念を使うか
  • その概念にどんな単語を使うか
  • それらをどう組み合わせるか

を適用することでより品質の高い名前をつけることができる

コードの臭い

リファクタが必要そうなコードに対して臭うことがある。

どのような時に感じるかというと

  • 長すぎるメソッド
  • 長すぎるパラメータリスト
  • switch文
  • 巨大なクラス
  • 怠け者クラス
  • 不適切な関係
  • 重複したコード etc...

が挙げられる。

言語的アンチパターン

  • メソッド名に書かれた以上の働きをするメソッド
  • 実際の働き以上のことをするかのごときメソッド名
  • メソッド名に書かれたのと真逆のことをするメソッド
  • 実際に格納されているよりも多くのものが含まれているかのごとき識別子名
  • 実際に格納されているよりも含まれているものが少ないかのごとき識別子名
  • 実際に格納されているものと真逆な識別子名

2種類の記憶

  • 手続き記憶(潜在記憶)

意識をせずに行うことができる記憶。

自転車の乗り方などがあげられる。

記憶したことを近くしている記憶のこと

forループの書き方などがあげられる。

アンラーニング

何かの知識が他のことを学ぶ上で障害になりうるということに繋がる。

顕在記憶を多く持っている場合、柔軟性が損なわれる可能性がある。

キーボードの操作などがそれにあたる。

時間経過と潜在記憶

プログラミングのための潜在記憶が多ければ多いほど、認知的負荷に余裕ができるので、大きな問題を解くことが簡単になる。

そのためには前述したフラッシュカードの手法などを使って、認知的負荷が下がるように覚えることも大事。

  • 認知的段階

新しい情報は、より小さな部分に分割する必要があり、目の前のタスクについて説明的に考えなければならないフェーズ

  • 連合的段階

パターンに反応できるようにまでに、新しいコードを対象として繰り返し作業する必要がある。

インデックスが0から始まることに慣れた人は、次第に欲しい要素の数から-1をすれば良いことに気づくなど

  • 自律的段階

潜在記憶になる。

自動化されたスキルといえる。

この潜在記憶と化したスキルを増やすことで問題解決に繋げることができる。

問題解決

問題解決にはしばしば、考えるという作業が重要と言われる。

それは当然なのだが、何もないところから考えてもうまく進まないのである。

ある研究によると

模範解答を渡されたグループと、渡されないグループで同じ問題を解き

また別の問題をどちらのグループにも模範解答を渡さずにやらせた結果、模範解答を得たグループの方が成績が良かったとある。

つまり、問題を解く際にワーキングメモリの使用率によって、問題の解決力が変わってくるのである。

プログラミングを書くこと

プログラミング中の活動

  • 検索
  • 理解
  • 転写
  • 増強
  • 探索

探索

コード内を調べ、特定の情報を探す作業のこと。

バグの正確な位置、特定のメソッドが呼び出される全ての箇所、変数が初期化される位置

この時、コメントなどを使って、探索の助けになるようにすることが大事。

理解

コードを読むところは検索と一緒だが、コードを実際実行し、リファクタリングも兼ねている。

このリファクタを通してコードの全体を理解したり関係性を知ることができる。

転写

単にコードを書くという活動

増強

検索と理解、転写をすべて組み合わせたもの。

機能の追加などでコードを追加することを指す。

検索

増強の作業と同様に、コードを書き、実行し、正しい方向に進んでいるかどうかを確認するためにテストを実行する。

その場その場で計画や設計を行うため、特にワーキングメモリに負担がかかる。

割り込みについて

プログラムを書いてる途中の割り込みについて

コードを書くのを中断された後に、同じ作業に戻るまで25分かかってしまうことが多い研究がある。

1分以内に作業を再開できたケースは、調査した全体のうちのたった10%だった。

しかし、割り込みが全くない状態でコードを書くのはなかなか実務の中では難しい。

そうした時に取る行動として

  • ロードブロックリマインダ

というものがある。

作業の途中で続きをやるのを忘れてしまわないように

わざとコンパイルエラーが出るようにしておいて、何をやっていたのかを確実に思い出せるようにしておくこと。

割り込みがある場合、都合の良いタイミング、つまりゾーンと呼ばれる時を避けるべきで、その状況を周囲に知らせることが重要になる。

例えばSlackのアイコンに集中のアイコンをつけるや、timesなどに今から1時間集中しますといったことを投げておくと良さそう。

基本的に人はマルチタスクはできないが、先ほどの自動化が多くなされた作業においてはマルチタスクをしても負担は大きくない。

なので、自分が今取り組んでいるタスクがどのようなものなのかを把握しておき、どれほど集中しなければいけないかを知っておく必要がある。

大きなシステムの設計と改善

ライブラリやフレームワーク、モジュールは他のプログラマーが変更はしないものの頻繁に利用するコードでよくあることとして

どのように構築しているのか、プログラムが理解しやすい形で書かれているかに関係している。

どんな言語でライブラリが作られているかなど。

コードを認知的側面から調査する方法をCDNと呼ぶ。

CDN

「このコードは他の人が変更が容易か」や、「このコードは他の人が内部の情報を見つけ易いか」といった質問に答える助けとなるもの。

元々、フローチャートなどの可視化の手法のために作られたもの。

思考やアイディアを表現するための表記法。

大規模なコードベースのユーザビリティを評価するために使用できる。

プログラミング言語が、その言語で書かれたコードが利用者にどのような認知的影響を与えるかをプログラマーが予測できるようにするためのフレームワーク

CDCB

CDNをどのように活用するかについて。CDNの拡張。

プログラマーが、自分のコードベース、ライブラリ、フレームワークが利用者に与える影響を理解するのを助けるフレームワーク

エラーの発生のしやすさ

Pythonは、Cほど強力な型システムがないので、エラーが検出しづらい。

JavaScriptのような動的型付け言語も、変数が生成される時に、特定の方で初期化されることがない。

それによるエラーの原因が生まれる。

一貫性

命名一つとっても、関数を作る際にそれが組み込み関数と同じ名前であれば

組み込まれているものなのか、誰かが作ったものなのかの判断がつかなかったりする。

命名規約が定まっていないことにより、フレームワークなどを使っていてもそれが認知負荷を引き起こす可能性がある。

拡散性

メソッドを必要以上に複雑にしたり、多くの機能を盛り込んでしまうことによって、メソッド名が長くなる。

プログラミングにおける、構成要素が多くなってしまい、どれほどの多くの記述や長さを必要とするかを表すものを拡散性という。

ワンラインで書くと一行で書けるが、とても読みづらいものになってしまうということが顕著。

暫定性

コードを書く前に構想を作っても、実際書き始めるとエラーが出たり、型の不一致などでなかなか進まないなどがあり、部分部分に集中せざるをえないことになる。

型というのはバグのないコードを書く上では便利だが、試行錯誤している最中は邪魔になってしまう。

このような状態を暫定性が低い状態と表現する。

粘性

既存のコードの変更がいかに難しいかを表す。

動的型付け言語で書かれたコードは、静的型付けに比べて変更が容易。

こういう状況は、粘性が低いと言える。

また、テストやコンパイルといった影響で実行に時間がかかったりする状況がある。

こういう状況は、粘性が高いと言える。

段階的評価

暫定制に関連している。

与えられたシステムにおいて、部分的な作業をチェックしたり実行したりすることがどれだけ簡単なのかを意味する。

シニアエンジニアとジュニアエンジニアの違い

シニアエンジニアは、チャンクで把握することができる。

エラーメッセージ、テスト、問題、解決策などのコードに関する知識にも対応できる。

しかし、ジュニアとシニアの間は二段階しかないのか?

実は、四つの段階に分けることができる。

  • 感覚運動期 - 計画や戦略は立てることができず、ただ感覚に基づいて動くことしかできない
  • 前操作期 - 仮説や計画を立て始めるが、それを確実に思考に活かすことはできない
  • 具体的操作期 - 目に見える具体的なものについては仮説を立てられるようになるが、一般的な結論を出すのはまだ難しい
  • 形式的操作期 - 形式的な推論をすることができるようになる

実際のプログラマーの行動

感覚運動期

プログラムの実行について、全く正しくない理解しか持たない。この段階では、プログラマーはプログラムを正しく読み、追いかけることはできない

前操作期

トレース表を作成するなどして、複数行のコードの結果を手作業で確実に予測することができるようになる。

前操作期のプログラマーは、コード全体ではなく、一部のコードについてどんな処理をしているかを推測することが多い

この期間が、一番プログラマーのフラストレーションがたまる段階。

コードの深い意味を理解することが難しく、推測で物事を進めがち。

そのせいで、一貫性がないように見えてしまう。

教える側からすると、「このプログラマーは頭がよくない」とか「やる気がない」と見えてしまい、イライラしてしまう。

具体的操作期

前操作期的な帰納的アプローチではなく、コードそのものを読んで、演繹的にコードについて理由づけを行うことができる

全てのコードをトレースしなくても、コードに対して推論をすることが可能になる。

コメントや識別子の名前を調べ、必要なときだけコードのトレースを行う。

形式的操作期

論理的で一貫性のある、体型的な推論ができる。

形式的操作的な推論には自分の行動を振り返ることも含まれ、これはデバッグには不可欠である。

コードと、自分自身の振る舞いについて、適切な意思決定ができる経験豊富なプログラマーであり、オンボーディングはほとんど必要ない。

新しいことを学ぶと一時的に物事を忘れてしまう

新しいプログラミングの概念を学んだり、初めて扱うコードベースを調べる際、プログラマーは一時的に低い段階に戻ることがある。

Pythonの経験があり、全ての関数を追いかけなくても読み解くことができるプログラマーだとしても、*argsを使った可変長低数の概念になじみがなければ、スムーズに読み解けるようになるまでに、いくつかの関数の呼び出され方を調べ、確認する必要がある。

初心者が学ぶプロセス

意味波に従うと言われている。

一般的な概念をまず理解し、何のために使われるのか、なぜ知る必要があるかを知ることから始まる。

最後に、具体的な内容から離れて抽象的なレベルまで戻り、概念が一般的にどのように機能するかを知り、腹落ちする必要がある。

これをリパッキングという。

アンチパターン

ハイフラットライン

初心者が抽象的な概念しか理解していない状態に起こる。

いかに便利かを知っていても、実際の構文を知らなければまだまだ学ぶことは多い。

ローフラットライン

熟練者が教える際に、なぜ必要なのか、どう便利なのかを説明せずに、具体的な情報を大量に与えてしまうこと。

下降専用エレベーター

熟練者が教える時に、なぜ重要かとどう扱うかを教えたとしても、その後のリパッキング、つまり新しい知識を長期記憶に統合するチャンスを与えなかった時に起こる。

新しい概念と、過去に知っている知識の間の共通点を明確に質問として尋ねることなどで後押しすることができる。