物事の普遍性への追求は人類のテーマです。
時代と共に変わりゆくソフトウェア分野の技術の中で普遍的に存在しているのか、共通して存在する中核的な技術はあるのだろうかということを考えたいです。
新しいもの、古くからあるもの
近年、マイクロサービス、クリーンアーキテクチャなどの○○○アーキテクチャ、Reactを始めとするの宣言的UIなどがそれぞれ分野違えど注目を集めてるように思います。また時代にとらわらずUNIX哲学に代表されるコマンドが活用されています。
決して、新しく登場した技術だから良いという考えているわけではなく、新しいものは、古い技術の問題点を克服に作られていることが多いため、優れているものが多いと考えています。
それは分割統治なのではないか?
ソフトウェア技術の分野も広いのでいろんな視点があるとは思うのですが、分割統治は普遍的な技術の候補になるのではないかと思います。
ここでの分割統治は、よく例に上がるマージソートのようなアルゴリズムだけではなく、解くべき問題を小さい問題に解を合成して最終的な解を得ることだとします。「一つのことをうまく行い、協調させて動かす」ことが複雑なことを成し遂げる秘訣だろうということです。UNIX哲学 - Wikipedia
ソフトウェアの技術は幅広いため、他の分割統治とは異なる視点での普遍的な技術は他で書かれることを期待しつつ、この記事では自分の観測範囲からこの分割統治が共通して使われていると考えられる技術を集めたいと思います。そして最後に少し哲学的なことも考えたいと思います。
非同期処理 - async/await
いまでは多くの言語で違えはあれどasync/awaitが導入されています。JavaScript, Python, C#, Dart, Rust ...。async/await以前は基本的にシングルスレッドであるJavaScriptでは非同期処理にコールバックが使われていて、コールバックの大量の入れ子によって発生するコールバック地獄が問題とされていました。以下のようにJavaScriptでPromiseを使っても最初の方の値を使おうとすると入れ子が発生します。(この例ではPromise.all()
使って入れ子減らせますがこれは例をシンプルにするためです。)
f1().then(a =>
f2().then(b =>
f3().then(c =>
console.log(a + b + c)
)
)
);
以下のようにasync/awaitを使うことで、入れ子が消えフラットになります。
(async () => {
const a = await f1();
const b = await f2();
const c = await f3();
console.log(a + b + c);
})();
分割統治の視点で見るとPromiseを返すf1()
, f2()
などをasync/awaitで合成できていると考えられます。つまりPromiseというデータ構造にすることで非同期な処理を切り出し、合成することが容易になったと言えるでしょう。
async/awaitとモナドの比較
この煩わしい入れ子をフラットにする技術は決して新しいアイデアではないのではないかです。そこで思いつくのが、モナドの合成で用いるHaskellのdo記法やScalaのfor式です。async/awaitはdo記法やfor式に似ているのではないということです。
以下はdo記法を使わないHaskellです。先ほどの.then()
の入れ子ととてもよく似ていることが分かります。
main :: IO ()
main =
f1 >>= (\a ->
f2 >>= (\b ->
f3 >>= (\c ->
print (a + b + c)
)
)
)
上記をdo記法で書くと以下のようにフラットになりasync/awaitを使った時にとても見た目が似ていることが分かります。
main :: IO ()
main = do
a <- f1
b <- f2
c <- f3
print (a + b + c)
Haskellではdo記法はコンパイラが最初のコードに機械的に変換します(糖衣構文)。上記のような入れ子をフラットにする技術は特に新しくなくdo記法が今まで可能にしてきたと思います。
async/awaitの良さは非同期計算の合成を容易にしたことはもちろん、非同期処理と同期計算との合成も容易にしたことのように思えます。JavaScriptのasync/awaitでは、通常のif文/for文/while文でawaitでき同期処理のconsole.log()
もawaitと同じレベルに書けます。Haskellのdo記法の場合、同じ文脈のモナドに持ち上げる必要がでてきます。そういう点でasync/awaitは非同期に特化した構文を用意した分非同期処理と同期処理の合成も容易にしたと考えられます。Haskellは特定の計算に限定せずすべてのモナドに対してdo記法が使える点で一貫性があり覚えることが減りシンプルと言えます。
do記法を考えれば、async/awaitが解決した入れ子の問題はものすごく新しい技術というわけではなくとも、非同期・同期処理問わず分割統治しやすくなっています。
ストリーム, パイプ, コレクション
ここではデータを細かく分けて、そのデータを流して処理する時に使うものをざっくりとストリームデータ処理とします。身近なストリームデータ処理はシェルのパイプでしょう。コマンド | grep 好きな文字
で好きな文字列を探したり、tar c . | 暗号化 > 保存先
で.tarを暗号化しながら保存するかもしれません。Node.jsでは「ストリームを制するものはNode.jsを制す」といわれNode.jsでもストリームは大事な役割を担っています。
パイプ - Unixパイプ
分割統治の視点で見ると、パイプはコマンド同士を合成する技術とみれます。コマンドごとに得意分野があり、そのコマンドがC言語で書かれていてもGo言語で書かれていてもRustでも未来の言語になっても合成が可能です。またパイプでの合成以外の観点でも、パイプは一時データ(ファイル)を作らず時間的にも空間的(メモリ・ストレージ的)にも効率が良いです。それに加え無限の乱数バイト列もcat /dev/urandom | ...
のようにを扱うことが可能です。
.map(), .filter()など
あらゆる関数型に影響を受けている言語の配列やリスト(言語によってはコレクションに対して)にあるメソッドや関数として.map()
やfilter()
やreduce()
などの演算(高階関数)が言語ごとに多少命名が異なりますが存在します。これらのメソッドまたは関数は、引数に受け取る関数を処理を変えることができます。どんな関数と組みわせるかで.map()
や.filter()
であらゆるシーケンシャルなデータに対して演算できる点で分割統治できています。
遅延処理になる、JavaのStreamやHaskellのリストやApache Sparkのデータ構造などでは、パイプと同じく一時データを作らず時間的空間的に効率もよくなる利点も持ち合わせます。
React/Vue/Flutter/SwiftUI - 宣言的UI
最近、Webアプリでの宣言的なUIのコンポーネント化技術が多く使われているように感じます。モバイルネイティブアプリでもFlutter, SwiftUIやJetpack Composeなどの宣言的なUIが注目されていると思います。この宣言的UIに関しても従来の命令的なものと比較して、ビュー同士を分割し合成することがよりしやすくなったと、分割統治の視点で考えられると思います。
React Hooksのただ関数に切り出しただけのカスタムフックでビューとロジックを分割し合成がより容易になることが示されて、ロジックの再利用やテストも容易になっていると思います。Vue 3で標準で利用できる予定のVue Composition APIはこのReact Hooksに影響を受けています。ビューとロジックの分割統治されたと見ることができると思います。
一貫した概念
ここでは一貫した概念は、Unix/Linuxの「すべてファイル」、Haskellの「すべてが式」アクターモデルの「すべてがアクター」のようなことを意味したいと思います。
例えば、Reactなどで用いられるJSXはHTMLが式になったリテラルとしてJavaScript上で扱えるようにしたと考えられるのではないでしょうか。HTMLが式・オブジェクト・値になることによっていままでオブジェクトに使えてきたあらゆることが可能になります。つまりHTMLを引数に渡したり、戻り値として返したり、配列に要素として詰め込んだり、オブジェクトに対してできていたことをすべてHTMLに対しても行えるようになるということです。式となったHTMLは式が使えるあらゆる箇所で使えることで、既存の関数や変数やクラスなどと一緒に(合成して)使うことがより容易になったと考えられます。ほぼHTMLを許すJSXのような拡張は行わなくとも、Reactに影響を受けたであろうFlutterやSwiftUIはUIを表すオブジェクトを.dartや.swift上のコードに書くため、VueのテンプレートよりReactに似ていると言えます。
このアイデアも決して新しいわけではなく、ScalaはXMLリテラルが扱え、XMLを直にScalaのコードとして書けます。つまり、今まで別だと考えていたHTMLとJavaScriptを一貫した概念"式"と扱えるようにすることでより自由に扱え分割統治しやすくなると思います。
パッケージ, パッケージマネージャー
いまは言語とセットのようにライプラリを管理するためのパッケージマネージャーがあります。言語によって呼び方の違いはあれどパッケージの分割された単位の一つだと思います。それら組み合わせたり管理するためにパッケージマネージャーがあるというふうに捉えられると思います。
Denoのimport
Node.jsの開発者のRyanが開発するDenoはまた違ったライブラリの取り込み方・合成の仕方だと思います。以下のようにURLでインポートができて、現在のところパッケージマネージャーを使わない方法を使っていると思います。実際に.../server.ts
をリンクをブラウザで開くとすぐにソースを確認することもできます。
import { serve } from "https://deno.land/std@v0.5/http/server.ts";
一瞬 <script src="">
時代のようにも見えるかもしれません。ですがDenoのimportと<script>
とは異なり、暗黙にグローバルに変数がのさばらなく、明示的に名前空間が作れます。そしてHTMLではなくJavaScriptの中で完結してライブラリを取り込み・合成ができます。
Ammonite (Scala)
パッケージマネージャを使わずという点では以下のAmmonite(Scala)も似ています。
import $ivy.`org.scalaz::scalaz-core:7.2.27`, scalaz._, Scalaz._
(Option(1) |@| Option(2))(_ + _)
// => Some(3)
ライブラリを取り込む手法・合成は、DenoやAmmoniteのようにパッケージマネージャを使わない方法もあります。まだこれらは発展途上かもしれませんが、一つのファイルでライブラリの依存も完結するシンプルさとインストールしない手軽さやポータビリティが今後より評価されるようになるかもしれません。パッケージマネージャーあるなし含め合成し、パッケージ(言語によってはモジュール)を再利用するのも分割統治と言えます。
GitHub Actions - 継続的インテグレーション(CI)
ソフトウェア開発に不可欠になっているCIについてです。
GitHubを中心とするOSSではTravis CIやCircleCIが良く使われていると思います。GitHub自体も類似の機能を持つGitHub Actionsを提供していて、このGitHub Actionsが分割統治の視点で個人的に注目してます。
それが、小さい部品に分かれたアクションを集めて設定ファイルを書く点です。たとえば、- uses: actions/checkout@v2
でチェックアウトや- uses: actions/setup-node@v1
でNode.jsのセットアップなどのアクションを並べていきます。"actions/..."はGitHub公式ですが、例えば- uses: peaceiris/actions-gh-pages@v3
でGitHub Pagesへのデプロイのようにサードパーティ製のアクションを使うこともできます。こういったアクションを作ることもできます。つまりCIで使いたい部品(アクション)を再利用したり切り出し可能です。Travis CI やCircleCI では調べた限りこういった機能はありませんでした。CircleCI Orbsという類似の仕組みがあることを@peaceiris 氏に教えていただきました。分割統治の視点でもGitHub Actionsは優れているのではないでしょうか。
その他色々
依存性の注入(DI)も分割統治でしょうし、分割統治の視点で見れば、アプリケーションごとを1つのDockerコンテナに分けて、それを合成するものがDocker ComposeであったりKubernetesだったりするのではないでしょうか。他のアプリケショーンと連携するためにREST API, GraphQLがあったり、他言語を直接使いたい場合は、発展途上かもしれませんがOracleのGraalVMを使うかもしれません。WebAssemblyが活発なので、複数言語間でいえばWASIの方が今後より発展するかもしれません。「細く分けて、組み合わせる」という分割統治はまだまだ色々なところで活躍しているでしょう。脳内で補完していただければと思います。
合成した時の副作用を減らす技術や型
小さく分けたものを組み合わせるにあたって、その周辺に様々な技術が現れてくるでしょう。例えば、副作用を持たない純粋関数やイミュータブル(不変)のデータなどです。関数を呼び出したり適用したりする時に、渡した引数を変更されたり、他の環境を変えたりなるべく驚きを減らすことは、組み合わせることが増えれば防ぎたくなるのは自然なことに思えます。これは"関数"をFaaSやコマンドなどに読み替えても同じく言えると思います。
型の必要性
近年、Python, PHP, Rubyといった軽量言語にも静的な型を導入される動きや、JavaScriptに対する静的型付けされたTypeScriptのような存在もあります。C言語のような低レイヤーの言語でのメモリサイズを決める型はマシンのためだったものが、堅牢なプログラム書く人のための型になっていると思います。分割統治の視点で見ると、実行時の型の誤りを極力排除して、確実に合成できることを保障しようとしているとも取れると思います。
CDプレイヤーにDVDをいれて故障が多かったのも"型"が違うものだったのに同じだったからで、3DSのソフトのように突起があれば防げたかもしれません。
現代のエディタ/IDEであれば書いている段階で赤い波線でまたに誤りがあるものの教えてくれます。もしくはそれに準じる静的な解析技術が健全な合成に寄与すると思います。
「問題を分割する方法は、解を合成する方法に依存する」
分割する方法、合成する方法が違えど、いろんな場面で分割統治という技術が使われていることが分かりました。分割統治してプログラムを組み立てる時に役にたつと思う言葉を引用したいと思います。
「問題を分割する方法は、解を合成する方法に依存する」
Scala Matsuri 2018のがくぞ さんの「Why "Composability" Matters」からです。大元は「Why Functional Programming Matters」です。
これを見て、「問題の分割方法が合成方法に依存する」ということを何度か捉え直してみると、合成方法がイマイチなら解の分割しても上手くいかないことがあるとも解釈できるのではと思います。複雑で長いコードを目の前にすると分割することに目が行きがちになることもあり、その後の合成をどのような方法を考えることこそが大事だということに改めて気付かされます。
哲学的なこと: 最終的にどこへ向かうのか?
では、分割統治は「最終的にどこへ向かうのか?」「どこに収束するのか?」と考えるときに思うことです。ここから技術的なことからは離れて、飛躍して少しばかり哲学的な感じです。
- UIは宣言的に記述されコンポーネントとして組みわせて再利用でき
- 小さいコマンドをパイプで組み合わせて、
- 非同期処理を表すPromiseというデータとしてロジックを分けawaitで合成し、
- ...
インターネットもコンピュータやその他装置のつながりで、そのコンピュータの中の論理回路もANDゲートやORゲートのような論理ゲートの組み合わせで、その論理ゲートの中の...
社会も我々の人体も小さいものの組み合わせで複雑なものを作っているのではないかと思うのです。いろんな臓器で成り立っていて、それらもまた小さな細胞からなりたっていて... 中を開けたらまた小宇宙が広がっているような。
ちょうどRubyの作者Matz氏の主張と似ているかもしれません。
Rubyの外観はシンプルです。けれど、内側はとても複雑なのです。 それはちょうど私たちの身体と同じようなものです。
(https://www.ruby-lang.org/ja/about/)
それは"美"なのではないか?
最終的に向かう先は"美"なのではないかと思うのです。
イタリアの画家のカルロッティがこのように"美"を定義しています。
すべての部位が完璧に調和して、手を加える必要がまったくないこと
(映画 『NEXT -ネクスト-』より)
これはUnix哲学、パイプの発明者のマキルロイの要約にも通じるところがあるではないでしょうか。
一つのことを行い、またそれをうまくやるプログラムを書け。
協調して動くプログラムを書け。
ガンカーズの"小さいものは美しい"とも通じるのではないでしょうか。
UNIX哲学 - Wikipedia
物事行き詰めると言語化できない領域に行くことはよくあると思います。すべての細かく分かれたパーツがバランスを保っていて合成され、それ以上足す必要がないシンプルな状態”美”に向かっているというのは、いままでコマンドとパイプや、Promiseとasync/awaitなどを見ても調和しこれ以上足す必要がないくらいシンプルに見えます。そういう完全には厳格に言葉で表せないもの"美"に我々は向かっているのかもしれません。
おわりに: トレードオフ
最後は飛躍しましたが、色々な技術で分割統治がある程度共通して使われていることを例をふまえて集めました。現実的にいまある全てを分けて合成しようと思っても、パフォーマンス、セキュリティ、後方互換性、"大人の事情"...など分割統治することとのトレードオフがあると思います。
例えば、HTTP/3で使用されるQUICはTCPが担っていた通信とTLSが担っていた暗号化が別れずに組み合わさったような新しいUDP上のプロトコルです。その分割されない理由は、パフォーマンス的にラウンドトリップの削減をしたり、ヘッダの暗号化でセキュリティ的なことやプロトコルの硬直化を防ぐ役割があるようです。(参考: QUICの中身が分からないから仕様読んでみた | κeenのHappy Hacκing Blog, 硬直化 - HTTP/3 explained)
この例以外でもいろんなトレードオフがあると考えられますし、制作途中のまだ全体像が見えてない段階で分割すると上手く行きづらい個人的な経験則もあります。
この記事では分割統治にフォーカスを当てました。他の視点で共通して使われている技術の記事が出ることに期待にしつつ終わりにします。