Sari la conținutul principal

Monade și comonade

O noțiune care este de neevitat în programarea funcțională este aceea de monadă. În principiu, o monadă este un functor care poate condensa mai multe aplicări ale sale într-o singură aplicare într-un mod monoidal. Cu alte cuvinte, o monadă este un monoid în categoria endofunctorilor din care aparține (ignorăm momentan posibilitatea de a nu avea categoria de endofunctori dacă există probleme de dimensionare). Bineînțeles, noțiunea duală este cea de comonadă, un comonoid în categoria endofunctorilor din care aparține. O comonadă face exact inversul fată de o monadă, desfășoară aplicarea sa în mai multe aplicări.

Deși comonadele nu sunt atăt de discutate cum sunt monadele, aceste sunt extrem de utile și interesante. Vom vedea în cele ce urmează cum anume monadele și comonadele ne ajută.

Monade

Fie C\mathcal{C} o categorie. O monadă este un triplet (T,η,μ)(T, \eta, \mu) unde T:CCT : \mathcal{C} \rightarrow \mathcal{C} este un endofunctor, η:IdCT\eta : Id_{\mathcal{C}} \Rightarrow T și μ:T2=TTT\mu : T^2 = T \circ T \Rightarrow T sunt două transformări naturale, numite unitatea, respectiv multiplicarea monadei, cu proprietatea că următoarele diagrame comută:

Diagramele pot fi condensate în formulele:

  • μηT=μTη=idT\mu \circ \eta T = \mu \circ T \eta = id_T
  • μTμ=μμT\mu \circ T \mu = \mu \circ \mu T

Practic, unitatea monadei duce obiecte în aplicări ale monadei iar multiplicarea condensează aplicări multiple astfel încât indiferent cum aplicăm transformările naturale obținem aceiași singură aplicare a monadei. Aceste două transformări naturale sunt cunoscute ca funcțiile return :: a -> T a și join :: T (T a) -> T a din Haskell dar de obicei monadele sunt definite ca operațiile return și bind :: T a -> (a -> T b) -> T b. Operația de bind este practic join aplicat după fmap. Alt nume pentru bind este flatMap pentru că asta și face, aplică o mapare și aplatizează (flattens) tipul de date, join este doar o operație de aplatizare și se mai numeste flatten. Utilitate monadelor constă în faptul că unitatea ne permite să înglobăm date într-un functor iar multiplicarea agregă datele din mai multe aplicări sau contexte într-un singur context ca să lucram mai ușor cu instanța functorului.

Comonade

Fie C\mathcal{C} o categorie. O comonadă este un triplet (G,ϵ,δ)(G, \epsilon, \delta) unde G:CCG : \mathcal{C} \rightarrow \mathcal{C} este un endofunctor, ϵ:GIdC\epsilon : G \Rightarrow Id_{\mathcal{C}} și G:GG=G2G \Rightarrow : G \circ G = G^2 sunt două transformări naturale, numite counitatea, respectiv comultiplicarea comonadei, cu proprietatea că următoarele diagrame comută:

Diagramele pot fi condensate în formulele:

  • ϵGδ=Gϵδ=idG\epsilon G \circ \delta = G \epsilon \circ \delta = id_G
  • Gδδ=δGδG \delta \circ \delta = \delta G \circ \delta

Counitatea și comultiplicarea acționează în sensul invers ca la unitatea și multiplicarea monadei, acestea mai sunt cunoscute în limbajele de programare ca extract :: G a -> a și duplicate :: G a -> G (G a) având și dualul lui bind ca cobind :: (a -> G b) -> G a -> G b. Ambele asigură că indiferent cum le aplicăm obținem aceleași tipuri de date imbricate. Comonadele sunt foarte utile pentru că counitatea poate extrage obiectul din interiorul aplicării functorului, lucru care lipsește la monade, adică la nevoie putem accesa direct datele dintr-o comonadă dacă dorim. Comultiplicarea este de asemena foarte utilă pentru că ne permite să duplicăm date și să le refolosim în contexte noi.

Legătura cu adjuncții

Un lucru important legat de (co)monade este faptul că acestea apar mereu dintr-o pereche de functori adjuncți. Dacă avem doi functori adjuncți LRL \vdash R, L:CDL : \mathcal{C} \rightarrow \mathcal{D} și R:DCR : \mathcal{D} \rightarrow \mathcal{C} atunci există o monadă T:DDT : \mathcal{D} \rightarrow \mathcal{D} și o comondă G:CCG : \mathcal{C} \rightarrow \mathcal{C}.

  • monada TT este compusul functorilor RLR \circ L, unitatea monadei este unitatea adjuncției η\eta iar multiplicarea este data de counitatea adjuncției μ:TT=RLRLRϵLRL=T\mu : T \circ T = R \circ L \circ R \circ L \xRightarrow{R \epsilon L} R \circ L = T
  • comonada GG este compusul functorilor LRL \circ R, counitatea comonadei este counitatea adjuncției η\eta iar comultiplicarea este data de unitatea adjuncției δ:LR=GLηRLRLR=GG\delta : L \circ R = G \xRightarrow{L \eta R} L \circ R \circ L \circ R = G \circ G

Canonic exista două categorii prin care putem defini adjuncții care produc monade, anume categoria Kleisli a unei monade și categoria Eilenberg-Moore sau categoria algebrelor pentru o monadă.

Categoria Kleisli

Fie C\mathcal{C} o categorie și cu o monadă (T,η,μ)(T, \eta, \mu). Categoria Kleisli a acestei monade este CT\mathcal{C}_T cu:

  • Ob(C)=Ob(CT)Ob(\mathcal{C}) = Ob(\mathcal{C}_T)
  • HomC(x,Ty)=HomCT(xT,yT)Hom_\mathcal{C}(x, Ty) = Hom_{\mathcal{C}_T}(x_T, y_T)

Cu alte cuvinte, obiectele sunt aceleași dar f:xTyHom(C)\forall f : x \rightarrow Ty \in Hom(\mathcal{C}) devin f:xTyTHom(CT)f^* : x_T \rightarrow y_T \in Hom(\mathcal{C}_T). Astfel avem:

  • idxT=ηXid_{x_T} = \eta_X^*
  • f:xTyTf^* : x_T \rightarrow y_T, g:yTzTg^* : y_T \rightarrow z_T, gTf=(μzTgf)g^* \circ_T f^* = (\mu_z \circ Tg \circ f)^*

Putem crea o pereche de functori adjuncți LRL \vdash R în felul următor:

  • L mapează fiecare obiect din C\mathcal{C} la același obiect din CT\mathcal{C}_T, Lx=xTLx = x_T și orice morfism f:xyf : x \rightarrow y la Lf=(ηyf)Lf = (\eta_y \circ f)^*
  • R mapează fiecare obiect din xTOb(CT)x_T \in Ob(\mathcal{C}_T) la R(xT)=TxR(x_T) = Tx și orice morfism f:xTyTHom(CT)f^* : x_T \rightarrow y_T \in Hom(\mathcal{C}_T) la Rf=μyTfRf^* = \mu_y \circ Tf

Se poate vedea ușor că acești doi functori sunt adjuncți și produc monada:

  • xOb(C)\forall x \in Ob(\mathcal{C}), (RL)(x)=RxT=Tx(R \circ L)(x) = Rx_T = Tx
  • f:xyHom(C)\forall f : x \rightarrow y \in Hom(\mathcal{C}), (RL)(f)=R((ηyf))=μyT(ηyf)=Tf(R \circ L)(f) = R((\eta_y \circ f)^*) = \mu_y \circ T(\eta_y \circ f) = Tf
  • unitatea adjuncției există deja și este unitatea monadei

Invers, obținem o comonadă G:CTCTG : \mathcal{C}_T \rightarrow \mathcal{C}_T:

  • xTOb(CT)\forall x_T \in Ob(\mathcal{C}_T), GxT=(LR)(xT)=L(Tx)=(Tx)TGx_T = (L \circ R)(x_T) = L(Tx) = (Tx)_T
  • f:xTyTHom(CT)\forall f^* : x_T \rightarrow y_T \in Hom(\mathcal{C}_T), Gf=(LR)(f)=L(μyTf)=(ηyμyTf)=(Tf)Gf^* = (L \circ R)(f^*) = L(\mu_y \circ Tf) = (\eta_y \circ \mu_y \circ Tf)^* = (Tf)^*
  • counitatea adjuncției este dată de multiplicarea și unitatea monadei pentru că f:xTyTHom(CT)\forall f^* : x_T \rightarrow y_T \in Hom(\mathcal{C}_T), (ηy)Tf=(μyTηyf)=(μyTfηx)=(Tf)T(ηx)(\eta_y)^* \circ_T f^* = (\mu_y \circ T\eta_y \circ f)^* = (\mu_y \circ Tf \circ \eta_x)^* = (Tf)^* \circ_T (\eta_x)^*
  • comultiplicarea acestei comonade este dată de deltax=(Tηx)delta_x = (T\eta_x)^* pentru că o diagrama în C\mathcal{C}

Devine următoarea diagramă în CT\mathcal{C}_T:

Algebre pentru o monadă

Înainte de a introduce categoria Eilenberg-Moore trebuie definite algebrele pentru monade, acestea diferă față de algebrele pentru un endofunctori prin faptul că pe lângă definiția generală a acestora au o structură suplimentară compatibilă cu unitatea și multiplicarea monadei.

Fie (T,η,μ)(T, \eta, \mu) o monadă, o algebră pentru acestă monadă este o algebră (a,α)(a, \alpha) cu structura suplimentară că următoarele diagrame comută:

Cu alte cuvinte, αηa=ida\alpha \circ \eta_a = id_a și αμa=αTα\alpha \circ \mu_a = \alpha \circ T\alpha.

Categoria Eilenberg-Moore

Fie C\mathcal{C} o categorie și cu o monadă (T,η,μ)(T, \eta, \mu). Categoria Eilenberg-Moore a acestei monade este CT\mathcal{C}^T cu:

  • Ob(CT)={(x,νx)  xOb(C),α:TxxHom(C)}Ob(\mathcal{C}^T) = \{(x, \nu^x)~|~x \in Ob(\mathcal{C}), \alpha : Tx \rightarrow x \in Hom(\mathcal{C})\}, adică mulțimea de obiecte este mulțimea algebrelor pentru TT
  • Hom(CT)={fT  fT=f:xyHom(C, νyTf=fνx),α:TxxHom(C)}Hom(\mathcal{C}^T) = \{f^T~|~f^T = f : x \rightarrow y \in Hom(\mathcal{C},~\nu^y \circ Tf = f \circ \nu^x), \alpha : Tx \rightarrow x \in Hom(\mathcal{C})\} este mulțimea homomorfismelor între algebrele pentru TT

Putem crea o pereche de functori adjuncți LRL \vdash R în felul următor:

  • L mapează xOb(C)\forall x \in Ob(\mathcal{C}) la Lx=(Tx,μx)Lx = (Tx, \mu_x) și f:xyHom(C)\forall f : x \rightarrow y \in Hom(\mathcal{C}) la Lf=(Tf)T:(Tx,μx)(Ty,μy)Lf = (Tf)^T : (Tx, \mu_x) \rightarrow (Ty, \mu_y)
  • R mapează (x,νx)Ob(CT)\forall (x, \nu^x) \in Ob(\mathcal{C}^T) la R(x,νx)=xR(x, \nu^x) = x și fT:(x,νx)(y,νy)\forall f^T : (x, \nu^x) \rightarrow (y, \nu^y) la R(fT)=f:xyR(f^T) = f : x \rightarrow y, cu alte cuvinte RR este un functor de uitare (forgetful functor), uită de anumite elemente din categorie, și pentru că este adjunctul la dreapta numim adjunctul la stânga al unui functor de uitate functor liber (free functor)

Se poate vedea ușor că acești doi functori sunt adjuncți și produc monada:

  • xOb(C)\forall x \in Ob(\mathcal{C}), (RL)(x)=R(Tx,μx)=Tx(R \circ L)(x) = R(Tx, \mu_x) = Tx
  • f:xyHom(C)\forall f : x \rightarrow y \in Hom(\mathcal{C}), (RL)(f)=R((Tf)T)=Tf(R \circ L)(f) = R((Tf)^T) = Tf
  • unitatea adjuncției există deja și este unitatea monadei

Invers, obținem o comonadă G:CTCTG : \mathcal{C}^T \rightarrow \mathcal{C}^T:

  • (x,νx)Ob(CT)\forall (x, \nu^x) \in Ob(\mathcal{C}^T), G(x,νx)=(LR)(x,νx)=L(x)=(Tx,μx)G(x, \nu^x) = (L \circ R)(x, \nu^x) = L(x) = (Tx, \mu_x)
  • fT:(x,νx)(y,νy)Hom(CT)\forall f^T : (x, \nu^x) \rightarrow (y, \nu^y) \in Hom(\mathcal{C}^T), GfT=(LR)(fT)=L(f)=(Tf)TGf^T = (L \circ R)(f^T) = L(f) = (Tf)^T
  • counitatea adjuncției este dată de multiplicarea și unitatea monadei pentru că fT:(x,νx)(y,νy)Hom(CT)\forall f^T : (x, \nu^x) \rightarrow (y, \nu^y) \in Hom(\mathcal{C}^T) ține condiția νxμx=νxTνx\nu^x \circ \mu_x = \nu^x \circ T\nu^x pentru algebra monadei și atunci ϵ(x,νx)=νx:(Tx,μx)(x,νx)\epsilon_{(x, \nu^x)} = \nu^x : (Tx, \mu_x) \rightarrow (x, \nu^x) este un homomorfism care face să comute:

Care ține pentru că indiferent de ce compunere de homomorfism aplicăm obținem următoarele diagrame comutative care sunt echivalente:

Putem verifica imediat compunerile:

νyTfTνx=νyTνyTTf=\nu^y \circ Tf \circ T\nu^x = \nu^y \circ T\nu^y \circ TTf = fνxμx=νyTfμxf \circ \nu^x \circ \mu_x = \nu^y \circ Tf \circ \mu_x
  • comultiplicarea acestei comonade este dată de deltax=Tηx:(Tx,μx)(TTx,μTx)delta_x = T\eta_x : (Tx, \mu_x) \rightarrow (TTx, \mu_{Tx}) este un homomorfism care face să comute:

Care ține pentru că indiferent de ce compunere de homomorfism aplicăm obținem următoarele diagrame comutative care sunt echivalente:

Putem verifica imediat compunerile pentru că ηyf=Tfetax\eta_y \circ f = Tf \circ eta_x:

μTyTTTfTTηx=μTyTTηyTTf=\mu_{Ty} \circ TTTf \circ TT\eta_{x} = \mu_{Ty} \circ TT\eta_{y} \circ TTf = TηyTfμx=TTfTηxμxT\eta_{y} \circ Tf \circ \mu_x = TTf \circ T\eta_{x} \circ \mu_x