プログラミングを学ぶにあたって詰まったことと、そこから学んだこと

toyokeizai.net

satoru-takeuchi.hatenablog.com

全然レイヤーが違うが、自分が何に悩んで、どういう風に理解したか、思い出しながら書き出してみる。

プログラミング歴

20歳からなので、現時点で10年ぐらいだが、中学生の時ちょっと触ったことがあった。

学習過程で詰まったこと

VBA: 最初の体験

  • 変数の概念。同じシンボルの中身が、代入が実行されたときの時系列で変化する、という概念が掴めなかった。後に再代入禁止の関数型言語を知って、やっぱそっちのが自然に感じる人もいるよね、と腑に落ちた。
  • 関数と引数の概念。プログラムは上から下に流れるものと理解していたので、その上下の外に処理を追い出す必要性がわからなかった。関数呼び出し側と関数定義側の引数の名前が違っていいことに混乱した。一定以上の規模のコードを書くことで理解できたと思う。

ループや if はすんなり理解できたが関数に手こずった。

Java: 大学の授業

  • ジェネリクスの概念: そもそも <T> の構文が難しく、構文上どこで何のスコープが何の型に有効になってるのか、まるでわからなかった。最初はたぶんデータ構造とアルゴリズムの教本の、ソートアルゴリズムジェネリクス対応しましょう、というやつだったと思うが、自分で書いたコードでジェネリクスが必要になるまで、ジェネリクスの必要性を理解できなかったように思う。
  • 標準ライブラリのデザインパターン: 特に Java の標準ライブラリで頻出するファクトリーパターンで、なぜこんなややこしい手続きが必要なのか理解できないものが多かった。
  • リスナー: Java6 の時代なので、Lambda や高階関数がなく、関数参照の扱いが難しかった

動くがAPIデザインが理解できない。こんな複雑な必要はないだろ、と思っていた気がする。

Python (2 系) 半年〜

  • 文字列のエンコーディングの概念: 主に python 2 系が悪いのだが、 ascii, sjis, unicode, utf8 の変換が、かなりややこしかった。今でも python の文字列の扱いは苦手意識がある。
  • パッケージマネージャの概念: 標準ライブラリ以外にライブラリがある、という発想が最初なかった。当時使われていた easy_install の出来が悪いのもあり、ポータブルな開発環境を作れなかった。そして今でも python のポータブルな環境構築は難しい。(自分は pyenv/pipenv を使ってるが、どうも主流になりそうにない…)

Python でプログラミングの基礎を覚えて、基本的な処理を不自由なくかけるようになったが、就職活動をやる必要があった当時、仕事がまったくなかったので、 JavaScript というか当時出たばかりの Node.js に手を出した。

JavaScript (Node.js) 2年目〜

  • 非同期の概念: この時点でもまだコードは上から下に読み下すもの、という意識があったので、読み下した順に上から時系列に実行されない非同期コールバックという概念を掴むのに時間が掛かった。
  • 名前空間の概念: window に紐づくグローバル変数と、レキシカルなスコープで宣言されたものにアクセスできる、というのに時間が掛かった。上の非同期のスコープの概念と合わさって、かなり理解に時間を要した記憶がある。
  • Promise: 非同期コールバックのプログラミングパターンが「処理の継続」なら、 Promise のは「いずれ解決される値のチェーン」と言えるが、そこで生まれるプログラミングの仕方の変化に対応するのに時間が掛かった。 また、 Promise は遅延以外に暗に例外も扱ってるのが話をややこしくしていると思う。 他の言語で言う Either と Future が混ざっている。

学んだこと

常に小さいプロジェクトを作って素振りする

仕事で書くコードは、いつだって現実と戦わねばならない。どんなに理想的な目標を掲げるエンジニアが設計していても、必ず経年劣化や、止むに止まれぬ事情で、技術的な負債がある。

そのような環境では、既存のコードの「重み」がノイズになってしまう。だから、新しいライブラリを試すときは、ノイズが少ない小さな環境で素振りをする。そしてライブラリが達成する本質的な課題や、その上で発生する別の課題を理解する。

自分はGitHubで気になったライブラリは必ず小さなリポジトリで素振りして評価するように心がけている。いきなり仕事のプロダクトに突っ込むことはしない。

仕事と同じスタックで、趣味のプロダクトを作るのは、とても勉強になるので、誰にとってもおすすめ。

静的型付け・オブジェクト指向の必要性

Java の過剰な抽象度とそれによる手数の多さから、自分はうんざりして Python にいったのだが、そのときに自分が書いたプログラムが自分の手足のように動く感覚、万能感を感じた。そしてパッケージマネージャからのインストールを覚えたあたりで、自分の能力が無限に拡張されるような感覚を覚えた。

しかし一定以上の規模のコードを書くには、何かしらルールが必要なのもわかってきた。自分でわかってるつもりになって書いたコードですら、5000行書いたあたりからいつも破綻しはじめる。

静的型付けが常に必須というわけではないが、静的型付けがない大規模環境には、その分テストコードを書く文化だったり、手動テストを多めに要求されたり、ドキュメントを書く文化だったりがある。あるいは、お行儀が良いコードパターンが決まっていて、それをレビューで要求されたりする。その経験を経て、自分は静的型付けってのは非常にコスパがよいドキュメント兼リンターなのだと理解した。(ので自分は TypeScript を使っている)

オブジェクト指向、というかその言語におけるオブジェクト指向デザパタは、デザパタをわかってる人同士のプロトコル伝達のためのツールであって、ゼロからAPIの体系を構築するより、効率的、ということを学んだ。

個人的な持論だが、初学者が Java や C の言語制約を乗り越えながらプログラミングの面白さを感じるのは難しいと思う。どうしてもプログラミングのためのプログラミングに終始しがちなので…

良いコードを書く必要性

良いコードを書くのは自己満足ではない。コード品質は、他のゴールに変換可能なバッファだ。

汚いコードを拡張しながら別の目的を達する汚いコードにするのは困難だが、綺麗なコードを元に汚いコードで目的を実現するのは比較的容易だ。汚いコードを高速化するには困難で、というか必ずリファクタして綺麗なコードを経由してからではないと、高速化というのは困難なことが多い。曲芸的なチューニングは基本的に品質を悪化させる。パフォーマンス目的で汚いコードを書くことはある(とくに数式をコードに落とすケース)が、その場合はコメントでその旨を記述して、かつ他に影響が出ないように影響を局所的に書かないといけない。

そして、自分が書いた経験の中で、良いコードの定義を持たないといけない。自分にとって良いコードとは、可読性があり、モジュール境界が明確で、テストしやすく、ツールによる静的な検査が可能で、拡張性があること。なので、非可逆な操作で名前空間をハックするタイプの DSL は一番やってはいけないこと、と認識している。

そして誤解されがちなのは、良いコードを書く条件は、「そのコードを書くのに掛けた時間とトレードオフ」、ではない。そういう側面も確かにあるが、支配的なのは「今までにコードを書いてきた総時間とのトレードオフ」で、いかによいパターンを事前に知っているか、が決め手になる。

時間があれば三回同じものを実装すると良いものになる、とはそういうことだと思う。

「ハック」の副作用

OSSのライブラリにパッチを当てて運用したり、グローバルなオブジェクトを書き換えたり、そういう所作を「ハッカー的でかっこいい」という見方もある。

しかし、それらの多くのケースで、ライブラリの責務を誤解していたり、単なる無理解でそのようなハックに手を出す人が多い。ハックは目先の問題を解決するが、それがどれぐらい重大な副作用が発生しうるか、想像できる必要がある。

例えば、ある言語のコミッタがある問題を解決するのにGCを止めて処理させていたりしたのを見たが、それはその言語のGCに対して深い理解がないとできないし、おそらく知識があったとしても推奨される解決策ではないだろう。

ライブラリ選定

OSS の時代、最新の流行についていかないことはリスクだと認識している。セキュリティの問題もあるが、ドキュメントの問題が大きい。誰も使ってないライブラリや、古すぎるバージョンで発生した解決策は、誰も知らないし、ドキュメントにも残ってない(ドキュメントは最新版以外残りづらい)。そして自分でコードを読んで、最悪パッチを当てて解決するが、そうするとよりメインラインと乖離してしまう。

フロントエンドの場合は、デザインのトレンドや、デバイスのライフサイクルの問題で、5~10年でUIを作り直すことが多いと思う。しかし技術的な大きなパラダイムが5年おきに変わる。これがフロントエンドの疲弊の原因であると理解している。

しかし、ライブラリが死んで、次のライブラリが出たとき、前のライブラリで発生した問題を踏まないように設計されたものが次のトレンドになる。ので、前世代を知らないと次世代のものを評価できない。エコシステムの変化は連続的であるので、自分がその道のプロを名乗るなら、連続的な変化を学ぶために常に追い続けなければならい。

ライブラリを使わないことも意識すべきだ。あるライブラリを自分の環境に入れたとしても、そのコードの一部しか使わない。その一部のために、全体として負債となる可能性があるライブラリなら、可能なら避けたほうが良い。「確かに目的に沿うが解決のアプローチがそもそも間違っていて他に誰も使ってないマイナーなライブラリを採用して後に負債になってしまう」のが初心者が陥りやすいアンチパターンだと思う。

DRY の守破離

「同じような処理は関数でまとめましょう」と最初は習う。そのとおりだと思う。

しかし同じ関数で異なる目的のものが混ざっていたり、実は無関係なものが一緒になってしまったり、するぐらいなら、DRY にしないほうがいいケースもある。

DRYのアンチパターンは、コードをグルーピングして抽出する際に、モジュール境界を破壊してしまうことだ。例えば、 Model の関数が View を引数にとって、 Model と View 双方に副作用を起こす関数は、確かに処理としてはまとまるかもしれないが、責務としては適切ではない。

モジュール境界面を意識することは、以下にテストしやすいコードを書いてるかというのと同義なので、「今書いてるコードにテストを書くならどうなるだろう」というのは常に意識すると、良いコードを目指しやすい。

クリーンアーキテクチャや DDD の戦略を知ることは、モジュール境界を意識させてくれる。それらを常に採用するべきとは言わないが、知っておくことは価値がある。

おわり

みんなも何に詰まったか、書いてみてください