ファンクターとカテゴリー

作曲ソフトウェア

スモークアートキューブからスモーク— MattysFlicks —(CC BY 2.0)
注:これは、JavaScript ES6 +で関数型プログラミングと合成ソフトウェア技術を一から学ぶ「ソフトウェアの作成」シリーズ(現在は本です!)の一部です。乞うご期待。まだまだたくさんあります!
<前へ| <<パート1からやり直し|次へ>>

ファンクターデータ型は、マップできるものです。これは、内部の値に関数を適用するために使用できるインターフェイスを備えたコンテナです。ファンクタを見たとき、「マッピング可能」と考える必要があります。通常、ファンクタータイプは、構造を保持しながら入力から出力にマッピングする.map()メソッドを使用してオブジェクトとして表されます。実際には、「構造の保存」とは、戻り値が同じタイプのファンクターであることを意味します(ただし、コンテナー内の値は異なるタイプの場合があります)。

ファンクターは、内部にゼロ個以上のものと、マッピングインターフェイスを備えたボックスを提供します。配列はファンクターの良い例ですが、プロミス、ストリーム、ツリー、オブジェクトなど、他の多くの種類のオブジェクトも同様にマッピングできます。JavaScriptの組み込み配列とプロミスオブジェクトはファンクターのように機能します。コレクション(配列、ストリームなど)の場合、通常、.map()はコレクションを反復し、指定された関数をコレクション内の各値に適用しますが、すべてのファンクターが反復するわけではありません。ファンクタとは、特定のコンテキストで関数を適用することです。

Promiseは、.map()の代わりに.then()という名前を使用します。通常、.then()は非同期の.map()メソッドと考えることができますが、ネストされたプロミスがある場合を除き、その場合は外側のプロミスが自動的にアンラップされます。繰り返しますが、約束ではない値の場合、.then()は非同期の.map()のように動作します。約束そのものである値の場合、.then()はモナドの.flatMap()メソッドのように機能します(.chain()とも呼ばれます)。したがって、プロミスはまったくファンクタではなく、モナドでもありませんが、実際には、通常はどちらとしても扱うことができます。まだモナドが何であるかを心配しないでください。モナドは一種のファンクターなので、まずファンクターを学ぶ必要があります。

他にもさまざまなものをファンクタに変えるライブラリがたくさんあります。

Haskellでは、ファンクタータイプは次のように定義されます。

fmap ::(a-> b)-> f a-> f b

aを取り、bを返す関数と、その中に0個以上のファンクターを指定すると、fmapは、その中に0個以上のbsを持つボックスを返します。 f aおよびf bビットは、「aのファンクター」および「bのファンクター」と読むことができます。つまり、f aはボックス内にあり、f bはボックス内にbsを持っています。

ファンクターの使用は簡単です。map()を呼び出すだけです。

const f = [1、2、3];
f.map(double); // [2、4、6]

ファンクターの法則

カテゴリには2つの重要なプロパティがあります。

  1. 身元
  2. 組成

ファンクターはカテゴリー間のマッピングであるため、ファンクターはアイデンティティと構成を尊重する必要があります。一緒に、それらはファンクターの法則として知られています。

身元

恒等関数(x => x)をf.map()(fは任意のファンクター)に渡すと、結果はfと同等(同じ意味を持つ)になります。

const f = [1、2、3];
f.map(x => x); // [1、2、3]

組成

ファンクターは合成法則に従う必要があります。F.map(x => f(g(x)))はF.map(g).map(f)と同等です。

関数合成とは、ある関数を別の関数の結果に適用することです。たとえば、xと関数fとgが与えられると、合成(f∘g)(x)(通常f f gに短縮-(x)は暗黙)はf(g(x))を意味します。

多くの関数型プログラミング用語はカテゴリ理論に由来し、カテゴリ理論の本質は構成です。カテゴリー理論は最初は怖いですが、簡単です。ダイビングボードから飛び降りたり、ジェットコースターに乗ったりするようなものです。カテゴリ理論の基礎をいくつかの箇条書きで示します。

  • カテゴリは、オブジェクトとオブジェクト間の矢印のコレクションです(「オブジェクト」は文字通り何でも意味できます)。
  • 矢印は射と呼ばれます。形態素は、コードとして関数として考えられ、表現されます。
  • 接続されたオブジェクトのグループa-> b-> cには、a-> cから直接移動する構成が必要です。
  • すべての矢印は、コンポジションとして表すことができます(オブジェクトのID矢印を持つ単なるコンポジションであっても)。カテゴリ内のすべてのオブジェクトには、識別矢印があります。

aを取りa bを返す関数gと、bを取りcを返す別の関数fがあるとします。 fとgの構成を表す関数hも必要です。したがって、a-> cからの合成は、合成f∘g(gの後のf)です。したがって、h(x)= f(g(x))。関数合成は、左から右ではなく、右から左に機能します。そのため、f∘gは、gの後にfと呼ばれることがよくあります。

構成は連想的です。基本的には、複数の関数を作成する場合(空想を感じている場合は射影)、括弧は必要ありません。

h∘(g∘f)=(h∘g)∘f=h∘g∘f

JavaScriptの構成法をもう一度見てみましょう。

ファンクターが与えられた場合、F:

const F = [1、2、3];

以下は同等です:

F.map(x => f(g(x)));
//と同等...
F.map(g).map(f);

エンドファンクター

Endofunctorは、カテゴリーから同じカテゴリーにマップするファンクターです。

ファンクターは、カテゴリーからカテゴリーにマップできます:X-> Y

Endofunctorは、カテゴリーから同じカテゴリーにマップします:X-> X

モナドはエンドファンクターです。覚えておいてください:

「モナドはエンドファンクターのカテゴリーにおけるモノイドにすぎません。どうしたの?"

うまくいけば、その引用がもう少し意味を成し始めていることを願っています。後でモノイドとモナドに行きます。

独自のファンクターを構築する

ファンクターの簡単な例を次に示します。

const Identity = value =>({
  マップ:fn => Identity(fn(value))
});

ご覧のとおり、ファンクターの法則を満たしています。

// trace()は簡単に検査できるユーティリティです
//コンテンツ。
const trace = x => {
  console.log(x);
  return x;
};
const u = Identity(2);
//アイデンティティ法
u.map(trace); // 2
u.map(x => x).map(trace); // 2
const f = n => n + 1;
const g = n => n * 2;
//組成法
const r1 = u.map(x => f(g(x)));
const r2 = u.map(g).map(f);
r1.map(trace); // 5
r2.map(trace); // 5

これで、配列にマッピングできるように、任意のデータ型にマッピングできます。いいね!

これは、ファンクターがJavaScriptで取得できるのと同じくらい簡単ですが、JavaScriptのデータ型に期待されるいくつかの機能が欠落しています。それらを追加しましょう。 +演算子が数値と文字列値に対して機能するとしたら、クールではないでしょうか?

これを機能させるために必要なのは、.valueOf()を実装することだけです。これは、ファンクターから値をアンラップする便利な方法のようにも思えます。

const Identity = value =>({
  マップ:fn => Identity(fn(value))、
  valueOf:()=> value、
});
const ints =(Identity(2)+ Identity(4));
trace(ints); // 6
const hi =(Identity( 'h')+ Identity( 'i'));
trace(hi); // "こんにちは"

いいねしかし、コンソールでIdentityインスタンスを検査したい場合はどうでしょうか? 「Identity(value)」と言うといいですね。 .toString()メソッドを追加しましょう:

toString:()=> `Identity($ {value})`、

クール。おそらく、標準のJS反復プロトコルも有効にする必要があります。カスタムイテレータを追加することでそれを行うことができます。

[Symbol.iterator]:function *(){
  降伏値;
}

これでうまくいきます:

// [Symbol.iterator]は標準のJS反復を有効にします:
const arr = [6、7、... Identity(8)];
trace(arr); // [6、7、8]

Identity(n)を取得し、n + 1、n + 2などを含むIDの配列を返したい場合はどうしますか?簡単ですね。

const fRange =(
  開始、
  終わり
)=> Array.from(
  {長さ:終了-開始+ 1}、
  (x、i)=> Identity(i + start)
);

ああ、でも、これをファンクターと連動させたい場合はどうでしょうか?データ型の各インスタンスにはコンストラクターへの参照が必要であるとの仕様がある場合はどうでしょうか?次に、これを行うことができます:

const fRange =(
  開始、
  終わり
)=> Array.from(
  {長さ:終了-開始+ 1}、
  
  // `Identity`を` start.constructor`に変更します
  (x、i)=> start.constructor(i + start)
);
const range = fRange(Identity(2)、4);
range.map(x => x.map(trace)); // 2、3、4

値がファンクターであるかどうかをテストする場合はどうでしょうか? Identityに静的メソッドを追加して確認できます。静的な.toString()をスローする必要があります。

Object.assign(Identity、{
  toString:()=> 'Identity'、
  x:> typeof x.map === '関数'
});

これらすべてをまとめましょう。

const Identity = value =>({
  マップ:fn => Identity(fn(value))、
  valueOf:()=> value、
  toString:()=> `Identity($ {value})`、
  [Symbol.iterator]:function *(){
    降伏値;
  }、
  コンストラクター:アイデンティティ
});
Object.assign(Identity、{
  toString:()=> 'Identity'、
  x:> typeof x.map === '関数'
});

ファンクタまたはエンドファンクタとしての資格を得るために、この余分なものはすべて必要ないことに注意してください。あくまでも便宜上です。ファンクターに必要なのは、ファンクターの法則を満たす.map()インターフェースだけです。

なぜファンクターなのか?

ファンクターは多くの理由で素晴らしいです。最も重要なことは、これらは抽象化であり、あらゆるデータ型で機能する方法で多くの有用なものを実装するために使用できることです。たとえば、ファンクタ内の値が未定義またはヌルでない場合にのみ、一連の操作を開始したい場合はどうでしょうか?

//述語を作成します
const exists = x =>(x.valueOf()!== undefined && x.valueOf()!== null);
const ifExists = x =>({
  マップ:fn => exists(x)? x.map(fn):x
});
const add1 = n => n + 1;
const double = n => n * 2;
// 何も起こりません...
ifExists(Identity(undefined))。map(trace);
//まだ何も...
ifExists(Identity(null))。map(trace);
// 42
ifExists(Identity(20))
  .map(add1)
  .map(double)
  .map(トレース)
;

もちろん、関数型プログラミングとは、小さな関数を構成して、より高いレベルの抽象化を作成することです。ファンクターで機能する汎用マップが必要な場合はどうしますか?これにより、引数を部分的に適用して新しい関数を作成できます。

簡単です。お気に入りの自動カレーを選ぶか、前からこの魔法の呪文を使用してください:

const curry =(
  f、arr = []
)=>(... args)=>(
  a => a.length === f.length?
    f(... a):
    カレー(f、a)
)([... arr、... args]);

これでマップをカスタマイズできます:

const map = curry((fn、F)=> F.map(fn));
const double = n => n * 2;
const mdouble = map(double);
mdouble(Identity(4))。map(trace); // 8

結論

ファンクターは、マップできるものです。より具体的には、ファンクターはカテゴリからカテゴリへのマッピングです。ファンクターは、あるカテゴリーから同じカテゴリー(つまり、エンドファンクター)にマップすることさえできます。

カテゴリは、オブジェクト間に矢印が付いたオブジェクトのコレクションです。矢印は射(別名関数、別名合成)を表します。カテゴリ内の各オブジェクトには、恒等射(x => x)があります。オブジェクトのチェーンA-> B-> Cには、コンポジションA-> Cが存在する必要があります。

ファンクターは、任意のデータ型で機能するさまざまな汎用関数を作成できる、高次の優れた抽象化です。

次:機能的ミックスイン>

ライブ1:1メンターシップでスキルをレベルアップ

DevAnywhereは、高度なJavaScriptスキルをレベルアップする最速の方法です。

  • ライブレッスン
  • 柔軟な時間
  • 1:1のメンターシップ
  • 実際の本番アプリを構築する
https://devanywhere.io/

エリックエリオットは「プログラミングJavaScriptアプリケーション」(O’Reilly)の著者であり、DevAnywhere.ioの共同設立者です。彼は、Adobe Systems、Zumba Fitness、The Wall Street Journal、ESPN、BBC、およびUsher、Frank Ocean、Metallicaなどのトップレコーディングアーティストのソフトウェアエクスペリエンスに貢献しています。

彼は、世界で最も美しい女性と、どこでも好きな場所で仕事をしています。