Katamorfizmusok és F-algebrák
ezért gyakran hallom a “katamorfizmus” és a “rekurziós sémák”szavakat. Mi ez az egész?
a Katamorfizmusok (vagy cata) a fold fogalmának általánosítása a funkcionális programozásban. Az F-Algebra és a rekurzív adatstruktúra alapján a katamorfizmus értéket fog előállítani az adatstruktúra rekurzív értékelésével.
mi az F-Algebra? Talán először megmutathat néhány kódpéldát?
a beállítás nem olyan egyszerű, Tehát kezdjük egyszerűen. Tegyük fel, hogy a következő adatstruktúrával rendelkezik egy kifejezés ábrázolásához:
és egy egyszerű kifejezés így nézhet ki:
miután csak egy kifejezés adatstruktúra haszontalan, természetesen szüksége lesz egy függvényt, hogy értékelje, majd kap az eredmény:
itt jön be a katamorfizmus általánosítása:
🤨, de mi értelme? Csak egy érvet alkalmaz egy függvényre?
ez azért van, mert notReallyCata
. A valós cata
függvény általános, és nem függ semmilyen adatstruktúrától vagy kiértékelő függvénytől. Lát, rekurzív adatstruktúra létrehozása, majd áthajtása egy általános minta, amelyet cata
megpróbál általánosítani.
Ok, akkor hogyan néz ki az igazi cata?
🤯
ezért kezdtük a notReallyCata
– val. Később lebontjuk a megvalósítást, amíg kattan. De most folytassuk a Expression
példánkkal. Először meg kell szabadulnunk a rekurziótól egy típusparaméter bevezetésével:
minden Expression
hivatkozást a
típusú paraméterrel helyettesítünk, így az adatszerkezet már nem rekurzív.
miért van egy
F
a típuskonstruktorok végén?
örülök, hogy megkérdezte — Ez egy tipp, hogy ExpressionF
lehet egy Functor
:
semmi divatos, csak valamilyen funkciót alkalmaz a becsomagolt értékmegőrző szerkezetre.
nem tudom, miért van szükségünk erre 🤔
most nincs értelme, de egy kicsit később lesz. A kifejezés létrehozásának módja nem változott (kivéve a konstruktor nevét):
de a kapott Típus más:
expr
mindent egyetlen Expression
– re omlik össze, míg a exprF
kódolja az expressziós fa fészkelési szintjével kapcsolatos információkat. Az értékelésről szólva, így mehetünk az eval végrehajtásáraExpressionF
:
a fő különbség az eredeti evalExpr
az, hogy nincs rekurzív hívás evalExprF
(ExpressionF
nem rekurzív, emlékszel?). Ez azt is jelenti, hogy kiértékelőnk csak egyetlen szintű kifejezéssel tud dolgozni:
és nem fordítja ezt:
egyszerűen azért, mert a exprF
elvárja a ExpressionF Int
– t, mi pedig a ExpressionF (ExpressionF Int)
– t toljuk.
ahhoz, hogy ez a munka tudnánk meghatározni egy másik értékelő:
úgy néz ki, kicsit ad hoc, mi van, ha mélyen beágyazott kifejezések?
Igen, tetszőleges beágyazott kifejezésnél ez a megközelítés nem skálázható — minden további fészkelési szinthez speciális függvényt kell írni.
van egy módja annak, hogy általánosítsuk ezt a fészkelést egy új típussal:
megjavítani? Úgy néz ki, mint egy rekurzív adatstruktúra, amely nem sokat tesz. Hogyan hasznos?
először nézzük meg az egyenlőségjel előtti kifejezést: valóban Fix
egy rekurzív adatszerkezet, amelynek egy típusa van paraméter f
. Ennek a paraméternek a kind * -> *
értéke van, pl. típusparamétert is igényel. Például nem lehet Fix
konstrukciót megadni Int
vagy Bool
, valami olyasminek kell lennie, mint Maybe
, List
vagy… ExpressionF
. Ezért vezettük be a type paramétert ExpressionF
. Ezután az egyenlőségjel után egyetlen Fx
típusú konstruktorunk van, amely egyetlen f (Fix f)
típusú argumentumot vesz fel, amely alapvetően egy kifejezés, amely felépíti f
értékét. Abban az esetben, Maybe
lenne Maybe (Fix Maybe)
, majd az egész dolog csomagolva Fx
a típus Fix Maybe
.
a típus aláírás zavaró olvasni az első, mert típus konstruktor lehet ugyanaz a neve, mint maga a Típus Plusz önálló hivatkozás. De nincs sokkal több, mint egy magasabb rendű típus becsomagolása egy adatstruktúrába. A btw, unfix
ellentéte a Fx
– nek, és csak a minta illesztése a Fx
– nél, és csomagolt értéket ad vissza, nem nagy ügy.
most minden ExpressionF
kifejezést kicserélünk Fix ExpressionF
– re. Figyeljük meg a különbséget a kifejezések szerkesztésében Fx
— vel és anélkül-alapvetően ugyanazok, kivéve, hogy elöl kell Fx $
:
az így kapott ‘fix’ verzió típusa Fix ExpressionF
tehát visszatérünk a rekurzív ábrázoláshoz, de most a unfix
függvényt kell használnunk a nem rekurzív adatstruktúra visszaállításához.
milyen előnyökkel jár a
Fix
? Úgy tűnik, hogy ugyanaz a megközelítés, mint az eredetiExpression
típus, de most van ez a furcsaFix
ésunfix
ostobaság?
igen, de megpróbáljuk általánosítani a hajtogatás folyamatát, további absztrakciók bevezetését igényli, mint például a Fix
és a Algebra
, amelyeket később tárgyalunk. Légy türelmes, majd később értelmesebb lesz.
tehát megvan a rögzített adatstruktúránk, hogyan nézne ki az értékelési funkció?
mivel a Fix ExpressionF
az egyetlen dolog, amit tehetünk vele hívja unfix
amely termel ExpressionF (Fix ExpressionF)
:
a visszaadott ExpressionF
lehet az egyik ValueF
, AddF
vagy MultF
, amelynek típusparamétere Fix ExpressionF
. Érdemes elvégezni a minta illesztését, és eldönteni, hogy mi a következő lépés:
igen, ugyanúgy néz ki, mint a legelső rekurzív kiértékelőnk Expression
azzal a kiegészítéssel, hogy a kifejezést unfix
– val kell kibontani. Akkor miért zavarja a Fix
egyébként?
itt a kulcs: újra felhasználjuk az eredeti ‘fix-less’ kiértékelőt ExpressionF
– re, és valahogy elosztjuk a Fix ExpressionF
struktúrán. Tehát ennek a függvénynek két argumentumot kell tartalmaznia — az értékelő és az értékelendő struktúra:
próbáljuk kitalálni a megvalósítást — az első logikus dolog az, hogy a unfix
használatával ExpressionF
– et kapunk, majd talán átadjuk evaluator
:
nyilvánvaló, hogy ez nem működik, a evaluator
ExpressionF Int
– re számít, nem pedig ExpressionF (Fix ExpressionF)
– re. Egyébként ne feledje, hogy a ExpressionF
egy Functor
? Ez az, ahol hasznos lesz — a fmap
használatával ugyanazt a folyamatot alkalmazhatjuk az expressziós fa belső szintjére:
álljunk meg egy pillanatra, és gondoljuk át, mi történik: egy almostCata evaluator
rekurzív függvényt adunk át a fmap
– be. Ha az aktuális kifejezés AddF
vagy MultF
, akkor ez a függvény egy szinttel mélyebbre kerül, és a fmap
ismét meghívásra kerül. Ez addig fog történni, amíg el nem érjük a ValueF
értéket, a ValueF
feletti fmapping a ExpressionF Int
típusú értéket adja vissza, és pontosan ez az, amit a evaluator
függvényünk Elfogad.
ha megnézzük a almostCata
– et, láthatjuk, hogy valójában nincs semmi konkrét ExpressionF
vagy Int
típusra, és elméletileg általánosítható valamilyen f
típusparaméterrel. Az egyetlen kényszer kell, hogy legyen egy Functor
példánya f
, mert a fmap
:
és ez a cata
végleges változata. Itt található a teljes megvalósítás néhány használati példával:
azt hiszem, ez király. De miért tho?
a kategóriaelméletben és a funkcionális programozásban sok fogalom meglehetősen elvont, és néha nehéz azonnali gyakorlati alkalmazást találni bizonyos ötletekre. De az absztrakciók és általánosítások keresése hasznos minták és elegáns megoldások megtalálásához olyan problémákra, amelyek egyébként ad-hoc megvalósítást igényelnek.
mellesleg, a ExpressionF -> Int
függvényünk Functor f => (f a -> a)
-ra történő általánosításával felfedeztünk egy másik fontos fogalmat, az úgynevezett F-Algebra. Alapvetően az F-Algebra a functor f
hármasa, néhány típus a
és az értékelő függvény f a -> a
. Ne feledje, hogy a a
itt nem polimorf — konkrét típusnak kell lennie, például Int
vagy Bool
, és hordozótípusnak hívják. Bármely endo-functor f
hozhat létre több F-Algebra azon alapul. Vegyük például az endo — functor f
ExpressionF
, a
Int
és az értékelő evalExprF
kifejezéseket. De megváltoztathatjuk a hordozó típusát és több algebrát állíthatunk elő:
ez csak a különböző értékelők, hogy át lehet adni
cata
, jobb?
igen, különböző hordozótípusokat választunk, és a megvalósítást választjuk. De ott a trükk — van egy anya az összes értékelő, hogy tudjuk létrehozni a szedés a fuvarozó típusát, hogy… Fix ExprF
.
Értékelés
Int
vagyBool
teljesen van értelme, de mi lenne ezinitialAlgebra
értékelni? Mikor kellFix
valaminek lennie az értékelő eredményeként?
természetesen nem írsz ilyesmit magad, csak meg akarod mutatni az f-algebrák és a cata mögött rejlő mélyebb jelentést. Valójában már van egy implementáció az ilyen értékelő számára, és pontosan Fx
konstruktor:
várj,
Fx
egy értékelő? Ez őrültség.
Igen, és ez nem a legegyszerűbb dolog, amit tehetünk — mentse a expesszió egy adatstruktúra. Míg az összes többi értékelő (algebra0
, algebra1
) valamilyen értéket produkált a kifejezés csökkentésével (például összegzés vagy összefűzés) Fx
csak becsomagolja a kifejezést anélkül, hogy bármilyen adatot elveszítene.
ezért vezettük be először a Fix
— ot-először az eredeti adatstruktúrát Fx
– vel értékeljük a kezdeti algebra Fix f
– be, majd a cata
– et használva a ‘valós’ értékelés fmap
– vel történik a konkrét értékelő inital algebra felett.
kategóriaelméleti szempontból az ugyanazon endo-functoron alapuló összes algebrák kategóriát alkotnak. Ennek a kategóriának van egy kezdeti objektuma, amely a kezdeti algebra, amelyet a hordozó típusának Fix f
kiválasztásával hoztunk létre. Van néhány nagyszerű blogbejegyzés Bartosz Milewski-tól, amelyeket nagyon ajánlom megnézni, ha mély kategorikus megértést szeretne kapni.
ez még mindig elég nehéz megérteni, nem hiszem, hogy teljesen értem a koncepció
ez mindig jobb, hogy nem a kezét: próbálja újra végrehajtási Fix
és cata
a saját, gondolni a lehetséges adatstruktúrák és algebrák. Például egy String
rekurzív módon ábrázolható (Char
fej és farok String
), a length
karakterlánc kiszámítható cata
értékkel. Íme néhány nagyszerű forrás a további olvasáshoz:
- az F-algebrák és a kissé eltérő F-algebrák megértése Bartosz Milewski-tól
- Katamorfizmusok 15 perc alatt Chris Jones-tól
- tiszta funkcionális adatbázis programozás fixpont típusokkal Rob Norris-tól
- Katamorfizmusok a Haskell wikin
- Jared gyakorlati rekurziós sémái Tobin