色んな事を書く

シンプルさを極めたエンジニアになりたい

【読書メモ】プログラマー脳

動機

  • 「良いコードとは認知不可の低いコードである」という漠然とした持論は持っていたが、「プログラミング中に発生する認知不可とは」が曖昧なので突き詰めたい
  • 理解し難い物事に対するストレスが半端ないから、極力そんなコードを後世に残したくない

プログラマー脳 ~優れたプログラマーになるための認知科学に基づくアプローチ | Felienne Hermans, 水野貴明, 水野いずみ |本 | 通販 | Amazon

1 章 コーディング中の混乱を紐解く

  1. 知識の不足 → 長期記憶
  2. 情報の不足 → 短期記憶
  3. 処理能力の不足 → ワーキングメモリ

コーディング時における混乱の原因と、それらがどの認知プロセスと関係があるかという話。

長期記憶はプログラミングの文法とか背景のソフトウェア工学の知識に関するものがあるだろう。C# だとこういう書き方をするとか、スタックってこういうものだよね~、みたいな話。

短期記憶はあるプログラムにおける変数の中身とかクラスの責務とかそういうのかな。やっぱこういう時に理解のしやすさ、ってのは記憶の残りやすさに一定関わってくると思っていて。変数名がそのコンテキストにおける役割を正しく表現できているかとか、責務を正しく表現されたクラス名とかだと覚えやすさは全然違うと思う。例えばだけど Service って Suffix がついたクラス名なのに状態を持っている、みたいなクラスがあるとそれだけで「なんで?」となってしまう。直感に反したことを行って、変な疑問が生まれるとそれだけで思考体力を使ってしまう。嫌だ。やっぱ DDD みたいに解決対象の問題から抽象度を下げてコードに落としていくアプローチは理解しやすさ高いと思うんだ。

もちろん、何が長期記憶にあって何が短期記憶にあるかは人によって違う。C# 歴が 5 年の人の長期記憶にあるものが 1 年の人は短期記憶にある、なんてことは珍しくないと思う。相手の脳内記憶装置の中身を完全に知る術はないんだけど、これまでにこなしたタスクとかどういう知識をインプットしているのかとか、その辺りから推測するしかない。1 on 1 とかセットして。でないと退屈なタスクを与えてしまったり、負荷の高すぎるタスクを与えてしまったりと不幸な感情になってしまうと思う。その人の成長に寄与しない。

ワーキングメモリってのは目の前の問題を解決するために何をするのか具体的に考えるために必要なスペースって感じかな。bit 演算とかリフレクションを使ってるコードを読むと特に負荷がかかってるなぁと思う。

って考えると処理能力の不足は短期記憶・長期記憶で補完できると思う。初めて読むプログラムなんて文法から調べていかなきゃいけないし (それってつまり短期記憶・長期記憶にもその情報がない)、全てをワーキングメモリで処理しなきゃいけない。だから意図が明確なインターフェースにするとかの工夫は読み手に取ってハッピーなものだと思う。何をしているのかわからないってなると実装を見に行かないといけないし、そうなってしまうと呼び出し元の情報はいったんどこかに退避させないといけない。実装を理解して戻ってきたときには一度退避させた情報を再度取り出さないといけない。きつい。ワーキングメモリは節約させてあげたい。

ある人にとっては意図が明確でも、そのほかの人にってはそうじゃないってのはどういう時だろう。ドメイン知識、みたいな前提知識が抜けている時はそういう状況になりうるよねと思う。逆に言うと意図が明確なコードはドメイン知識を学ぶ道具になるんだよなぁ。「読み手の短期記憶・長期記憶に意味のある情報を理解しやすい状態で表しつつ、ワーキングメモリの負荷を減らすコード」を書いていけると良さそう?なんでこのコードはこうなっているんだっけ?ってなってしまうと、その後改修を行うたびに余計なメモリを使うよね。そんなことなくスムーズに手を動かせる状態にしたい。

2 章 コードを速読する

なぜコードを記憶するのは難しいのでしょうか。その最大の理由は、短期記憶の容量が限られていることです。

馴染みのないコードを読むのが辛いのは短期記憶を使うしかないから。一つ一つ何をしているのか理解して脳内メモリに保存して読み進めていかなきゃいけない。短期記憶は容量も限られており、かつその容量は想像以上に小さいことも要因の一つ。

上手くやっていくにはインプットした内容は脳以外のどこかに出力するなりするしかないんだろう。一行一行処理の流れを理解しても仕方ないから、大枠を捉えて読んでいかないといけない。

コード読むのが早い人は長期記憶を使っている。ある程度読んだだけで何をしているのか把握でき、記憶容量も節約できるため速い。

これまで本章では、特定のトピックについて記憶している情報が多いほど、情報を効果的に分割できるということを見てきました。

長期記憶の中に過去に見聞きしたものやある種のパターンを置いていて、目の前のこととそれらを当てはめながら理解している。このまとまった単位をチャンクっていう。コードを書くときは処理を適切な単位で区切っていこう。それがメソッドだったりクラスだったりするんだろう。

ただ区切れば良いんじゃなくて、特定のトピックってのがポイントだろうなぁ。別々のトピックの事柄を同じメソッドにまとめてしまっては逆に読みづらい。Get の接頭辞が付いたメソッドの中で副作用が起きてたら「なんで?」となってしまうように。

デザインパターンとか特定のドメイン知識に左右されない良いチャンクだと思う。ただその知識そのものを得るのにコストがかかったりするし、そもそも気付かない人もいるから難しい。パターン名をクラス名に含めるとかはドメインが汚れる可能性もあるしあまりやりたくない。

上手くチャンク化された部分を振り返ってみる。自分のプログラムとか短時間で記憶して、何もない状態から書き直してみる。それで一致している部分は適切なチャンクがなされている可能性が高い。なぜなら短期記憶に残っているから。

ざっと読んだけど、実験結果を使って説明してくれるので、自分の価値観に根拠を持たせることができる。変数名の大切さとか。

3 章 プログラミング言語の文法を素早く習得する

何を記憶しているかということは、コードをどれだけ効果的に処理できるかということに影響を与えます。

ググれば文法なんてすぐにわかるけど、それを長期記憶に入れておくだけでコードを読む時間は大幅に変わる。またググると広告とかどのページを見るとか実際のコーディングやコードリーディングから意識が削がれる要因は多々あり、そこからやりたかった作業に戻る為には時間を要してしまう。自分の長期記憶だけで目の前のことを解決する方が生産性は高い。

IDE 使ってると呼び出そうとしているメソッドの目的やパラメータの説明を表示してくれるからググる手間は省ける。ってのがあるからやっぱコメントは大事だね。

記憶を強化するための 2 つのテクニック、「想起練習(積極的に何かを思い出そうとする)」と「推敲(新しい知識を既存の記憶と積極的に結びつける)」を取り上げます。

長期記憶にとどめておく為には想起と推敲が必要。ある瞬間に思い起こすよう脳を叩き上げたり、既存の記憶と絡めて新しい事柄をインプットしていく事で知識の価値が高まっていく。既存知識との紐づけのために自分なりの言語化をしていくのも重要とも言えるよね。

4 章 複雑なコードの読み方

脳内デバックをしていて一番しんどいなって思うのが戻り値の実際の値がが想像つきにくい時。HttpResponse みたいな複雑なオブジェクトになると、実行時にデバックしてみないとどんな値になりうるのか想像つきづらい時がある。

そういうフレームワーク固有の複雑なオブジェクトを緩和するために独自モデルとか作るのはありだと思ってる。ドメイン固有の知識を表現して、課題外在性負荷を減らしていく作戦。ただ、何を抽象度高く捉えたものなのか曖昧だと把握するのが困難な場合もある。例えば色んな外部 API へのリクエストとプロダクトのやり取りを共通するために ApiClient みたいなのを作っても、それが API ごとに別々の型を持ってたりすると途端につらい。複数 Repository でコード管理してるとこういうの起きやすい。

認知負荷とかワーキングメモリにかかる負荷のこと。コードの工夫だけでどうにかするのは無理だね。ドメイン知識に関してはある程度どこかにまとめたものが欲しいけど、実装とか言語機能に関しては各自で身につけて欲しいってのはある。

負荷のタイプ 概要
課題内在性負荷 その問題自体がどのくらい複雑化
課題外在性負荷 その問題の妨げとなる外的要因
学習関連負荷 考えたことを長期記憶に保持する際に引き起こされる認知的負荷

ソフトウェアで言うと、課題内在性負荷ってのは解決しようとしている問題そのものの複雑さのこと。課題外在性ってのは問題を解決するために書いたプログラムの複雑性の事かな。解こうとしている対象の問題の知識や制約をいかに表現出来るかが大事だと思う。「課題内在性負荷≒課題外在性負荷」みたいなイメージになるコードを書くようにしてる。

処理が分散しているコードは、読み進めるためにはあちこちに定義された関数をスクロールや検索で見つけ出す必要があるため、ワーキングメモリには負担がかかる可能性があります。

スクロールとかするだけで脳内の負荷高まるしね。ショートカット機能があるといえど、べた書きほど読みやすさに勝るものはないのかな。でも複雑性の高いものに名前をつけるってのはメリットだし、それこそチャンキングだしね。過度な構造化程認知負荷を高めるものはないとは思っている。

テストコードでも、あるケースで使うテストデータはケースごとに準備しておくことが望ましいのだと思う。テストコードを読んで仕様把握したい時にあっちこっちに飛んでしまうと辛いからね。とはいえ何でもかんでもケースごとに定義しているとコード数が爆増してそれはそれでつらいので、あるテストケースで検証したい処理に必要なデータだけケースで定義して、それ以外のセットアップに必要なデータは共通化させたい。Builder Pattern を使ってテストデータの生成の表現力を高めてもよい。

5 章 コードの深い理解に到達する

コード内でどんな処理が行われているのかがよくわかったら、次はそのコードについてより深く考えてみましょう。そのコードは、どのようにして書かれたコードなのでしょうか。

なぜそのコードになったのかっていう意図を汲み取っていくって話だと思った。例えばだけどアプリケーション側で結果整合とか冪等性とか意識しないといけないと、コードが複雑になりやすい。なぜその処理順序なのかとかいったコードの意図は考えてもわかんない部分もある。その順序を間違えてしまうと事故になる可能性があるとか。こういうのはコメントで残していくしかないよねぇ。こういうミドルウェアなどの要因で書かれたコードの理解をするのって大変。

 

コードの背景を推論するにあたり、変数が中心的な役割を果たすことは間違いないでしょう。

変数名も大事だけど型も大事だと思う。immutable なものってその時点で変数の振る舞い方の可能性を減らせるのがいいよね。可変ではないってのは読みやすさの一つだと思う。ReadOnlyList とか使っておくと、どんな状況でも List の要素に変化がないって事がわかるし。

コードの意図を汲み取る為には変数の役割を知り、コードの中でどんな役割を担っているのかいるのか見定めることが大切。でもそれだけで意図まで汲み取れるかというと難しそうではある。

 

ほとんどすべての変数は、たった11個の役割に分類できると主張しています。

あるプログラムの変数の役割をどう決めたのか、それを決めるのに参考にした情報は何か、これらがあればコードの読み方は変わって来るのか?どの変数に注力してコードを読めば良いのかは明確になるはあるかもしれない。ただそれをするくらいならコード読んだ方が早いんじゃないかという気もする。初めて読む部分だけ意識するとかでも良さそう。

 

ソースコードを理解するための 2 つの異なるレベル、すなわちテキスト構造の理解と計画の理解というモデルを作成しました。

 

テキスト構造の理解は、キーワードが何をすることを意味するのか、変数の役割は何かと言ったプログラムの一部分に関数r表面的な理解に関連するものです。一方、計画の理解は、プログラマーがそのプログラムを作る時に何を計画していたのか、何を目指していたのかといったことを理解することを表しています。

確かにな~。ライブラリのコードを読んでいて、それ単体だったら何をしているのかは理解できるんだけど、それが全体のうちのどこに位置するもので、どうやって使われるのかってのはコードを読んだだけじゃ理解できない。Web フレームワークの認証機能とかそんな感じがする。フレームワーク系の計画はドキュメント読めばわかりそうだけど。

 

コード内の注目すべきポイントが、すなわちフォーカルポイントと呼ばれる場所を探すことから始めることに気付きました。

ソースコードリーディングのスタートはフォーカルポイントを見つけること。例外が発生したポイント、あるクラスの public method、Azure Function のエントリポイント、などなど。そこから周辺の知識を集めて読んでいくよね。これはすごい分かる。目的もなく漠然とコードを読んでも頭に入ってこない。

DI とかテキスト的な読みやすさは向上できても計画的な読みやすさの向上には繋がりにくいものもある。どの段階で初期化されているのかわかんないしね。

6 章 プログラミングに関する問題をよりうまく扱えるようにする

問題を考えるときに頭の中で利用し、実際に手を動かして何らかの作業をすることを必要としないモデルもあります。これをメンタルモデルと呼びます。

メンタルモデルを使ってコードを読んだりプログラミングを理解すると効率が良いよって話。メモリ上でのデータの持ち方を木に例えて理解しやすくするやつ。

考えてみたら木構造かどうかなんてコンピュータには関係ないしね。コンピュータではあくまで 0 と 1 でしか表現されていないわけで、それを最も効率的に保持しておける構造を考えただけ。それを人間に説明する為に図に起こして、それが木のような形をしていたから木構造と呼ばれているだけだし。解決したいのは高速な検索を行えるデータ構造だし。

そもそもよく開発とかで使われているモデルって単語は何だろう。問題領域を抽象化して注目すべきことのみを抽出したもの、と理解してる。図とかモデルの一種。自分の理解を他人に伝えたりする場合に使うのもモデルだよね。そういった意味では HttpClient とかもモデルではあるんだよね。

問題解決における問題の表現方法を変えることで問題の解法がシンプルになるって事だよな?整数を 2 でわるみたいな話があった時に、整数を 2 進数で表現しておくと 1 bit 右シフトすれば良いだけになる。ドメイン駆動における問題領域と解決領域の考え方に近いな。いかに複雑な物事をシンプルに捉えて解決していけるのかを極めたいエンジニアになりたいわけで。

良くコードを書くのが速いって言われているけど、多分そんなことない。ある問題があったときに、それを解くコードを書き始めるんじゃなくて最もシンプルな解法を見つける事から始める。シンプルであればあるほどコードも少なくて済むしね。解決したい問題への理解とその問題を解く方法を考えるのが速いのだと思う。

 

より詳しいメンタルモデルが作れるようになればなるほど、システムを正確に理解できるようになり、それに関する質問に正しく答えられるようになるわけです。

メンタルモデルが具体的であるほどそれで説明できる事象への正答率が上がる。つまりメンタルモデルを深く理解した上でコードを読んだ方が誤った理解になりにくい。バイナリサーチとかエンコーディングとか読むの苦手だけど、それはメンタルモデルへの理解が足りていないからかな?

7 章 思考に潜むバグ

特に引用して残しておきたいものはなかった。長期記憶の中の理解が、新しいことへの理解を妨げてしまうことがある、って話。そういうのを誤認識と言ったりする。例えばあるプログラミング言語ではファイルなどのリソースの管理をプログラマが行う必要がないが、別の言語では必要がある、みたいな。必要ないと思い込んでいたためにファイルを確保しっぱなしにしてエラーになってしまったり、とか。

ドメインに対する理解も同じことが言えそう。ヒューリスティックな判断と化してしまっていると、いつかそれが暗黙知になって予期せぬバグを生むはず。そもそもこういうのは起こさないように本質捉えて設計しないといけない。

教えるとか理解をするのは別問題。既存で別のメンタルモデルを持っている人間にそれを置き換える新しいものを教えたとしてすぐに置き換わるわけでないない。きちんとモデルを理解してもらわないと、正しく使いこなせない。

人間が何かを完全に忘れる (長期記憶から消し去る) ことはほとんどなくて、大半の場合は思い出すのが難しくなっているだけ。だからあるきっかけで古い概念を思い出したりそれが目の前の問題を解くための障壁となったりもする。

ある概念に対して複数の理解をしている場合もある。例えばセーターは「体を温めるもの」「体の熱を逃さないもの」みたいな理解をしていたとする。こういった理解が競合してしまい複雑な問題を解くときの足枷になる事もある。例えば「雪だるまにセーターを巻くとはやく溶けるのか」みたいな問題に。普通に考えたら温めるものではないので溶けはしない。むしろ熱を逃さないから溶けにくいだろう。そこに「体を温めるもの」という誤った認識があると誤った解を導いてしまう。間違った認識を押さえ込んで正しい理解を採用することを抑制とかいったりする。メタ認知をしたりアンラーニングすると良さそう

こういう負の転移を防ぐにはペアプロとかで誤認識をしていないのか拾っていくといいと思う。あとはテストを書いて動かしてみるとか。結合試験もそうだね。実際に何がどう動いてその結果どうなるのかは自分で試してみるのが一番早い。まずは自分の認識が間違っていることに気付くのが一歩目だと思う。