Leapcell:次世代のサーバレスWebホスティングプラットフォーム
関数型プログラミングの詳細説明
あなたは関数型プログラミングを耳にしたことがあり、場合によってはすでに何らかの期間使っているかもしれません。しかし、それが何であるかを明確に説明できますか?
オンラインで検索すると、簡単に多くの回答を見つけることができます:
- オブジェクト指向プログラミングや手続き型プログラミングと並行するプログラミングパラダイムです。
- 最も重要な特徴は、関数が一等公民であることです。
- 計算プロセスを再利用可能な関数に分解することを強調します。典型的な例は、
map
メソッドとreduce
メソッドから構成されるMapReduceアルゴリズムです。 - 副作用のない純粋な関数のみが適格な関数です。
上記のすべての記述は正しいですが、不十分です。もっと深い質問に答えていません:なぜこのようにするのでしょうか? これがこの記事が答えようとする目的です。私はあなたが関数型プログラミングを理解し、最も簡単な言葉でその基本的な構文を学ぶのを助けます。
I. 圏論
関数型プログラミングの起源は、圏論と呼ばれる数学の一分野です。関数型プログラミングを理解する鍵は、圏論を理解することです。これは複雑な数学で、世界のすべての概念体系を「圏」に抽象化できると考えています。
1.1 圏の概念
圏とは何でしょうか? Wikipediaの一言での定義は以下の通りです:「数学において、圏は「矢」で結ばれた「対象」から構成される代数構造です。」
つまり、相互に一定の関係を持つ概念、物事、オブジェクトなどはすべて「圏」を形成します。それらの間の関係を見つけることができれば、何にでも「圏」を定義することができます。
例えば、様々な点とそれらの間の矢は圏を形成します。矢は圏のメンバー間の関係を表し、正式な名前は「射」です。圏論では、同じ圏のすべてのメンバーは異なる状態の「変換」であると考えられています。「射」を通じて、あるメンバーを別のメンバーに変換することができます。
1.2 数学的モデル
「圏」は特定の変換関係を満たすすべてのオブジェクトであるため、その数学的モデルは以下のように要約できます:
- すべてのメンバーが集合を形成します。
- 変換関係は関数です。
つまり、圏論は集合論のより高度な抽象化です。簡単に理解すると「集合 + 関数」です。理論的には、関数を通じて、あるメンバーから圏の他のすべてのメンバーを計算することができます。
1.3 圏とコンテナ
「圏」を、以下の2つのものを含むコンテナとして想像することができます:
- 値。
- 値の変換関係、つまり関数。
以下は、単純な圏を定義するコードです:
class Category {
constructor(val) {
this.val = val;
}
addOne(x) {
return x + 1;
}
}
上記のコードでは、Category
はクラスであり、またコンテナでもあり、値 (this.val
) と変換関係 (addOne
) を含んでいます。ここでの圏は、互いに1つの差があるすべての数であることに気付いたかもしれません。
なお、この記事の以下の部分で「コンテナ」と言及される場合、すべて「圏」を指します。
1.4 圏論と関数型プログラミングの関係
圏論は関数を使って圏の間の関係を表現します。圏論の発展に伴い、一連の関数操作方法が開発されました。この一連の方法は当初は数学的な操作にのみ使用されていました。後に誰かがコンピュータ上で実装し、今日の「関数型プログラミング」となりました。
本質的に、関数型プログラミングは単なる圏論の操作方法です。数学的論理、微積分、行列式と同じ種類のものです。すべて数学的な方法です。たまたまプログラムを書くために使用できるだけです。
では、なぜ関数型プログラミングでは関数が純粋で副作用のないものである必要があるのか理解できましたか? それは数学的な操作であり、元々の目的は他のことをせずに値を評価することだからです。そうでなければ、関数の操作規則を満たすことができません。
要するに、関数型プログラミングにおいて、関数はパイプのようなものです。一方の端に値が入り、もう一方の端から新しい値が出てきて、他の影響はありません。
II. 関数合成とカリー化
関数型プログラミングには2つの最も基本的な操作があります:合成とカリー化。
2.1 関数合成
ある値が別の値になるために複数の関数を通過する必要がある場合、すべての中間ステップを1つの関数に結合することができます。これを「関数合成」と呼びます。
例えば、X
とY
の間の変換関係が関数f
で、Y
とZ
の間の変換関係が関数g
の場合、X
とZ
の間の関係はg
とf
の合成関数g ∘ f
です。
以下はコード実装(JavaScript言語を使用)です。なお、この記事のすべての例コードは簡略化されています。完全なデモについては、「参照リンク」のセクションを参照してください。2つの関数を合成する簡単なコードは以下の通りです:
const compose = function (f, g) {
return function (x) {
return f(g(x));
};
}
関数合成はまた、結合則を満たさなければなりません:
compose(f, compose(g, h))
// は次と等価
compose(compose(f, g), h)
// は次と等価
compose(f, g, h)
合成はまた、関数が純粋である必要がある理由の1つです。不純な関数が他の関数とどのように合成されるでしょうか? 様々な合成の後、期待される動作を達成することをどのように保証することができるでしょうか?
前述のように、関数はデータのパイプのようなものです。そして、関数合成はこれらのパイプを接続することで、データが一度に複数のパイプを通過できるようにするものです。
2.2 カリー化
f(x)
とg(x)
をf(g(x))
に合成するには、f
とg
の両方が1つのパラメータのみを受け取ることができるという隠れた前提があります。複数のパラメータを受け取ることができる場合、例えばf(x, y)
とg(a, b, c)
の場合、関数合成は非常に面倒になります。
ここでカリー化が登場します。いわゆる「カリー化」とは、多パラメータ関数を単一パラメータ関数に変換することです。
// カリー化前
function add(x, y) {
return x + y;
}
add(1, 2) // 3
// カリー化後
function addX(y) {
return function (x) {
return x + y;
};
}
addX(2)(1) // 3
カリー化により、すべての関数が1つのパラメータのみを受け取ることを保証することができます。以下の内容で特に断りがない限り、関数は1つのパラメータ、つまり処理対象の値のみを受け取るものと仮定します。
III. ファンクター
関数は同じ圏内の値の変換にのみ使用されるのではなく、1つの圏を別の圏に変換するためにも使用できます。これにはファンクターが関係してきます。
3.1 ファンクターの概念
ファンクターは関数型プログラミングにおける最も重要なデータ型であり、操作と機能の基本単位でもあります。
まず、それは圏であり、つまり値と変換関係を含むコンテナです。特別な点は、その変換関係が順番に各値に適用され、現在のコンテナを別のコンテナに変換することができるということです。
例えば、左側の円は人名の圏を表すファンクターです。外部の関数f
が渡されると、右側の朝食の圏に変換されます。
より一般的には、関数f
は値の変換(a
からb
)を完了します。それをファンクターに渡すことで、圏の変換(Fa
からFb
)を達成することができます。
3.2 ファンクターのコード実装
map
メソッドを持つ任意のデータ構造は、ファンクターの実装と見なすことができます。
class Functor {
constructor(val) {
this.val = val;
}
map(f) {
return new Functor(f(this.val));
}
}
上記のコードでは、Functor
はファンクターです。そのmap
メソッドは関数f
をパラメータとして受け取り、次に新しいファンクターを返します。その中に含まれる値はf
によって処理された値(f(this.val)
)です。
一般的に、ファンクターの特徴は、コンテナがmap
メソッドを持っていることです。このメソッドはコンテナ内の各値を別のコンテナにマッピングします。
以下はいくつかの使用例です:
(new Functor(2)).map(function (two) {
return two + 2;
});
// Functor(4)
(new Functor('flamethrowers')).map(function(s) {
return s.toUpperCase();
});
// Functor('FLAMETHROWERS')
(new Functor('bombs')).map(_.concat(' away')).map(_.prop('length'));
// Functor(10)
上記の例は、関数型プログラミングにおける操作はすべてファンクターを通じて行われることを示しています。つまり、操作は値に直接行われるのではなく、これらの値のコンテナであるファンクターに対して行われます。ファンクター自体には外部インターフェイス(map
メソッド)があり、様々な関数は演算子です。それらはインターフェイスを通じてコンテナに接続され、コンテナ内の値を変換させます。
したがって、関数型プログラミングを学ぶことは、実際にはファンクターの様々な操作を学ぶことです。操作方法をファンクターにカプセル化できるため、さまざまな種類のファンクターが派生しています。操作の種類だけだけの種類のファンクターが存在します。関数型プログラミングは、さまざまなファンクターの応用によって実際の問題を解決することになります。
IV. of
メソッド
先ほど新しいファンクターを生成する際に new
コマンドを使用したことに気付いたかもしれません。これは関数型プログラミングらしくありません。なぜなら、new
コマンドはオブジェクト指向プログラミングの象徴だからです。
関数型プログラミングでは一般的に、ファンクターには新しいコンテナを生成する of
メソッドがあると考えられています。
以下は new
を of
メソッドに置き換えたものです:
Functor.of = function(val) {
return new Functor(val);
};
すると、先ほどの例は以下のように変更できます:
Functor.of(2).map(function (two) {
return two + 2;
});
// Functor(4)
こちらの方が関数型プログラミングらしくなります。
V. Maybe ファンクター
ファンクターは様々な関数を受け取り、コンテナ内の値を処理します。ここで問題が生じます。コンテナ内の値が null 値(例えば null
)である可能性があり、外部の関数が null 値を処理するメカニズムを持っていない場合があります。null 値が渡されると、エラーが発生する可能性が高いです。
Functor.of(null).map(function (s) {
return s.toUpperCase();
});
// TypeError
上記のコードでは、ファンクター内の値が null
で、小文字を大文字に変換する際にエラーが発生します。
Maybe ファンクターはこのような問題を解決するために設計されています。簡単に言えば、その map
メソッドには null 値チェックが含まれています。
class Maybe extends Functor {
map(f) {
return this.val? Maybe.of(f(this.val)) : Maybe.of(null);
}
}
Maybe ファンクターを使用すると、null 値の処理でエラーが発生することはありません。
Maybe.of(null).map(function (s) {
return s.toUpperCase();
});
// Maybe(null)
VI. Either ファンクター
条件分岐操作 if...else
は最も一般的な操作の1つです。関数型プログラミングでは、Either ファンクターを使用してこれを表現します。
Either ファンクターは内部に2つの値を持ちます:左側の値(Left
)と右側の値(Right
)。右側の値は通常の状況で使用される値で、左側の値は右側の値が存在しない場合に使用されるデフォルト値です。
class Either extends Functor {
constructor(left, right) {
this.left = left;
this.right = right;
}
map(f) {
return this.right?
Either.of(this.left, f(this.right)) :
Either.of(f(this.left), this.right);
}
}
Either.of = function (left, right) {
return new Either(left, right);
};
以下は使用例です:
var addOne = function (x) {
return x + 1;
};
Either.of(5, 6).map(addOne);
// Either(5, 7);
Either.of(1, null).map(addOne);
// Either(2, null);
上記のコードでは、右側の値が値を持っている場合は右側の値が使用され、そうでない場合は左側の値が使用されます。このように、Either ファンクターは条件分岐操作を表現します。
Either ファンクターの一般的な用途の1つは、デフォルト値を提供することです。以下は例です:
Either
.of({address: 'xxx'}, currentUser.address)
.map(updateField);
上記のコードでは、ユーザーが住所を提供していない場合、Either ファンクターは左側の値にあるデフォルトの住所を使用します。
Either ファンクターのもう1つの用途は、try...catch
を置き換えることで、左側の値をエラーを表すものとして使用することです。
function parseJSON(json) {
try {
return Either.of(null, JSON.parse(json));
} catch (e: Error) {
return Either.of(e, null);
}
}
上記のコードでは、左側の値が空であればエラーがないことを意味し、そうでない場合は左側の値にエラーオブジェクト e
が含まれます。一般的に、エラーを引き起こす可能性のあるすべての操作は、Either ファンクターを返すことができます。
VII. Ap ファンクター
ファンクターに含まれる値が関数であることも十分にあり得ます。あるファンクターの値が数値で、別のファンクターの値が関数である状況を想像できます。
function addTwo(x) {
return x + 2;
}
const A = Functor.of(2);
const B = Functor.of(addTwo)
上記のコードでは、ファンクター A
内の値は 2
で、ファンクター B
内の値は関数 addTwo
です。
時々、ファンクター B
内の関数がファンクター A
内の値を使って計算できるようにしたいと思うことがあります。ここで Ap ファンクターが登場します。
ap
は「applicative」の略です。ap
メソッドを展開する任意のファンクターは Ap ファンクターです。
class Ap extends Functor {
ap(F) {
return Ap.of(this.val(F.val));
}
}
ap
メソッドのパラメータは関数ではなく、別のファンクターであることに注意してください。
したがって、先ほどの例は以下のように書けます:
Ap.of(addTwo).ap(Functor.of(2))
// Ap(4)
Ap ファンクターの重要性は、複数のパラメータを持つ関数に対して、複数のコンテナから値を取り出し、ファンクターの連鎖操作を実現できることにあります。
function add(x) {
return function (y) {
return x + y;
};
}
Ap.of(add).ap(Maybe.of(2)).ap(Maybe.of(3));
// Ap(5)
上記のコードでは、関数 add
はカリー化された形式で、合計2つのパラメータを必要とします。Ap ファンクターを通じて、2つのコンテナから値を取り出すことができます。別の書き方もあります:
Ap.of(add(2)).ap(Maybe.of(3));
VIII. モナド ファンクター
ファンクターは任意の値を含むことができるコンテナです。ファンクターが別のファンクターを含むことは完全に合法です。ただし、これによりネストされたファンクターが生成されます。
Maybe.of(
Maybe.of(
Maybe.of({name: 'Mulburry', number: 8402})
)
)
このファンクターは3つの Maybe
がネストされています。内部の値を取得するには、連続して3回 this.val
を取る必要があります。これはもちろん非常に不便です。そこでモナド ファンクターが登場しました。
モナド ファンクターの役割は、常に単層のファンクターを返すことです。それは flatMap
メソッドを持ち、このメソッドは map
メソッドと同じ機能を持ちます。唯一の違いは、ネストされたファンクターが生成された場合、その内部の値を抽出し、常に単層のコンテナを返し、ネストの状況が発生しないようにすることです。
class Monad extends Functor {
join() {
return this.val;
}
flatMap(f) {
return this.map(f).join();
}
}
上記のコードでは、関数 f
がファンクターを返す場合、this.map(f)
はネストされたファンクターを生成します。したがって、join
メソッドは flatMap
メソッドが常に単層のファンクターを返すことを保証します。これは、ネストされたファンクターが平坦化されることを意味します。
IX. I/O 操作
モナド ファンクターの重要な応用の1つは、I/O(入出力)操作の実装です。
I/O は不純な操作であり、通常の関数型プログラミングではこれを処理することができません。このとき、I/O 操作をモナド ファンクターとして書き、操作を完了する必要があります。
var fs = require('fs');
var readFile = function(filename) {
return new IO(function() {
return fs.readFileSync(filename, 'utf - 8');
});
};
var print = function(x) {
return new IO(function() {
console.log(x);
return x;
});
}
上記のコードでは、ファイルの読み取りと印刷自体は不純な操作ですが、readFile
と print
は純粋な関数です。なぜなら、これらは常に IO
ファンクターを返すからです。
IO
ファンクターが flatMap
メソッドを持つ Monad
である場合、以下のようにこれら2つの関数を呼び出すことができます:
readFile('./user.txt')
.flatMap(print)
驚くべきことに、上記のコードは不純な操作を完了しますが、flatMap
が IO
ファンクターを返すため、この式は純粋です。純粋な式を通じて副作用のある操作を完了することができます。これが Monad
の役割です。
返されるのは依然として IO
ファンクターであるため、連鎖操作を実現することができます。したがって、多くのライブラリでは、flatMap
メソッドは chain
と改名されています。
var tail = function(x) {
return new IO(function() {
return x[x.length - 1];
});
}
readFile('./user.txt')
.flatMap(tail)
.flatMap(print)
// 以下と同等
readFile('./user.txt')
.chain(tail)
.chain(print)
上記のコードは user.txt
ファイルを読み取り、その後最後の行を選択して出力します。
Leapcell:次世代のサーバレスWebホスティングプラットフォーム
最後に、サービスのデプロイに最適なプラットフォーム:Leapcell をおすすめします。
1. 多言語対応
- JavaScript、Python、Go、または Rust で開発できます。
2. 無料で無制限のプロジェクトをデプロイ
- 使用量に応じて課金 — リクエストがなければ料金は発生しません。
3.圧倒的なコスト効率
- 使った分だけ課金で、アイドル時の課金はありません。
- 例:25ドルで平均応答時間60msで694万回のリクエストをサポートできます。
4. ストリームライン化された開発者体験
- 直感的なUIで簡単にセットアップできます。
- 完全自動化されたCI/CDパイプラインとGitOps統合。
- アクション可能なインサイトを得るためのリアルタイムメトリクスとログ。
5. 簡単なスケーラビリティと高性能
- 高い同時実行数を簡単に処理するためのオートスケーリング。
- 運用に関するオーバーヘッドは一切ありません — 構築に集中できます。
LeapcellのTwitter:https://x.com/LeapcellHQ