機能的プログラミング:ゼロからヒーロー
Ethan Miller
Product Engineer · Leapcell

機能的プログラミングの詳細な説明
機能的プログラミングについて聞いたことがあるかもしれませんし、しばらく使ったこともあるかもしれません。しかし、それが何かを明確に説明できますか?
オンラインで検索すると、多くの答えを簡単に見つけることができます。
- オブジェクト指向プログラミングや手続き型プログラミングと並行するプログラミングパラダイムです。
- その最も重要な特徴は、関数が第一級市民であることです。
- 計算プロセスを再利用可能な関数に分解することを重視します。典型的な例は、
map
メソッドとreduce
メソッドで構成されるMapReduceアルゴリズムです。 - 副作用のない純粋な関数のみが有資格の関数です。
上記のすべての記述は正しいですが、それだけでは十分ではありません。より深い質問、つまり、なぜこのようにするのか、という問いに答えていません。この記事はそれに答えることを目的としています。機能的プログラミングを理解し、その基本的な構文を最も簡単な言語で学ぶお手伝いをします。
I. カテゴリー理論
機能的プログラミングの起源は、圏論と呼ばれる数学の一分野です。機能的プログラミングを理解するための鍵は、カテゴリー理論を理解することです。それは、世界のすべての概念システムを「カテゴリー」に抽象化できると考える複雑な数学です。
1.1 カテゴリーの概念
カテゴリーとは何ですか?Wikipediaからの1文の定義は次のとおりです。「数学では、カテゴリーとは、「矢印」でリンクされた「オブジェクト」で構成される代数構造です。」
つまり、相互に特定の関係を持つ概念、事物、オブジェクトなどはすべて「カテゴリー」を形成します。それらの間の関係を見つけることができる限り、何でも「カテゴリー」を定義できます。
たとえば、さまざまな点とそれらの間の矢印はカテゴリーを形成します。矢印はカテゴリーのメンバー間の関係を表し、正式名称は「射」です。カテゴリー理論では、同じカテゴリーのすべてのメンバーは、さまざまな状態の「変換」であると考えられています。「射」によって、1つのメンバーを別のメンバーに変換できます。
1.2 数学的モデル
「カテゴリー」は、特定の変換関係を満たすオブジェクトのすべてであるため、その数学的モデルは次のように要約できます。
- すべてのメンバーがセットを形成します。
- 変換関係は関数です。
つまり、カテゴリー理論は、集合論のより高レベルな抽象化です。簡単な理解は「集合+関数」です。理論的には、関数を使用して、カテゴリーの他のすべてのメンバーを1人のメンバーから計算できます。
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)
合成は、関数が純粋でなければならない理由でもあります。不純な関数を他の関数とどのように合成できますか?さまざまな合成の後、予想される動作を確実に実現するにはどうすればよいでしょうか?
前に述べたように、関数はデータのパイプのようなものです。次に、関数の合成は、これらのパイプを接続して、データを一度に複数のパイプを通過できるようにすることです。
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. ファンクター
関数は、同じカテゴリー内の値の変換だけでなく、あるカテゴリーを別のカテゴリーに変換するためにも使用できます。これには、ファンクターが関係します。
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ファンクターには、左側の値(Left
)と右側の値(Right
)の2つの値が含まれています。右側の値は通常の場合に使用される値であり、左側の値は右側の値が存在しない場合に使用されるデフォルト値です。
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ファンクター
ファンクターに含まれる値は、関数である可能性があります。1つのファンクターの値が数値であり、別のファンクターの値が関数である状況を想像できます。
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. Monadファンクター
ファンクターは、任意の値を含むことができるコンテナです。ファンクターが別のファンクターを含むことは完全に合法です。ただし、これによりネストされたファンクターが発生します。
Maybe.of( Maybe.of( Maybe.of({name: 'Mulburry', number: 8402}) ) )
このファンクターには、3つのMaybe
がネストされています。内部の値を取得するには、this.val
を3回連続で取得する必要があります。これはもちろん非常に不便であるため、Monadファンクターが登場しました。
Monadファンクターの役割は、常に単層のファンクターを返すことです。これにはflatMap
メソッドがあり、map
メソッドと同じ機能があります。唯一の違いは、ネストされたファンクターが生成された場合、後者の値を抽出して、常に単層のコンテナが返され、ネストされた状況が発生しないようにすることです。
class Monad extends Functor { join() { return this.val; } flatMap(f) { return this.map(f).join(); } }
上記のコードでは、関数f
がファンクターを返す場合、this.map(f)
はネストされたファンクターを生成します。したがって、join
メソッドは、flatMap
メソッドが常に単層のファンクターを返すようにします。これは、ネストされたファンクターがフラット化されることを意味します。
IX. IO操作
Monadファンクターの重要なアプリケーションは、I/O(入出力)操作を実装することです。
I/Oは不純な操作であり、通常の機能的プログラミングでは処理できません。このとき、I/O操作はMonadファンクターとして記述して、操作を完了する必要があります。
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. 簡単なスケーラビリティと高パフォーマンス
- 高い同時実行性を簡単に処理するための自動スケーリング。
- 運用上のオーバーヘッドゼロ—構築に集中するだけです。

リープセルツイッター:https://x.com/LeapcellHQ