第1章: 私たちは一体何をしようとしているのか
はじめに
こんにちは!フランクリン・フリスビー教授です。ご縁あって皆さんに関数型プログラミングを教えることになりました。私の経歴はさておき、皆さんについてお聞きしましょう。少なくともJavaScriptの基本を理解し、オブジェクト指向プログラミングの実務経験があり、実践的なプログラマとして活躍されていることでしょう。昆虫学の博士号は不要ですが、バグの発見と修正のスキルは必要です。
関数型プログラミングの前提知識は仮定しません(仮定すると痛い目を見るのは周知の事実です)。ただし、可変可能な内部状態や制御不能な副作用、無原則な設計による問題には遭遇した経験があることを期待しています。自己紹介はここまでにして、本題に入りましょう。
この章の目的は、関数型プログラミングの本質を体感していただくことです。次章以降を理解するためには、プログラムの関数型と呼ぶ要件を把握する必要があります。さもないと、単にオブジェクトを避けるために無秩序なコードを書くことになりかねません。嵐に見舞われた時に頼る羅針盤のように、明確な目標を定める必要があります。
一般的なプログラミング原則には、暗黙のガイドラインが存在します: DRY(Don't Repeat Yourself/重複排除)、YAGNI(You Ain't Gonna Need It/必要になる前に作るな)、疎結合高凝集、最小驚異原則、単一責任原則などです。
過去に私が学んできた全てのガイドラインを列挙しても意味はありません... 重要なのは、これらが関数型環境でも有効である点です。ただし、これらは最終目標への通過点に過ぎません。ここで理解していただきたいのは、私たちがキーボードを叩く際の真の意図、つまり関数型の理想郷(ザナドゥ)です。
短い遭遇
まず異常な例から見ていきましょう。カモメの群れをモデル化したアプリケーションです。群れが結合すると大きくなり、繁殖すると配偶相手の数だけ増加します。これは良質なオブジェクト指向コードではなく、現代の代入ベースアプローチの問題点を強調するための例です。注目してください:
class Flock {
constructor(n) {
this.seagulls = n;
}
conjoin(other) {
this.seagulls += other.seagulls;
return this;
}
breed(other) {
this.seagulls = this.seagulls * other.seagulls;
return this;
}
}
const flockA = new Flock(4);
const flockB = new Flock(2);
const flockC = new Flock(0);
const result = flockA
.conjoin(flockC)
.breed(flockB)
.conjoin(flockA.breed(flockB))
.seagulls;
// 32一体誰がこんな恐ろしいコードを書くのでしょうか?内部状態の変化を追跡するのは極めて困難です。さらに驚くべきことに、計算結果が不正です!正解は16であるべきなのに、flockAの値が永続的に変更されています。これはIT界の無法地帯!これは野生の算術です!
理解できなくても問題ありません(私も理解していません)。重要なのは、状態と可変値が把握困難であるという点です。この小さな例でさえもそれが言えます。
関数型アプローチで再挑戦しましょう:
const conjoin = (flockX, flockY) => flockX + flockY;
const breed = (flockX, flockY) => flockX * flockY;
const flockA = 4;
const flockB = 2;
const flockC = 0;
const result =
conjoin(breed(flockB, conjoin(flockA, flockC)), breed(flockA, flockB));
// 16今回は正解が得られました。コード量も大幅に削減。関数のネストが若干分かりにくい点はありますが(第5章で改善)、本質を深堀りしましょう。概念を明確化する利点があります。実はカスタム関数conjoinとbreedは、単なる加算と乗算の処理です。
これらの関数に特別な処理はありません。関数名をmultiplyとaddに改称することで真の姿を明らかにしましょう。
const add = (x, y) => x + y;
const multiply = (x, y) => x * y;
const flockA = 4;
const flockB = 2;
const flockC = 0;
const result =
add(multiply(flockB, add(flockA, flockC)), multiply(flockA, flockB));
// 16これによって古代からの知恵が蘇ります:
// associative
add(add(x, y), z) === add(x, add(y, z));
// commutative
add(x, y) === add(y, x);
// identity
add(x, 0) === x;
// distributive
multiply(x, add(y,z)) === add(multiply(x, y), multiply(x, z));その通り、この忠実な数学的性質が役に立つでしょう。即座に思い出せなくても心配無用です。私たちの多くは、これらの算術法則を学んでからかなりの時が経っています。この性質を使ってカモメプログラムを簡素化できるか見てみましょう。
// Original line
add(multiply(flockB, add(flockA, flockC)), multiply(flockA, flockB));
// Apply the identity property to remove the extra add
// (add(flockA, flockC) == flockA)
add(multiply(flockB, flockA), multiply(flockA, flockB));
// Apply distributive property to achieve our result
multiply(flockB, add(flockA, flockA));見事です!呼び出し関数以外にカスタムコードをほとんど書く必要がありません。完全性のためにaddとmultiplyの定義を記載していますが、実際には既存ライブラリの関数を使用すれば十分です。
「数学的な例を前面に出して都合の良いストローマン論を展開している」とか「現実のプログラムはこんなに単純ではなく、そのように推論できない」と思うかもしれません。加算と乗算は誰もが知っているので、数学の有用性が明確にわかるようにこの例を選びました。
心配無用。本書では圏論(カテゴリー理論)や集合論、ラムダ計算を織り交ぜつつ、現実世界の例題をカモメ例と同レベルの簡潔さで解説します。数学者である必要はありません。一般的なフレームワークを使うように自然に扱えます。
関数型スタイルで日常的なアプリケーションを開発可能だと知って驚くかもしれません。無法は犯罪者には都合が良いですが、本書では数学の法則を認め従う堅牢なプログラムを書きます。簡潔で理路整然とし、車輪の再発明をしないプログラムです。
全てが調和する理論を使いたいのです。具体的問題を汎用的で合成可能な要素で表現し、その特性を活用します。命令型プログラミングの「何でもあり」アプローチ(「命令型」の定義は後述)より規律が必要ですが、数学的枠組みで得られる成果は驚くべきものです。
関数型の北極星を垣間見ましたが、本格的な旅路に必要な具体的概念がまだ残っています。