双射とF-代数
だから私はしばしば”catamorphism”と”recursion scheme”という言葉を聞いています。 それは何ですか?
Catamorphisms(またはcata)は、関数型プログラミングにおける折畳の概念の一般化である。 F代数と再帰的なデータ構造が与えられた場合、カタモルフィズムはデータ構造を再帰的に評価することによって値を生成します。
F-代数とは何ですか? 多分あなたは最初にいくつかのコード例を示すことができますか?
設定はそれほど簡単ではないので、簡単に始めましょう。 式を表すために次のデータ構造があるとしましょう:
単純な式は次のようになります:
もちろん、結果を評価して取得する関数が必要です:
これがカタモルフィズムの一般化の出番です:
🤨, しかし、ポイントは何ですか? 関数に引数を適用するだけですか?
それはnotReallyCata
だからです。 実際のcata
関数は汎用であり、特定のデータ構造やエバリュエーター関数には依存しません。 参照してください、再帰的なデータ構造を作成し、それを折りたたむことは、cata
が一般化しようとする一般的なパターンです。
さて、本当のcataはどのように見えますか?
🤯
それがnotReallyCata
から始まった理由です。 それがクリックされるまで、後で実装を分解します。 しかし今私達のExpression
の例を続けてみましょう。 まず、型パラメータを導入することによって再帰を取り除く必要があります:
Expression
へのすべての参照はa
型パラメータに置き換えられ、データ構造は再帰的ではなくなりました。
型コンストラクタの最後に
F
があるのはなぜですか?
あなたが尋ねてうれしい—それはExpressionF
ができるヒントですFunctor
:
構造を保持するラップされた値にいくつかの関数を適用するだけです。
なぜそれが必要なのか分かりません🤔
それは今は意味がありませんが、少し後になります。 ここで、式の作成方法は変更されていません(コンストラクタ名を除きます):
しかし、結果の型は異なります:
expr
すべてを単一のExpression
に折りたたみ、exprF
は式ツリーのネストレベルに関する情報をエンコードします。 評価について言えば、これは私たちがevalの実装について行くことができる方法ですExpressionF
:
元のevalExpr
との主な違いは、evalExprF
への再帰呼び出しがないことです(ExpressionF
は再帰的ではありません、覚えていますか?). また、エバリュエーターは単一レベルの式でのみ動作できることを意味します:
そして、これでコンパイルされません:
単純にexprF
がExpressionF Int
をexpepectsし、ExpressionF (ExpressionF Int)
を押しているからです。
それを機能させるために、別の評価者を定義することができます:
ちょっとアドホックに見えますが、深く入れ子になった式がある場合はどうなりますか?
はい、任意のネストされた式の場合、このアプローチはスケーラブルではありません—追加のネストレベルごとに特殊な関数を記述する必要があります。
この入れ子を新しい型で一般化する方法があります:
修正? それほど多くのことをしない再帰的なデータ構造のように見えます。 それはどのように便利ですか?
最初に等号の前の式を見てみましょう:実際にFix
は、1つの型パラメータf
を持つ再帰的なデータ構造です。 このパラメータにはkind* -> *
があります。 たとえば、Int
またはBool
を提供するFix
を構築することはできません。Maybe
、List
または…ExpressionF
のようなものでなければなりません。 これが、ExpressionF
の型パラメータを導入した理由です。 次に、等号の後に、基本的にf
の値を構成する式であるf (Fix f)
型の単一の引数を取る単一の型コンストラクタFx
があります。 Maybe
の場合、それはMaybe (Fix Maybe)
になり、すべてがFx
でラップされてタイプFix Maybe
になります。
型シグネチャは、型コンストラクタが型自体と自己参照と同じ名前を持つことができるため、最初に読むのが混乱します。 しかし、高次の型をデータ構造にラップするだけではありません。 ところで、unfix
はFx
とは反対であり、Fx
のパターンマッチングであり、ラップされた値を返します。
ここで、式ツリーのすべてのExpressionF
をFix ExpressionF
に置き換えます。 Fx
の有無にかかわらず式を構築する際の違いに注意してください—それらは基本的に同じですが、前に追加する必要がありますFx $
:
‘固定’バージョンの結果の型はFix ExpressionF
なので、再帰的な表現に戻りますが、非再帰的なデータ構造を取り戻すにはunfix
関数を使用する必要があります。
Fix
を持つことの利点は何ですか? 元のExpression
タイプと同じアプローチのように見えますが、今はこの奇妙なFix
とunfix
ナンセンスがありますか?
はい、しかし、我々は折り畳みのプロセスを一般化しようとしている、それは我々が後で議論するFix
とAlgebra
のような追加の抽象化の導入を必要とします。 私と一緒に負担し、それは後でより多くの意味をなさないはずです。
だから、私たちは私たちの’固定’データ構造を持って、評価関数はどのように見えるでしょうか?
与えられたFix ExpressionF
我々はそれで行うことができる唯一のことは、生成するunfix
を呼び出すことですExpressionF (Fix ExpressionF)
:
返されるExpressionF
は、型パラメータとしてFix ExpressionF
を持つValueF
、AddF
、またはMultF
のいずれかになります。 パターンマッチングを行い、次に何をすべきかを決定するのは理にかなっています:
はい、それはExpression
の最初の再帰的な評価者と同じように見えますが、式をunfix
でラップ解除する必要があります。 では、なぜFix
を気にするのですか?
ここにキーがあります: 私たちはExpressionF
の元の’fix-less’エバリュエーターを再利用し、何とかそれをFix ExpressionF
構造上に配布します。 したがって、これは2つの引数を取る関数でなければなりません—評価者と評価する構造体:
実装を理解してみましょう-最初に行うべき論理的なことは、unfix
を使用してExpressionF
を取得し、それをExpressionF
渡すことですevaluator
:
明らかにこれは機能しませんが、evaluator
はExpressionF (Fix ExpressionF)
ではなくExpressionF Int
を期待しています。 ところで、ExpressionF
はFunctor
であることを覚えていますか? これが便利な場所です—fmap
を使用して、式ツリーの内部レベルに同じプロセスを適用できます:
私たちは再帰関数almostCata evaluator
をfmap
に渡しています。 現在の式がAddF
またはMultF
の場合、この関数は1レベル深く渡され、fmap
が再び呼び出されます。 これはValueF
に達するまで起こり、ValueF
をfmappingするとExpressionF Int
型の値が返され、それがevaluator
関数が受け入れるものです。
almostCata
を見ると、ExpressionF
またはInt
型に固有のものは実際にはなく、理論的にはいくつかの型パラメータf
で一般化できることがわかります。 唯一の制約は、f
のFunctor
インスタンスを持つ必要があります。fmap
:
そして、それがcata
の最終版です。 以下は、いくつかの使用例を含む完全な実装です:
それはクールだと思います。 しかし、なぜカントー?
圏論や関数型プログラミングにおける多くの概念はかなり抽象的であり、時には特定のアイデアのための実用的なアプリケーションをすぐに見つ しかし、抽象化と一般化を探すことは、それ以外の場合はアドホックな実装を必要とする問題のパターンとエレガントな解決策を見つけるのに役立
ところで、私たちのExpressionF -> Int
関数をFunctor f => (f a -> a)
に一般化することによって、F-代数と呼ばれる別の重要な概念を発見しました。 基本的にF-代数は関手f
、ある種の型a
と評価関数f a -> a
の三重である。 ここではa
は多型ではなく、Int
やBool
のような具体的な型でなければならず、キャリア型と呼ばれます。 任意のendo-functorf
に対して、それに基づいて複数のF代数を作成できます。 式の例を取る—endo-functorf
はExpressionF
、a
はInt
、評価者はevalExprF
です。 しかし、我々はキャリアの種類を変更し、より多くの代数を生成することができます:
それは
cata
に渡すことができるだけの異なる評価者ですよね?
はい、異なるキャリアタイプを選択し、実装を選択しています。 Fix ExprF
になるようにキャリアタイプを選択することで作成できるすべての評価者の母親があります。
Int
またはBool
に評価することは完全に理にかなっていますが、このinitialAlgebra
は何を評価しますか? 私の評価者の結果として何かのFix
が必要なのはいつですか?もちろん、あなたはそのようなことを自分で書くことはありません。f-algebrasとcataの背後にあるより深い意味を示したいだけです。 実際、私たちはすでにそのような評価者のための実装を持っており、それはまさにFx
コンストラクタです:待って、
Fx
は評価者ですか? それは狂っている。はい、それはあなたができる最も簡単なことをします—expessionをデータ構造に保存します。 他のすべてのエバリュエーター(
algebra0
、algebra1
)は式を減らすことによって何らかの値を生成しましたが(合計や連結など)Fx
は、データを失うことなく式をラップするだけです。これが、最初に
Fix
を導入した理由です—最初にFx
で元のデータ構造を初期代数Fix f
に評価し、cata
を使用して”本当の”評価は、初期代数よりも具体的な評価者をfmap
圏論の観点から見ると、同じ遠藤関手に基づくすべての代数は圏を形成する。 このカテゴリには、キャリアタイプをFix f
として選択することによって作成された初期代数である初期オブジェクトがあります。 Bartosz Milewskiによるいくつかの素晴らしいブログ記事があります。それはまだ理解するのはかなり難しいです、私は概念を完全に理解しているとは思わない
それは常に手で行う方が良いです:あなた自身で
Fix
とcata
たとえば、String
は再帰的に表すことができます(String
のChar
headとtailとして)、文字列のlength
はcata
で計算できます。 ここにそれ以上の読書のためのある大きい資源はある:
- F代数とわずかに異なるF代数の理解Bartosz Milewski
- Catamorphisms in15minutes By Chris Jones
- Rob Norris
- Haskell wikiのCatamorphisms
- Pure Functional Database Programming with Fixpoint Types by Haskell wiki
- Practical recursion scheme by Chris Jones
- Catamorphisms by Haskell wiki
- Practical recursion scheme by Chris Jones
- Catamorphisms by Haskell wiki
- ジャレッド-トービン