ABONAMENTE VIDEO REDACȚIA
RO
EN
NOU
Numărul 150
Numărul 149 Numărul 148 Numărul 147 Numărul 146 Numărul 145 Numărul 144 Numărul 143 Numărul 142 Numărul 141 Numărul 140 Numărul 139 Numărul 138 Numărul 137 Numărul 136 Numărul 135 Numărul 134 Numărul 133 Numărul 132 Numărul 131 Numărul 130 Numărul 129 Numărul 128 Numărul 127 Numărul 126 Numărul 125 Numărul 124 Numărul 123 Numărul 122 Numărul 121 Numărul 120 Numărul 119 Numărul 118 Numărul 117 Numărul 116 Numărul 115 Numărul 114 Numărul 113 Numărul 112 Numărul 111 Numărul 110 Numărul 109 Numărul 108 Numărul 107 Numărul 106 Numărul 105 Numărul 104 Numărul 103 Numărul 102 Numărul 101 Numărul 100 Numărul 99 Numărul 98 Numărul 97 Numărul 96 Numărul 95 Numărul 94 Numărul 93 Numărul 92 Numărul 91 Numărul 90 Numărul 89 Numărul 88 Numărul 87 Numărul 86 Numărul 85 Numărul 84 Numărul 83 Numărul 82 Numărul 81 Numărul 80 Numărul 79 Numărul 78 Numărul 77 Numărul 76 Numărul 75 Numărul 74 Numărul 73 Numărul 72 Numărul 71 Numărul 70 Numărul 69 Numărul 68 Numărul 67 Numărul 66 Numărul 65 Numărul 64 Numărul 63 Numărul 62 Numărul 61 Numărul 60 Numărul 59 Numărul 58 Numărul 57 Numărul 56 Numărul 55 Numărul 54 Numărul 53 Numărul 52 Numărul 51 Numărul 50 Numărul 49 Numărul 48 Numărul 47 Numărul 46 Numărul 45 Numărul 44 Numărul 43 Numărul 42 Numărul 41 Numărul 40 Numărul 39 Numărul 38 Numărul 37 Numărul 36 Numărul 35 Numărul 34 Numărul 33 Numărul 32 Numărul 31 Numărul 30 Numărul 29 Numărul 28 Numărul 27 Numărul 26 Numărul 25 Numărul 24 Numărul 23 Numărul 22 Numărul 21 Numărul 20 Numărul 19 Numărul 18 Numărul 17 Numărul 16 Numărul 15 Numărul 14 Numărul 13 Numărul 12 Numărul 11 Numărul 10 Numărul 9 Numărul 8 Numărul 7 Numărul 6 Numărul 5 Numărul 4 Numărul 3 Numărul 2 Numărul 1
×
▼ LISTĂ EDIȚII ▼
Numărul 76
Abonament PDF

Monad Transformers

Kovács György
Scala Developer @ Itiviti



PROGRAMARE


Cu toate că pare un concept foarte complicat, Monad Transformer se bazează pe concepte de bază: în mod specific, functorul [7] și monada [8]. Aceste concepte sunt esențiale pentru orice programator funcțional. Având cunoștințele necesare, nu este dificil de înțeles ce este un Monad Transformer și în ce situații s-ar putea folosi.

Folosim monade pentru a controla efectele într-un program pur funcțional. Cu aceste monade, putem să înlănțuim mai multe funcții care produc un anumit tip de efect, de exemplu IO într-un mod riguros și type-safe.

Un program în general produce mai multe efecte, precum comunicarea cu un proces extern, erori și modificarea unei stări interne. Toate aceste efecte sunt suprapuse și înlănțuirea lor cu alte procese poate fi foarte complexă și greu de înțeles. Scopul Monad Transformerilor este stivuirea monadelor în cazuri în care producem mai multe efecte diferite. Astfel, înlănțuirea lor se simplifică.

Problema

Această structură poate fi întâlnită uneori în cod: ceva.map(_.map(f)) . De exemplu dacă avem un Option care conține o listă de numere și vrem să incrementăm toate numerele:

numberListOption.map(_.map(_ + 1))

În exemplul de sus avem doi functori imbricați. Ar fi o soluție mai elegantă să avem un map special, care s-ar comporta ca două mapuri din exemplul de mai sus. Din fericire, putem să compunem functorii folosind funcția compose:

def compose[F[_]: Functor, 
            G[_]: Functor]:      
            Functor[F[G[_]]] 

val optionList = Functor[Option].
  compose(Functor[List]) 

val data: Option[List[Int]] = 
  Some(List(1, 2, 3)) 

optionList.map(data)(_ + 1) 
// == Some(List(2, 3, 4))

După aplicare, obținem un singur functor, și codul de mai sus devine: ceva.map(f), având aceiași funcționalitate ca exemplul original.

Doi functori se pot compune, însă acest lucru nu este posibil la monade [5]. Totuși există o cale prin care putem combina mai multe tipuri de monade, dacă creăm o structură nouă. Acea structură este desigur Monad Transformer.

Din moment ce nu se poate generaliza combinarea monadelor, nu există o variantă de monad transformer general, ci e nevoie să fie implementat câte o versiune de monad transformer pentru fiecare monadă pe care o vrem să folosim. Din fericire, există implementări pentru toate monadele definite în cats [4], și le putem combina fără probleme.

Structura

Un monad transformer este o monadă care conține o monadă. Zicem că modifică o monadă, sau adaugă un efect la o monadă. De exemplu, aceasta este definiția monad transformerului EitherT din librăria cats:

final case class EitherT[F[_], A, B]
  (value: F[Either[A, B]])

Unde F[_] este o monadă. De regulă, F reprezintă ori alt Monad Transformer, ori monada Id sau IO.

Dacă alegem ca F să fie Id, EitherT[Id, A, B] este echivalent cu Either[A, B] prin definiția tipului Id din cats: type Id[A] = A.

De obicei IO este monada de bază, care este extinsă cu monad transformere.

EitherT[IO, Error, A] înseamnă că avem un efect care poate produce erori. Putem extinde mai mult, dacă de exemplu avem și un efect de colectarea logurilor ca o listă de stringuri:

WriterT[EitherT[IO, Error, ?], List[String], A]

Această reprezentare cu semnul întrebării în Scala este posibilă doar folosind pluginul Kind Projector [9].

Primul parametru din WriterT trebuie să fie F[_], așa că va trebui să aplicăm parțial constructorul lui EitherT. Un alt fel de a exprima acest lucru ar fi astfel:

type G[A] = EitherT[IO, Error, A] WriterT[G, List[String], A]

S-ar putea considera, practic vorbind, că un monad transformer este doar un wrapper peste tipurile de genul F[G[A]], unde F și G sunt monade, în primul exemplu F[Either[A, B]] devine EitherT[F, A, B]. Din convenție, un monad transformer se termină cu litera mare T. Așa se diferențiază varianta monad transformer a unei monade. De exemplu, Either și EitherT, State și StateT etc.

Nu există variantă de monad transformer pentru IO. De aceea, el este de obicei monada de bază.

Utilizare

Dacă programăm, probabil codul nostru produce efecte. Un mod de a controla efectele este folosirea monadelor. De exemplu, monadele IO, Option, Either etc. . Putem să avem mai multe funcții care produc efecte, și putem să le conectăm folosind monade având siguranța respectării tipurilor.

Însă codul poate produce mai multe efecte deodată. De exemplu, unele computații pot comunica cu procese externe și pot returna valori, dar și erori. Așa un proces ar avea o semnătură precum IO[Either[Error, Value]].

Să presupunem că avem două funcții:

def getUserByName(name: String): IO[User]
def getEmployerOfUser(user: User): IO[Employer]

Pentru a conecta cele două funcții, am putea folosi flatMap:

getUserByName(name).flatMap(getEmployerOfUser)

Sau am putea folosi:

for {
    user    ← getUserByName(name)
    employer ← getEmployerOfUser(user)
} yield employer

Logica este simplă și ușor de urmărit.

Dar dacă putem avea și erori? Semnăturile funcțiilor de mai sus se vor schimba:

def getUserByName(name: String): 
       IO[Either[Error, User]]
def getEmployerOfUser(user: User):     
       IO[Either[Error, Employer]]

Combinația lor folosind flatMap:

getUserByName(name).flatMap { 
   case e@Left(_) ⇒ IO.pure(e)
   case Right(a) ⇒ getEmployerOfUser(a)
}

și folosind:

for {
   userEither ← getUserByName(name) 
   employer    ← userEither match {
     case e@Left(_) ⇒ IO.pure(e)
     case Right(a) ⇒ getEmployerOfUser(a)
    }
} yield employer

Logica de înlănțuire devine mai complexă. Codul se complică, mai ales dacă mai avem un efect sau dacă avem mai multe funcții. Totodată este mai greu de văzut logica de business. Ceea ce este un mare dezavantaj, mai ales pentru programatorii care folosesc limbaje funcționale și se mândresc de abilitatea de a crea limbaje specifice de domeniu (DSL).

O soluție care rezolvă complexitatea înlănțuirii în exemplul nostru ar fi folosirea Monad Transformerelor. Vom folosi EitherT[IO, Error, A]. Pentru a face codul un pic mai lizibil, vom folosi un type alias:

type Result[A] = EitherT[IO, Error, A]

Astfel funcțiile noastre se transformă în:

def getUserByName(name: String): Result[User]
def getEmployerOfUser(user: User): Result[Employer]

Și putem să le înlănțuim ca înainte:

getUserByName(name).flatMap(getEmployerOfUser)

sau

for {
    user ← getUserByName(name) 
    employer ← getEmployerOfUser(user)
} yield employer

Dezavantaje

Adevărul este că Monad Transformer poate fi o unealtă folositoare, dar vine cu costuri.

Un cost ar fi performanța. Creăm și distrugem obiecte la fiecare operație. Acest lucru înseamnă inevitabil că avem un cost de memorie dar și de calcul.

Un alt dezavantaj este că un monad transformer poate complica înlănțuirea funcțiilor în cazuri în care funcțiile nu au toate același tipuri de efecte. Să considerăm exemplul cu cele două funcții de mai sus: getUserByName, getEmployerOfUser. În exemplul de sus, am presupus implicit că avem un nume cu care apelăm getUserByName. Dar, dacă citim numele de la tastatură sau primim un json cu nume, vom avea o funcție getName:

def getName: IO[String]

Problema este că rezultatul funcției getName este un String, nu un Either. De aceea, nu putem pur și simplu să îl legăm de celelalte funcții. Direct legat de acest aspect, există metoda liftF. Lift ne permite să transformăm orice functor în monad transformerul nostru. Mai jos avem exemplul pentru EitherT:

def liftF[F[_]: Functor, A, B](fb: F[B]): EitherT[F, A, B]

Astfel, dacă includem getName, for comprehension devine:

for {
    name    ← EitherT.liftF(getName) 
    user    ← getUserByName(name) 
    employer ← getEmployerOfUser(user)
} yield employer

Structura aceasta nu pare foarte complicată, dar se adaugă complexitate și se alterează performanța. Acest aspect s-ar vedea mai ales în situația în care avem mai multe funcții care au doar efect IO, și unele care au și eroare. Dacă vrem să folosim monad transformere atunci, va trebui să liftăm toate acele funcții. Acest lucru nu ar fi elegant, dar ce e mai important este că ajungem înapoi la complexitatea înlănțuirii pe care am vrut să o evităm de la început.

Concluzii

Monad transformer poate fi o unealtă foarte folositoare pentru orice programator funcțional, cât timp acel programator știe când e bine să fie folosit, și când nu e bine. Se folosește cel mai eficient în zone localizate de cod, avantajând clar lizibilitatea codului. Dar se evită în cazuri mai complexe.

Nu se recomandă folosirea lor în situația în care este prioritară performanța, la fel ca atunci când avem un număr de efecte mai mare de doi sau trei, pentru că există riscul apariției de funcții care produc doar un singur sau două efecte. Acest lucru înseamnă că vor apăra o mulțime de liftFuri în cod.

Monad transformer este un concept foarte puternic și folositor, în contextul în care avem funcții similare care produc același efecte, iar claritatea codului este mai importantă decât viteza execuției.

Referințe

  1. Monads in Haskell
  2. B. Sullivan, D. Stewart and J. Goerzen - Real World Haskell
  3. Monad Transformers - Wikipedia
  4. Functional Programming abstractions for Scala
  5. Gabriele Petronella - Monad Transformers down to earth
  6. Type Class - Wikipedia
  7. Bartos Milewski's Programming Cafe - Functors
  8. Bartos Milewski's Programming Cafe - Monads for the Curious Programmer, Part 1
  9. Underscore - Type lambdas and kind projector

NUMĂRUL 149 - Development with AI

Sponsori

  • Accenture
  • BT Code Crafters
  • Accesa
  • Bosch
  • Betfair
  • MHP
  • BoatyardX
  • .msg systems
  • P3 group
  • Ing Hubs
  • Cognizant Softvision
  • Colors in projects

Kovács György a mai scris