(2021/2/25 内容を見直し、加筆訂正しました)
先日、こちらの記事で組織やプロジェクトの運営におけるコミニュケーションパスの問題について書きました。
同じような問題が、プログラミングにおいても言えます*1。
プログラムにおける変数や関数、クラスやメソッドなどのオブジェクト*2をノードとして見ると、それらにアクセスするという行為は、コミュニケーションを取っているのと同じです。
下の図のように、プログラムとは、様々なノードとノード間のコミュニケーションパスの塊とみなすことができます。
変数や関数をグローバルに参照するとすぐにスパゲッティ化するのは、コミニュケーションパスの本数が爆発的に増加することも原因となっています。
会社やプロジェクトなどの組織の場合、せいぜい数百個程度のノードを相手にすれば良い話ですが(それでも問題ですが)、プログラミングの場合、数千や数万のノードがひしめき合うことになります。そのため、なんの対処もせずにプログラムを書くと、絶望的なほどにコミュニケーションパスが増大し、手に負えないほどに複雑化してしまうのです。
プログラムの複雑さは、コミニュケーションパスの量に比例します。
逆に言うと、コミニュケーションパスを減らすことができれば、ソフトウェアの複雑化を抑えることができるということです。つまり、コミュニケーションパスの量を量ることで、ソフトウェアの複雑さを数値化することができ、より良い進化の方向性について、定量的に判断することができるかもしれません*3。
コミニュケーションパスの性質
コミニュケーションパスとは、お互いにやりとりしあう可能性のあるノード同士を結ぶ線のことです。
全てのノードが、全てのノードに対してコミュニケーションを取り得る場合、「ノード数 × (ノード数 - 1) ÷ 2」で本数を計算することが出来ます。
試しのノード数2000までのコミュニケーションパスの増加をグラフにしてみます。
2000ノードの時のコミュニケーションパスの本数が1999000本となっており、相当「ヤバイ」感じがします。(^^;
ただし、増加数の増加(つまり微分)は、だんだん1に近づくようで、最初急峻に立ち上がりながら大きな数になると伸び率が1倍に近づいて安定するようです。
とはいえ、2000ノードの時点で約1000倍、10000ノードの時は49995000本と約5000倍となっていますので、単純な線形の伸びよりは遥かに増加率が大きいのですが・・・ジワジワ伸びる感じですね。(^^;
客 「〇〇機能を追加しよう。設定を追加すればできるでしょ?」
僕 「いやいや、これ以上設定を増やしたらヤバイっす。。。でも、制御しきれなくなるほどではない気もするし・・・。うーん」
というように「真綿で首を絞めるように苦しくなっていく」感じを数字で表したかと思うと、妙な納得感があります。
アプローチ
コミュニケーションパスの増加を押さえるための手段は、大まかに分けてこのような感じになります。
- ノードを減らす
- ノードをグループ化する
- ノード間の通信を減らす
それぞれのアプローチについて説明します。
ノードを減らす
ノードを減らすというのは、共通処理をまとめるところから始まって、クラスの継承やインターフェースを利用して、関数や変数の数を単純に減らすという方法です。
単純ですが、問題に真っ向から立ち向かっており、効果も期待できます。
別のエントリーでも書きましたが、処理の抽象化というのは、難しい作業となります。
オブジェクト指向プログラミング言語では、クラスの継承やインターフェースを使用して処理の抽象化を表現します。また、クラスを分割し役割分担することで、担当する役割の処理を複数のクラスに書かなくてもいいようにするなど、色々なテクニックを駆使してプログラムに係るノードの数を減らそうと試みます。
関数型プログラミング言語では、型クラスや代数的データ型を利用して、膨大な数のケースの組み合わせをシンプルに抽象化し、必要に応じて自由に組み合わせて計算をします。
この基板の上に、高階関数やモナドなどの仕組みが乗り、高度な抽象化を実現しています。
ノードをグループ化する
ノードを分割することによって、コミュニケーションパスの本数は劇的に少なくなります。
オブジェクト指向プログラミング言語では、モジュール化であったり、ローカルスコープやクラスによるカプセル化が相当します。
しかし、クラスによるカプセル化は、クラス自体が巨大になると効果が薄れるという問題があります。
そのため、クラスに持たせる機能はなるべく単機能でシンプルにして、クラスとクラスの関係性によって処理を記述していくというのは、オブジェクト指向プログラミングの特徴的な方法論となります。
しかし、クラスが増えていくとクラス間のコミュニケーションパスが増加する事になるため、ここのバランスが難しいところです。
また、特定のクラスを多数のクラスが参照している状態では、グローバル変数と同じ問題が発生する可能性があります。
ノード間の通信を減らす
ノード間の通信を減らすというのは、変数をイミュータブルにすることによって、読み取り専用としたり、関数の副作用をなくしていくアプローチです。
これは主に関数型言語を用いたプログラミングの際に良しとされる特徴です。
変数の変更は、それを参照する(=スコープに入っている)処理に影響を与えます。
これは、間接的にスコープ内の処理通しがコミュニケーションパスを発生させることを意味します。
変更の発生しないオブジェクトや変数は、読み取り専用となるわけですが、この場合間接的なコミュニケーションは発生しないため、コミュニケーションパスの増加が抑えられます。
言語のサポートがあるかは別として、イミュータブルパターンは、どんなプログラミングスタイルでも有効なので、みんなせっせと取り組んでいます。
関数の参照透明性
全体的に関数型プログラミングの方向に誘導されつつありますが、関数の参照透明性とは、関数に渡した引数が戻り値以外の副作用を発生させない事を指します。
参照透明性のある関数を組み合わせてプログラミングすると、出来上がったプログラムは、きれいなツリー構造になります。
これは、組織におけるコミュニケーションパスの問題 - セカイノカタチの時に示したコミュニケーションパスを最小化する3つの基本的な形に即しています。
プログラムは、この形を保っている以上、コミュニケーションパスを最小化することができるということです。
ここにミュータブルなオブジェクトやグローバル変数、外部とのIOなどの副作用が加わると、間接的なコミュニケーションにより、あっという間にプログラムが複雑化してしまいます。
まとめ
ノードを減らすというのは、プログラムを抽象化していく作業です。
どのプログラミングパラダイムでも、それぞれのアプローチで色々な機能がサポートされています。
高度なテクニックを用いて抽象化されたプログラムは、シンプルに対象物を最小限の複雑さで構成することが出来ます。
その代わりプログラムは難しくなります。(^^;
ノードをグループ化するというアプローチは、主にオブジェクト指向プログラミングが得意とする手法です。
クラスやパッケージに分割していって、カプセル化することによってノードの結合を分割することができるのです。
一方、関数型プログラミング言語は、ノードをツリー状に構成することによって、コミュニケーションパスを最小化しようとしています。
コミュニケーションパスの観点から見た時に、オブジェクト指向プログラミングのアプローチは、その最小化に目標が定まっていないように見えるため、十分に正しいとは言えないのかもしれません。
ノード間の通信を減らすという観点は、オブジェクト指向プログラミングにあまり見られなかった観点です。
参照透明性を確保することにより、プログラムは、ツリー構成となります。
コミュニケーションパスは最小限になり、影響しあうノードを劇的に減らすことが可能です。
コミュニケーションパスの最小化という観点で両者を比較した場合、関数型プログラミングのアプローチのほうが優れています。
というか、偶然だと思いますが、関数型プログラミング言語は、コミュニケーションパスの最小化を真っ直ぐに目指しています。
これは、方向性としては非常に正しく、プログラミングの一つの到達点なんだと思います。
参照透明性は、オブジェクト指向プログラミング言語でも実現可能なことですので、どちらのパラダイムでも積極的に目指して行けたらいいなと思いました。