プログラム入門:抽象化の話
プログラムの入門としての抽象化の話をします。ベテラン向けじゃないんで、ベテランの人は読んでも新しい事は無いと思います。
データ分析界隈だと、数学や物理などで抽象的な事というのには良く出会ってきた経験を持つので、抽象的な物は得意な方だ、という人は多いと思うし、実際そうだと思う。
ただ、プログラムの抽象化は違う部分もあるので、一度プログラムの抽象化という物を初心者の気持ちになって学んでみる機会は必要とも思う。 ここではそんな話をしてみる。
カプセル化
プログラムの抽象化と言えばカプセル化なのだが、いまいち良い解説が無いので自分で書く事にする。
一般的な事は知らないけれど、プログラムにおいては、抽象化の定義は「情報を落とす」事となっている。 全ての情報を持っている所から、特定の情報だけにする事でアクセスする「口」を定める訳だ。
だからだいたいの抽象化は物を「削る」のだけど、一つだけ例外がある それが「カプセル化」。カプセル化はプログラムにおいて特に重要な抽象化となる。
カプセル化の定義
カプセル化とは、「複数の物をまとめて名前を付けて、その名前で一つの物として扱う事」というのが定義となる。 学術的なちゃんとした定義はどっかで調べてください。(ちょっとググったが手頃なのが見つからなかった)。
例えば構造体とかはカプセル化っぽい。以下みたいなの。
struct Polar {
double r;
double theta;
};
rとthetaという二つの浮動小数点変数を一つにまとめて、Polarという名前をつけている。
カプセル化が他の抽象化と違う所として、「新しいエンティティを生み出す」という所がある。 他の抽象化はだいたい削るのが基本なのだが、カプセル化だけは「生み出す」。
ただし抽象化の定義には合致していて、情報は捨てている。 エンティティの名前で呼んでいる時は、そのほかの二つのdoubleで表せる概念、例えば身長と体重の組では無くなっている。 もともとのdouble二つでは極座標でも身長と体重の組でもなんでも扱えるのだが、 Polarにした後はそういうなんでも扱える部分の一部だけに限定されて、極座標だけを扱う事になった。
関数を作るのもカプセル化といえる。
double average(double* vals, int len) {
double sum = 0.0;
for(int i = 0; i < len; i++) {
sum += vals[i];
}
return sum/len;
}
複数の命令、forを回したり足し合わせたり長さで割ったり、といった命令をまとめて、averageという名前をつけている。 使う側はaverageという名前でこの命令全体を実行するのと同じ事が実行出来る。
複数の要素をまとめて一つの名前をつけて、その名前でもって一つのエンティティとして扱う事、これをカプセル化という。
カプセル化とは概念を生み出す事
複数の物を集めて名前をつける、というのはあまりにも汎用的すぎて、入門のうちはその概念を改めて考えてみるご利益は少ないと思う。 ただプログラムを書くようになったら、どこかのタイミングで考えてみるに値する。
カプセル化、ひるがえってプログラムが特別な所は、概念を生み出す機会の多さにある。 数学でも変数はつけるので、概念を生み出している事もちょくちょくある。 ただ、プログラム的な意味で複雑な概念を生み出す機会は、新しい数学を作る時くらいしか無い。 そういう場はプログラム程は頻繁では無いと思う。
プログラムでは、新しい概念を生み出す事を日常的に行う。 だから数学等で抽象的な物になれていても、この概念を生み出す所の抽象化は少し初心者の気持ちになって学ぶ姿勢が必要な所と思う。
プログラムというのは、究極的には一番下の命令を羅列するだけで目的の挙動をさせる事は出来る。 だからプログラムの初期の段階では、解きたい問題に対して、その解法を記述する命令を羅列する、という活動となる。
プログラムを読む時も、最初のうちは命令列の羅列まで掘り下げていく事が読む事であり、 抽象を壊して全情報に戻す作業が理解する、という事になる。
だが、これは入門時だけの事で、一定以上の現実的なプログラミングにおいては、 それよりも抽象を構築する事の方がメインとなる。 その問題に合わせた「概念を生み出す」というのがプログラムの中心的活動、と言っても過言では無い。
理解をする時も、一番下の命令の羅列までブレークダウンするのでは無く、構築されている抽象を理解するように心がける。
プログラムをする時は、いつもこの概念を生み出す、という事について自覚的になって、 どういう概念を生み出すべきか?という風に考える必要がある。 概念を生み出す時には、いつもカプセル化がその主体となる。 数学とかをたくさん触ってきた人は特に、このカプセル化について改めて考えてみて、 プログラムを見直してみる価値があると思う。
抽象化の二面性
概念を生み出すのがプログラムの重要な特徴だが、この生み出した概念、 というのは、「そう考えてやる」事で初めて確固とした実在を得る。 逆にそう考えてやらないと、あまりちゃんとした概念として存在出来ない。
だいたいの抽象化には、二面性がある。 それは使う側と作る側だ。
感覚的には、水面に浮かんでいる葉っぱを地上から見るのと、水面の下から見る、という二つの見方があり、この二つの見方を「意図的に」切り替える必要がある。
先ほどの平均を求める関数を考えよう。
double average(double* vals, int len) {
double sum = 0.0;
for(int i = 0; i < len; i++) {
sum += vals[i];
}
return sum/len;
}
使う側から見た抽象化
これは、使う側からは、以下のようなシグニチャで、
double average(double* vals, int len)
配列の平均を求めるようにふるまう、という風に考える。
実装がどうなっているか、という事は考えずに、提供する機能だけに注目してやる事が大切。 使う側からは「XXという名前でYYという機能を提供している」と、「何をしてくれるか」という視点で考えるのが大切。
中の実装も分かっているのだけど、あえてそこを使う側で考える時は「知らないフリ」をする。 この頭の切り替えがプログラムにおける抽象化の大切なポイント。 実装をあえて「忘れる」事で、提供している「何」に集中する。 この意識を集中させる事で、作った概念がより確固とした物になる。
この、実装をあえて「考えない」で、提供する「何」に着目するのは、出来るようになるべく意識して訓練する必要がある。(そんな難しくも無いけど)
これが水面の葉っぱを地上から見た場合。
作る側から見た抽象化
抽象化にはいつも二面性がある。使う側は既に述べた一面。 もう一つの面は実装の側となる。
実装に着目する時は、使う側から見る時とは反対に頭を切り替えて、実現方法の方に着目する。
着目するのは、実装、つまり以下の所になる。
double sum = 0.0;
for(int i = 0; i < len; i++) {
sum += vals[i];
}
return sum/len;
これこそがaverageという物だ、というのが、実装側から見た視点となる。 葉っぱを水の中から見上げるイメージ。 どう使われるかでは無くて、どう実現しているか、という方向から考える。
こちらの視点は初心者でも普通に持っている物なので難しい事は無い。 ただ、初心者はいつもこちらの視点を持ち続けてしまう。 プログラムにおける抽象化においては、こちらの視点は意図した時だけに限って、 その場合はこちらに集中する、というのが重要。
いつもなんとなく実装を思ってはいけない。あくまで「実装側から見るぞ!」と思った時だけ見る。 この見方を頻繁に切り替えて、両方一緒には見ないのが大切。
問題領域の言葉で問題を記述する
encapsulationで概念を作り出す事が大切で、その概念をはっきりした物とする為には使う側と作る側を意識してはっきり分けて考える事が大切、という話をした。 これで概念を作り出す事は出来るようになった。
次に、ではその作り出す能力を使って、「どういう概念を作るか?」という事を考えたい。
プログラムの基本としては、まず使う言葉を「問題領域の言葉」まで引き上げて、その問題領域の言葉で問題を記述する、というのがある。 この問題領域の言葉を作るのに「概念を作る」ということを使っていく。
問題領域の言葉の例
何が問題領域の言葉か、というのは明確な定義がある訳じゃないが、いくつかの例を挙げることは出来る。 そこからなんとなく感じ取ることは出来ると思うので、例を挙げていこう。
例えば「線形リストのある値のノードまでたどって、その一つ前のnextを新しく作ったノードに差し替えて、新しく作ったノードのnextを先程見つけたノードのnextに差し替える」という表現は、いかにも低レベルな表現だ。 これよりは、「新しくノードを作って、適切な場所に挿入する」の方が抽象度は高い。 さらにこれがスプレッドシートのアプリを作っているなら、「シートのある列の隣に、新しい列を挿入する」の方がよりスプレッドシート、という問題に近い。 この「シートのある列の隣に、新しい列を挿入する」が問題領域の言葉と言える。 「シート」、「列」、こうしたものは問題領域の言葉だ。 なるべくそういう言葉でコードを書く方が良い。
もう一つ例を挙げよう。 「HTTPのリクエストを投げて、結果のxmlのarticleという要素のtitleの一覧を集めて表示し、その表示のtagにはこのarticleのサブツリーをぶら下げておく」よりは、「最新の記事の一覧をリクエストし、記事の一覧のタイトルを表示する。tagには記事をぶら下げておく」の方がより問題領域の言葉に近い。 HTTPとかxmlとかそのelementの名前よりは、最新の記事とか記事の方がより問題領域の言葉に近い。
arrのposに値を代入してpos++するよりは、値をスタックにプッシュする方が問題領域の言葉にちょっと近い(問題に依るが)。 SQLにitemIdをキーにしてtagcloudのフィールドにtagidをもったレコードを追加するよりは、itemに対するタグを追加する、の方が良い。
問題領域とは、解こうとしている問題の言葉なので、解こうとしている問題によって何がより問題領域に近いか、は変わっていく。 ただ以上の例で雰囲気はわかると思う。
言葉を作る、という場合の言葉とは?
言葉というのは抽象的な言い方なのだが、これが抽象的な言い方がもっとも適切なので少し説明がわかりにくい部分もある。 例えば新しい問題領域用のプログラム言語を作るなら文法とか語彙が必要になり、いわゆるDSLを作る、という事になる。 一方で問題領域の言葉を作る、と言った場合、普通はDSLに限らずもっと単純なAPIとかも言葉とみなす。 そこで何を言葉とするかは曖昧なのだが、例を挙げてなんとなく空気を感じてもらおう。
もとの問題が大きなAという記述をされているとする。 これから一部を切り出してBという概念を作り、Aのコードを一部Bに切り出す。 で、AはBを使う形でA’という形に変える。
この時にBに対する操作というのが一番原始的な意味での言葉となる。 これだけでは言葉というにはあまりにもしょぼいのだけど、こうして登場キャラクターが増えてきて操作も増えていくと、割と立派な言葉に育っていく。
ただ、単に例えば関数とかクラスでは不十分な場合ももちろんあり、より言葉として複雑である方が望ましくなってくると、何らかの形のDSLを作る事も自然な延長としてありえる。 特に言語内で、ホスト言語と切り離したセマンティクスを与えやすい仕組みのある言語では、比較的一般的でもある。 C#のLinqやkotlinのtype safe builderなどはそうした問題領域の言葉をリッチにしやすい言語機能と言える。
抽象化という観点からすると、これらの問題はあまり本質的では無い。 より本質的には、あいまいな言い方になるが「言葉っぽいと感じられるのが言葉」ということになる。
言葉っぽいと感じるためのヒント
言葉を作っていく感覚を養うには、「言葉とはXXだ」という定義から始めるよりも、 どういう物を言葉っぽいと感じるかのヒントのようなところからはじめて、それを意識しつつ日々コーディングをしていく中で学んでいくのが良いと思う。
そこでいくつか、自分が言葉っぽいと感じる時に思う条件をつらつらと書いていく。
抽象度が揃っている
例えばバイト配列に対して値を吐き出していくEmitterというクラスがあるとしよう。
Emitterにemit_intがあるなら、emit_stringはあって良いと思う。 一方で、ldr命令のレジスタの場合を吐き出す、emit_ldr_registerはちょっと抽象度がずれているので同じ階層にあるべきでは無い。
Emitterが言葉的に思えるためには、そのメソッドの抽象度が揃っている方が良い。
組み合わせに変な制約が無い
問題をブレークダウンしていく過程でモジュール化が進むと、もともとはある関数Bは、別の関数Aのあとでしか呼ばれない、という場合がある。 こういう、言葉からは想像されない裏の条件があるのはあまり言葉っぽく無い。
ブレークダウンしていったもとの問題からは生まれない組み合わせでも動く、というのは言葉っぽさを感じる瞬間だ。 もとの問題から離れても動く事で、抽象化の2つの視点、「使う側からの視点」がより強化される。
そのレイヤーの中の構成要素で自由に組み合わせを変えても字面の期待通りの振る舞いをする時、 これを言葉っぽいと感じる。
後述するボトムアップで作っていく場合、ここが一番重要なポイントと思う。
一方でどんな呼び方してもちゃんと動くように作る、というのは、必要無い制約を勝手に設けて問題を難しくしてしまう部分もあるので、ここはトレードオフ。ただ言葉っぽく感じるという観点からすれば、自由な組み合わせが許される方が良い。
扱っている問題領域にそもそもそういう言葉がある
これは結構重要で、今扱っている抽象度のレイヤがそもそもプログラムの外の世界にも存在していて、そこにそういう言葉がある場合、それは言葉っぽさを感じる。
例えばBankAccountにdepositとかwithdrawとかtransferがあると言葉っぽい。
いつもドメインの言葉をそのまま定義すれば良いとは限らないが、定義する事が出来てそれが良い抽象になりそうな時は積極的にやりたい。 この時はドメインの言葉で問題が書けるので、恩恵は大きい。
言葉をボトムアップで作るかトップダウンで作るか
問題領域の言葉というのは、普通作り始める段階では何が適切かはわかっていない。 それを知る、というのはプログラムというアクティビティの本質的な部分である。 そして何を作るかがわかっていないのだから、それをどう作るかも当然分かっていない。
さて、問題領域の言葉というのは、普通それ自体がほとんど答えに近い。 だから問題領域の言葉を作るのは、直接問題を解くのよりほんの少しマシなくらいしか簡単になってない。 だから問題領域の言葉をそのまま作るのは普通は難し過ぎるので、直接それを作るのではなく、その一つ下のサブ問題領域の言葉で作る事になる、で、その為にはサブ領域の言葉も作ることになる。 このサブ問題領域の言葉もだいたいは複雑なので、さらに下にサブサブ領域の言葉を作ることになる。
こうして、言葉はレイヤーになることが多い。
さて、とつぜん真ん中あたりのレイヤーの言葉を作る、というのは普通は出来ない。 何が必要か良く分からないし、あまりにも漠然とし過ぎているからだ。
一方で、普通は問題領域のそばと、一番下の実装のそばのレイヤーはなんとなくわかることが多い(往々にして最終的にはこの最初の分かってるつもりの物とは似ても似つかない物になりがちだが)。 問題領域の言葉は解こうとしている問題の世界で普通に出てくる物が多い。 実装の一つ上のレイヤーは、問題の解法を実装していこうとしたら必要になるutility関数のような物から始まりがちだ。
ということで、問題領域の言葉を作っていく時には、一番上から作るかと、一番下から作るかの二通りの方法がある。というか普通は両方から交互に進めていく感じになる。
まず、問題領域の言葉を考える。その言葉を表すインターフェースくらいをぼんやり決める。 ただ実装はまずはハリボテ。なんとなく問題をその言葉で記述する。
次に実装。必要な物を作るのに必要な物を一段くらい考える。その上に何が載るかは良く分からない。 こちらは割と具体的に作れるが、実は必要でない物を作ってしまうのが良くある。 そこで軽く必要そうな物を雑に作って、ハリボテな感じに一応つなげようとしてみる。 もちろん間が無いのできれいにはつながらないのだが、そのところで、上はもうちょっと下にこう降りていかないとだめだな、と気付き、下側のレイヤはもっと上にこういうのを足していかないとだめだな、と分かっていく。
この上と下の両方からちょっとずつ埋めていく。
プログラムの入門ではとりあえず実装をしてそれを整理していく、という形に進みがちだし、それはそれで良い事もあるが、このやり方は小さな問題でしか使えない。 より難しい問題を解くときには、よりしっかりとした抽象概念が必要で、それを作るには練習が要る。
言葉っぽくするために良いように分割
問題AからBを切り出して、A’とBにするケースを考えよう。
あるコードAを切り分ける時に、どこをA’に残し、どこをBに移すか、というのを決める必要がある。 これを考えるのに、「それをBに足すのは、Bのドメインの言葉として適切か?」というのは重要な基準となる。 また、Bという物自体を考え出すのにも、それが「言葉として良さそうか?」という視点が大切となる。
単に長いから切り出す、から一歩進んで、作り出す抽象としてはどちらが良いか?という視点でコードを切り分けて、時にはコードを追加して切り分けたりしていくようになりたい。
中規模のプログラムの作り方
ここまでの話が重要になる一番の瞬間というのは、これから作るプログラムの規模が結構大きい、となった時だろう。
問題が一度に考えられない規模になる時が来る
プログラムの入門時は、解き方が隅から隅まで分かる問題を解く。 だからこの規模の問題では、最初にただ動くだけのコードを書く事は、 そんなに抽象化に気を配ってなくても書ける。 一度書いてからコードを整理していく過程で重複を関数にしたり、という風に進められる。
だが、一定より規模が大きくなると、一度に考えられる限界を超える。 個人差はあるが、目安としては3000行くらいが限界の閾値と思う。 3000行を超えるくらいの規模になると、一度に頭の中に全ての答えを書き切る事が出来なくなるので、 まずベタに書いてからくくりだす、という方法が使えなくなる。
この一度にはプログラムの全体が把握出来なくなる規模を、中規模、と呼ぶ事にしよう。
この規模の問題に挑むようになった時に、ここまでの抽象化の話がクリティカルに重要になってくる。
なお、余談だがデータ分析屋など数学や理論系に強い人は、この容量が普通のプログラマより大きい事が多い。 だから普通のプログラマより限界が来るのが遅いせいで、意識してないとかえってこの実体を作り出す抽象化能力を獲得する時期が遅れてしまう場合がある。(ただしこの容量は最終的には大きい方が良い。後述)
トップダウンに考える場合
一度に全てのプログラムを書けない時にどう書くか? トップダウンとボトムアップの両方のやり方があるが、 初めて中規模のプログラムが書けるようになる瞬間というのは、だいたいトップダウンにプログラムが書けるようになった時だと思う。
まず、その解くべき問題を記述するのにちょうど良い抽象度の言葉を考える。 実装よりも前に言葉を考えて、「こんな言葉があったらいいな」と想像する訳だ。 そしてその言葉で解法を記述する。 この時は「ちょうど良い抽象度の言葉」は意識的に使う側の面から見る。
この時にだいたい1000行以下くらいに収まるようになっていれば、この階層では隅から隅まで考えられるので、答えが入門の時と同じように記述出来る。 ただここには、この「ちょうど良い抽象度の言葉」を生み出す、という新しいスキルが必要になる。
この後にその「ちょうど良い抽象度の言葉」を実現する必要がある。 そこでこれを実装する為に、見る角度を切り替えて実装側からこの言葉を見る。
この先に言葉を考えて、あとから実装を考える、という考え方が逆になるのが、中規模のプログラムのむずかしさと言える(そんな大した事でもないが)。
「ちょうど良い抽象度」というのは曖昧な表現だが、例えば問題を解くという、solve、という言葉を定義してsolve()とか一行だけ書く事にはほぼ意味が無い。 Application.run()とかセンスの無いフレームワークにはありがちだが、こういう抽象は無駄だ。
問題を記述する価値がある程度には解法からは遠いが、解の記述が複雑になりすぎない程度には近い、という距離感が必要。 また、単に問題からの距離や記述量だけじゃなくて、言葉として適切な深さ、というのもある。 何かしらの抽象度に既に人間は概念を持っている事が多くて、目的の深さのそばにあるそれらの概念にマッピングするのが良い。 完全に新しく言葉を生み出す必要がある場合もあるが、これは一段難度が高く、 そんなには機会は無いので(これが一番プログラムの面白い所でもあるが)、まずは既存の抽象を考えながら作るのが良い。
「銀行口座」に「預金」を「預ける」とか。
言葉を考えられるようになる為には、既存の言葉を知るのが早い
言葉を作るのが大切、というのは良いとする。 で、実際にちょうど良い言葉を作ろう、と思っても、どこから手をつけて良いのか、なかなか難しい。
そこで最初は、既存の中規模のコードを読むのが良いと思う。 何を読むかは好きな物で良いが、今勉強しているものが良いんじゃないか。 コンパイラなら言語処理系、OSならなんかのカーネル。
ただ、あんまり本格的な奴は辛いので、小さくてクリーンな奴を読むのが良い。 gccやclangじゃなくてawkくらいにしておく、とか、LinuxじゃなくてMINIXくらいにしておく、とか。 目安としてはコアが3万行くらい以下が良い。 また、現在の複雑になってしまった最新バージョンじゃなくて、 なるべく初期の、バージョン1とかも小さくて良い。
コードを読む時に、ただ読むのでは無くて、この作られている言葉を意識して読んでいくと、 「なるほど、こういう感じに作るもんなのか」というのが自分の中で貯まっていって、 自分で書く時も書けるようになると思う。 プログラマならだいたい誰でも「XXという中規模のコードを読んで、同じ規模の物が書けるようになった」という瞬間を覚えているものだ。 自分の場合は学部4年の頃か?DOSモバのGUIライブラリである、mnwというののコードを読んだ時だった。 今でもその時の感動は覚えてます。
言葉に意識する場合は、C言語とかならヘッダファイルに言葉が定義されるので、ディレクトリ構造とヘッダファイルからどういう言葉なのかを頑張って読み取ろうとする必要がある。 と言っても見るだけだとよっぽど良く書かれているソフトウェアじゃないと分からないが。
そこでヘッダファイルとかから、どういう言葉か予想する。 そしてその使われ方を見る事でその予想があってるかを確認し、間違えていそうならこの予想を修正する。 この「想像ー>確認」を何回かやって、言葉の「実装を見ずに」どういう物かを把握する。 こういう風に、意識的に作られている抽象を「使う側の面から見て」把握する訓練をする。
C言語じゃなくてもだいたいは何らかのインターフェースを知る方法があるのが普通なので、そちらに着目するのは変わらない。 Kotlinとかならクラス名やpublic methodのAPIに着目して同じ事をする。 なおkotlinとかの方がIDEで使われてる場所の一覧が簡単に見れるので、慣れると楽。
ちなみに、この話のもとになっている「暇つぶしCプログラミング教室」も、この中規模の入り口くらいになるように問題を選んでいる。 こちらの誘導がある程度この言葉を作るという事を想定して書いているので、課題をやっていく過程で作るための試行錯誤が出来るんじゃないかと思っている。
ボトムアップに考える場合でも違いはある
中規模の問題が初めて解けるようになる時というのは、だいたいトップダウンに考える技術を身に着けた時だと言った。 だがこれは入口に過ぎない。やはりボトムアップにも考えられる方が、さらに先に進む時には重要になる。 たとえば解こうとしている問題が全然分からない時はボトムアップから始めて着想を得るのが普通。 ボトムアップに作っていくのは初心者でも出来るので、中規模な物がこれで作れるなら最初からそうすればいいじゃないか、という気もするかもしれない。
だがボトムアップに書くのも、やはり中規模のプログラムが書けるようになる為には少しジャンプがある。 なんとなく既存のコードから便利関数を作るのは初心者でも出来るのだが、 中規模のコードを書けるようになる為には一度作ったものの「実装を忘れる」必要がある。 というのは実装を覚えていると頭に全部入りきらないのが中規模の定義だからだ。
言葉を作ってその実装を忘れて、その言葉を使う側からだけ考えてさらに一つ上の抽象度の言葉を作る。 そういう事が出来る為には「実装を忘れられる程度にちゃんと言葉になっている」必要がある。 これは初心者のうちにはなかなか難しい。
そこで結局は、ボトムアップにやっていく時も、ある程度の規模のコードでどうやって下の方の言葉を作っていくか、 というのの答えを見る必要はある。 だから結局は上からやるのと同じように、既存のコードから学ぶ時期は必要。 どちらが中規模の壁を超えやすいかは微妙で、好みとか言語の違いかもしれない。 自分はトップダウンの方が先に出来るようになった。
だが、ボトムアップにやっていく方法の場合、言語を作り出す感覚は割と初期に学べるが、 初心者のうちはその出来が悪い事が多く、これを認識するのが難しい。 なんか規模を大きくしていくと変にバグって全然進まなくなる。 これを克服するのはトップダウンのように明確に出来た出来ないが認識出来ないのでより難しい気も個人的にはする。
要約すると、ボトムアップの方が言葉を作るのは簡単だが、酷い言葉を作りがちでその事に気づくのが難しい。
なんにせよ、最終的には両方必要になるので、どこかのタイミングでトップダウンもボトムアップも両方正しい抽象を生み出せるようになる必要がある。
余談だが、ボトムアップにやりやすい言語というか環境というのもある気がする。 例えばJava, C, C++, C#, JS, go¥あたりはトップダウンに書きやすい気がする。 PowerShell, R, ruby, F#, LispやSchemeあたりはボトムアップに書きやすい。
Python, kotlin, scalaあたりはどっちもやりやすい気もする。気のせいかもしれない。 kotlinはボトムアップ向きな言語かもしれないなぁ。
大規模な物は中規模よりさらに難しい
最後に余談だが、中規模な物が作れるようになると、一瞬すべてを理解した気になるのだが、実はそこからも結構先がある。 目安としては10万行を超えるくらいのプログラムあたりで限界が来る。 なんでか良くわからんがとにかくこの辺に何かある。
抽象を切り替えて各レイヤでの記述を短くしても、 深さが深くなりすぎて切り替えがうまく出来なくなる限界が来るからなのかなぁ。 理論的にはそんな限界なさそうな気がするんだが、確かにある。
結局完全には問題は分離出来ていないので、全体の理解がある程度は必要なんだと思う。
理由はおいといて、とにかく、レイヤが深すぎると何かの限界が来る。 そこで単に問題領域の言葉を適切に定義してそれで問題を記述するだけでは作れない規模というのがある。 そしてそれが必要な事がある。
これをどうするか、というのはこの記事より一段上の難度の話なのと、 最近のトレンドはそもそもそうならないように頑張る方がいいんじゃないか、 という方向に進んでいる気がするので、あんまりここでは突っ込まない。
ただ、大規模のコードベースでは、この深さ的な限界を避けるために、 少しレイヤーの数を減らして、そのぶん問題の記述の量を増やす、という事は割と行われている。
だから中規模な時の良いコードとは少し違う汚さが大規模なコードにはある事が多いが、 それは書いている人が素人なのじゃなくて、一段難しい問題に取り組んだ結果だから、という場合がある(単にへぼいプログラマが書いただけの場合もあるが。人が増えるとへぼいプログラマが入るのは例外というよりは必然だ)。 その辺の事わからずにイキって批判とかしてるのを見かけるとちょっと痛々しいがそれが若さか…
話を戻して。この大規模な問題の場合、一度に考えられる問題の領域は大きい方が良い。 これは先ほどいったように、複雑な数学とか物理とかやってた人間の方が大きい事が多くて有利と思う。 そして個人差があるので、中には大規模なコードが理解出来ない、というプログラマもいる。 というか普通は理解出来ない。出来る方がレアな気がする。 ただ訓練すれば誰でもある程度は身につけられる能力とは思うが。
あと、さらに余談だが、大規模なだけじゃなくて、問題の複雑さが本質的に結構な記述量を要求するような時も、 一度に考えられる問題の容量は大きい方がいい。 だから中規模なプログラムを書けるようになる時にはちょっと障害になる事もある大きな問題把握容量は、 長期的には重要な武器であり資質なので、大きな容量に越したことは無い、と個人的には思う。