しかしながら、一つの言語を習得することは、一つの文化を学ぶことを意味し、生半可な覚悟では済まない。その根底にある動機はプログラミングを楽しむこと、そして、最終的に自分自身の言語を見つけ出すことになろうか。自然言語が人間魂の投影であるならば、プログラミング言語は技術魂の投影とでもしておこうか...
"Ruby, Io, Prolog, Scala, Erlang, Clojure, and Haskell"
ここでは、Java, C++, Python などの人気言語が意図的に外されるが、むしろ惚れっぽい天の邪鬼にはありがたい。商用プロジェクトにどっぷり浸かっていると、新たな言語に触れる機会は著しく限られ、新たな概念を持ち込もうとすれば、激しい抵抗にもあう。おまけに、プログラミング言語の評価では、勝者と敗者に振り分けて語られる風潮がある。人間社会がこれだけ多様化しているにもかかわらず、多数決が正義とされるのは、この分野に限った話ではない。ドメイン固有とは、多様化した人間社会における居場所を求めることに他ならない。真に汎用言語というものは、それが長所であると同時に短所となりうる。これといったキラーアプリケーションがないことを意味するのだから。おそらく相対的な認識能力しか発揮できない知的生命体が、万能な言語を編み出すことなど不可能であろう。
また、Ruby が比較対象とされるところも、個人的に馴染みやすい書となっている。各言語とも三日間の講習コースとして計画され、21日 + αというわけだが、読破するのに1ヶ月以上かかってしまった。
本書の不思議なところは、掲載されるコードをそのまま入力し、同じ結果が得られるかどうかを確かめるだけで、何かに挑戦してみたいという気分にさせてくれることだ。新たなスキルを身につけようとする時、最初に良き教師に出会えれば幸運である。ただ、教師は人とは限るまい。書籍も素晴らしい教師となりうる。とはいえ、学問の道で最短距離を駆け抜けることは至難の業。学ぶ側から高みに昇っていき、良き教師、良き書籍を嗅ぎ分ける眼力を養うしかない。そのために、失敗を受け容れるしかなさそうだ...
「プログラミングを学ぶことは、泳ぎを覚えるのに似ている。いくら理論を学んでも、プールに飛び込んで、息をしようと喘ぎながら水の中で手足をばたつかせる経験には代えられない。最初水中に沈んだときはパニック状態になるが、水面にひょいと顔を出して空気を飲み込むと、うれしい気分になる。自分は泳げる!と思えるからだ。 ...
禅の指導者は、数学が出来るようになりたければラテン語を勉強せよと言うだろう。プログラミングでも同じだ。オブジェクト指向プログラミングの本質を深く理解するには、論理プログラミングや関数型プログラミングを勉強する必要がある。関数型プログラミングに上達したければ、アセンブラを勉強する必要がある。」
... Erlang の作者 Joe Armstrong
1. 7つの言語とその人物像
言語の性格を語るのに、映画とその登場人物を重ねながら、多分に文学的な要素を交える趣向(酒肴)もなかなか...
Ruby は、メリー・ポピンズ。
乳母という仕事から考え得る限り情熱を引き出すことで、家事を効率的なものにする。スプーン一杯の砂糖があるだけで、苦い薬も飲めるのよ!まさにシュガーシンタックスこそが抽象度を高める。コレクション操作におけるシンプルなループ構文や直感的な範囲指定など豊富な API によって...
「オブジェクト指向の設計哲学における重要な教義は、実装ではなく、インタフェースに合わせてコーディングするというものだ。ダックタイピングを導入すると、ほとんど何の作法もなしに、この設計哲学を簡単に適用できる。」
Io は、フェリス・ビューラー。
何でも一度はやってみるタイプで、若くて、ずる賢く、分かりやすいが、まったく予測不能ときた。柔軟性が高いがゆえに、少しおかしな動きをする。
「問題は何をするかではない、何をしないかだ!」
商業的には成功していない言語だが、コルーチン、アクター、フューチャに基づく並行性構文を絶賛している。
Prolog は、レインマンのレイモンド。
要領の悪い自閉症だが、型にはまると超人的な能力を発揮する。確かに古い言語で、洗練されているとは言い難いが、条件の厳しい専門性において強力。
「この言語は驚くほど利口な場合もあれば、それと同じくらいイライラさせられることもある。素晴らしい答えが得られるのは、質問の仕方が分かっている場合だけだ。」
Scala は、エドワード・シザーハンズ。
はさみを持つ人間と機械のあいのこは、まさにオブジェクト指向型と関数型の橋渡しをするハイブリッド。世間から非難される幻想的なキャラクターは、時にはぎこちなく振る舞い、時には驚くほどの能力を見せる。厳密に言えば、純粋な関数型ではない。C++ が純粋なオブジェクト指向型ではないように。
Erlang は、マトリックスのエージェント・スミス。
仮想世界マトリックスの人工知能プログラムであるスミスは、姿形を自由に変え、複数の場所に同時に存在できる。この言語は並行性における分散処理と耐障害性という特徴を有し、そこには「クラッシュさせろ!」という哲学があるという。普通とは真逆の発想だが、信頼性に自信がある裏返しか。それも、副作用なしという前提で初めて機能する哲学だ。
「Erlang のような謎めいた言語はほとんど見かけない。Erlangは、難しいことを簡単にし、簡単なことを難しくする並行処理指向言語だ。」
Clojure は、スターウォーズのヨーダ。
賢明なカンフーの達人、丘の上に立つ預言者、謎めいたジェダイの指導者。見た目は地味ながら非常に高い能力を秘め、ありとあらゆる生命体に遍在するエネルギー、すなわちフォースを自在に操る。Lisp 方言の一つで、マクロや高階構文を使いこなすには、フォースを見抜く修行が必要というわけだ。おまけに、話し方は語順が逆で理解しづらく、前置記法ときた。
Haskell は、スタートレックのスポック。
論理と真理を崇拝し、世代を超えて愛されるひたむきな純粋さ。
「Haskell は、関数型プログラミングにおける多くの純粋主義者にとって、純粋と自由の象徴である。その機能は豊富で強力だが、それには代償が伴う。Haskell では、関数型プログラミングを少しかじってみるといった安易な考えは許されない。とにかく関数型プログラミングという料理を余すことなく完食することを強制される。」
2. 関数型とオブジェクト指向型の相互運用
プログラミング言語の進化が、コンピュータ科学の中で比較的ゆるやかなのは、頭の固いアル中ハイマーとってありがたい。歴史を振り返れば、二十年くらいのサイクルで大きなパラダイム変革が生じている。現在では、並行性と信頼性が中心的な話題であろうか。
ここでは、Ruby と Prolog を除く5つの言語が並行性モデルを採用している。並行性におけるオブジェクト指向型言語の問題は、副作用から生じる可変状態を許すことにある。複数のスレッドやプロセスが同時に発生すると、この可変状態の管理が極端に複雑になる。
とはいえ、状態を持つことはモジュールの抽象度を高める効果があり、オブジェクト指向では根幹をなす概念の一つである。
一方、純粋な関数型は、同じ関数を何度呼び出しても、同じ入力に対して必ず同じ答えを返す。まさに数学の関数概念だ。副作用なし!これを保証するだけで、競合状態における複雑さが解消される。このタイプには、Scale, Erlang, Clojure, Haskell が属す。
しかしながら、オブジェクト指向との関わり方では、大きく戦略が違うようだ。関数型とオブジェクト指向型の共存を目指すものもあれば、オブジェクト指向と完全に決別するものもあり、あるいは、現実的に共存を認めながら最終的に決別しようというものもある。
Scala のアプローチは共存方針で、強い関数型の性質を用いながらオブジェクト指向を混入している。
Clojure のアプローチは互換性重視だが、「OOPは基本的に一部欠陥がある」という考え方が根底にある。JVM上で構築され、Javaオブジェクトを直接利用できるメリットがあるが、Javaとの相互運用性の方が重視され、Clojure という言語自体を拡張することが目的ではないようだ。
Erlang と Haskell は基本的にスタンドアロン言語で、オブジェクト指向を一切許容しない。
ちなみに、Ruby はオブジェクト指向型でありながらコードブロックによって関数型の概念を少し導入しているものの、これらの並行性からは程遠いようである。
3. 並行性構文のコンポーネント
本書は、並行性構文で鍵となる三つのコンポーネントを挙げている。それは、コルーチン、アクター、フューチャ。その基盤を成すのがコルーチンで、プロセスの実行を自由に停止し、再開できる機構だという。複数の入口と出口を持つ関数のようなものか。
Java や Cベースの言語は、プリエンプティブ・マルチタスクの概念が用いられる。この並行性概念に、変更可能な状態を持つオブジェクトが組み合わさると、予測が極端に難しくなる。
対して、コルーチンは従来方式とは異なり、協調的マルチタスク(ノンプリエンプティブ・マルチタスク)に不可欠な機構で、非同期でメソッドが呼び出せるという。
アクターは、メッセージの送信、メッセージの処理、他のアクターの生成を行う並行性プリミティブで、複数のメッセージを同時に受信してキューに登録し、その内容をコルーチンで処理していく。
メッセージは、sender, target, arguments の三つのコンポーネントからなり、リフレクションを実行するためのメソッドが多数用意されている。尚、アクターは、理論上スレッドより有利だという。
フューチャは、非同期のメッセージを呼び出しによって返される結果オブジェクトのこと。他からの問い合わせは、結果値が使用可能になるまでブロックされるという。
本書は、こうした並行性概念を、Io の事例で味あわせてくれる。というのも、シンタックスが、コルーチン、アクター、フューチャの実装にマッチしているからだという。
Io のシンタックスは、なかなかの感動モノ!母音二文字の名が象徴するように、単純でローレベルに設計されている。それはメッセージを単純にチェーン連結したもので、クラスとオブジェクトを区別しない。メッセージそのものがオブジェクトで、ルートオブジェクトからひたすら実体を複製していく(クローニング)。このクローンがプロトタイプと呼ばれ、プロトタイプベースの言語というわけだ。非同期のメッセージを送信すると、アクターそのものがオブジェクトになる。しかも、Io のフューチャは、デッドロックを自動的に検出する機能を備えているという。シンタックスは、Lisp のように単純で、セマンティクスは強力で柔軟。
しかしながら、Ruby のように動的な性質を実現した代償として、単一スレッドではパフォーマンスが悪い傾向にあるという。乱暴に言えば、すべての知識や経験は関連付けで成り立っている。DNAの螺旋構造もまたチェーン連結のごとく増殖していく。プラトンが唱えたイデアのような精神の雛形は、いわば理想像。理想なんてものは暇人の考えることで、実体はすべて実践の関連付けのみで構築されることを暗示しているのだろうか。
尚、もともとは、Steve Dekorte がインタープリタの動作を理解するための練習で書いた、いわば趣味レベルの言語だそうな。
アクターの機構は、Scala や Erlang も継承し、パターンマッチングを用いて着信メッセージを照合し、条件分岐させて実行するという。その高度なパターンマッチングの方法としてユニフィケーションを紹介してくれる。ユニフィケーションは、パターンマッチングの親戚のようなもので、実は、Prolog の備える機構だという。
さらに、Erlang とその仮想マシンは堅牢なモニタリング機能を備えるため、異常な兆候を察知した時点で、通知したりプロセスを再起動したりできるという。
いまや、スレッドやプロセスを開始してセマフォを持つような仕組みでは不十分で、アクターやフューチャなどの高機能な並行性構文の必要性が高まっているようである。
4. ソフトウェアトランザクションメモリ(STM)
並行処理にとって可変状態は、オブジェクト指向に潜む悪の根源。これに対処するため、Io と Scala はアクターベースのモデルを用いて、変更不能な構文を用意している。
Erlang は軽量プロセスを用いたアクターと、効果的なモニタリングと通信を可能にする仮想マシンによって、比類のない信頼性を実現している。
一方、Clojure のアプローチは他とは異なり、STM を使う。
ところで、データベースではトランザクションによってデータの整合性を保証する、二つの制御方法を見かける。一つはロックで、競合するトランザクションが同時に同じ行にアクセスするのを防ぐ。二つはバージョニングで、各トランザクションが競合データのコピーを保持できるように複数のバージョンを用意して、互いの干渉を防ぐ。Java などでは、競合スレッドから保護するためにロックを用いるため、こちらの方が馴染みがあろうか。
しかし、ロックは並行制御のための作業が負担となり、なかなか耐え難いものがある。
対して、STM の戦略では、複数のバージョンを用いて一貫性と整合性を維持するという。Clojure で STM を用いるのは、スレッド間の一貫性を保ちながら可変状態を実装できるということらしい。STM は、共有リソースに対する分散アクセスをトランザクションでくるむ方法で、参照状態を変更する時はトランザクションのスコープ内で行う必要がある。その意味では、Clojure はデータベース言語にも見えてくる。Clojure では、dosync関数でくるみ、複数のスレッドやプロセス間での整合性を維持するという。Lisp方言の Clojure は、こうしたアプローチに理想的な言語だという。Lispは、マルチパラダイム言語だから。
尚、STM は比較的新しい概念で、人気のある言語でも採用され始めているようである。
5. リスト内包表記とモナド
Haskell は、本書に登場する唯一の純粋な関数型言語である。Scale、Erlang、Clojure も関数型ではあるが、命令型の概念を少し取り入れている。Haskell にそのような逃げ道はない。純粋とは、同じ関数に同じ入力を与えれば必ず同じ結果が得られるという意味で、けして副作用を許さず、書き換えることのできる状態がどこにも存在しないってことだ。
そのために、I/O処理やエラー処理といった簡単な処理が難しくなる。その対処法として、Haskell では「モナド」という概念を用いて状態を保持する方法を紹介してくれる。
ところで、リスト内包表記は、Python など他の言語にも広まりつつある表記法ではあるのだけど、極めて数学的な表現であることを改めて実感させてくれる。つまり、カリー化や高階関数を表記する上で効果的だということを。カリー化とは複数の引数をとる関数を渡す表記法で、高階関数とは戻り値に関数を返したり、引数に関数を受け取る関数のこと。どちらもラムダ計算のような理論モデルに欠かせない概念だ。この表記法によって、再帰処理がリスト処理によって強力になる。ちなみに、Ruby は高階関数の代わりにコードブロックを使う。
リスト内包表記は、複数の概念をまとめて一つの強力な構文を作り上げる。これを適合しやすい演算は、フィルタ、マップ、直積といったところであろうか。
さて、モナドの方だが、それは精神融合のようなものか。ライプニッツ風モナドロジーが頭に浮かぶ。それは、副作用のない関数群という安定性に着目し、型の異なる関数を融合するという考え方である。モナドの構成要素は、以下の三つ。
- コンテナになるものの型を変数に取る型構成子。コンテナとして使えるものは、変数やリストなど値を保持できるもので、ここに関数を収める。
- 関数をラップしてコンテナに格納する return 関数。
- 関数をラップするバインド関数(>>=)。関数を数珠つなぎにする。
- ラップした値はそのまま関数に渡すことができる。
return x >>= f = f x - 情報を失うことなく値をアンラップおよびラップできる。
monad >>= return = monad - バインドをネストした場合と、それらをシーケンシャルに呼び出した場合とで結果が同じ。
(m >>= f) >>= g = m >>= (\x -> f x >>= g)
しかしながら、モナドが柔軟性が高いとはいえ、高度な知識が必要という意味では融通が利かない。
6. おまけ... 強い型付けと静的な型付けの混同
強い型付けと静的な型付けの混同しないように!と注意を促される。実は、おいらもよく見失う。
大雑把に言うと、強い型付けは、2つの型に互換性があるかどうかを検出し、互換性がなければエラーを投げるか、型の強制変換を行うという意味。
本書は、表面的には、Java と Ruby はどちらも強く型付けされているとしている。ただし、この見解は単純化しすぎると断りつつ。
一方、アセンブラや C言語は、弱く型付けされている。整数値か文字列かなどは、コンパイラは意識しない。だから、書き手が意識することになる。
静的な型付けと動的な型付けは、また別の問題である。静的型付けでは、型の構造体に基いてポリモーフィズムが行われるという。つまり、遺伝的な青写真によってアヒルかどうかを判定するのが静的型付けで、鳴き声や歩き方によってアヒルかどうかを判定するのが動的型付けであると。
静的型付けは、コンパイラとツールがコードに関する情報を保持し、エラーを捕捉したり、コードを強調表示したり、リファクタリングしたりできる点では有利だが、そのために作業量が増え、制約が生まれる。このトレードオフをどのように感じるかは、プログラマの経験やセンス、あるいは好みや設計分野にも左右されるだろう。