Am început să lucrez cu Scala acum câteva luni. Unul dintre conceptele pe care am avut cele mai multe dificultăți să le înțeleg este Either monadă. Deci, am decis să mă joc cu el și să-i înțeleg mai bine puterea.

În această poveste împărtășesc ceea ce am învățat, sperând să ajut programatorii să se apropie de acest frumos limbaj.

Fie monadă

Either este una dintre cele mai utile monade din Scala. Dacă vă întrebați ce este o monadăEi bine … Nu pot intra în detalii aici, poate într-o poveste viitoare!

Imagina Either ca o cutie care conține un calcul. Lucrați în interiorul acestei cutii, până când decideți să obțineți rezultatul din ea.

În acest caz specific, Either caseta poate avea două „forme”. Poate fi (fie) a Left sau a Right, în funcție de rezultatul calculului din interiorul acestuia.

Vă aud auzindu-vă: „OK și pentru ce este util?”

Răspunsul obișnuit este: tratarea erorilor.

Putem pune un calcul în Either, și faceți-o un Left în caz de erori sau a Right care conține un rezultat în caz de succes. Utilizarea Left pentru erori și Right căci succesul este o convenție. Să înțelegem acest lucru cu un cod!


În acest fragment definim doar un Either variabil.

Îl putem defini ca un Right care conține o valoare validă sau ca Left care conține o eroare. De asemenea, avem un calcul care returnează un Either, adică poate fi un Left sau a Right. Simplu, nu-i așa?

Proiecție dreaptă și stângă

Odată ce avem calculul în casetă, este posibil să dorim să obținem valoare din aceasta. Sunt sigur că vă așteptați să apelați un .get pe Either și extrageți rezultatul.

Nu este atât de simplu.

Gândește-te la asta: îți pui calculul în Either, dar nu știți dacă a avut ca rezultat un Left sau a Right. Deci, ce ar trebui să .get sunati inapoi? Eroarea sau valoarea?

Acesta este motivul pentru care pentru a obține rezultatul ar trebui să faceți o presupunere cu privire la rezultatul calculului.

Iată unde proiecție intră în joc.

Începând de la un Either, puteți obține un RightProjection sau a LeftProjection. Primul înseamnă că presupuneți că calculul a avut ca rezultat un Right, acesta din urmă într-un Left.

Știu, știu … poate fi puțin confuz. Este mai bine să îl înțelegeți cu un anumit cod. Dupa toate acestea, codul spune întotdeauna adevărul.


Asta este. Rețineți că atunci când încercați să obțineți rezultatul de la un RightProjection, dar este un Left, obțineți o excepție. Același lucru este valabil și pentru un LeftProjection și aveți un Right.

Interesant este că poți să faci o hartă pe proiecții. Aceasta înseamnă că puteți spune: „presupuneți că este un drept: faceți asta cu el”, lăsând Left neschimbată (și invers).

De la opțiune la oricare

Option este un alt mod obișnuit de a face față valorilor nevalide.

Un Option poate avea o valoare sau poate fi gol (valoarea ei este Nothing). Pun pariu că ai observat o asemănare cu Either… Este chiar mai bine, pentru că putem transforma un Option într-un Either! Timp de cod!


Este posibil să se transforme un Option la o Left sau a Right. Partea rezultată a Either va conține valoarea Option dacă este definit. Misto. Așteaptă un minut … Ce se întâmplă dacă Option este gol? Avem cealaltă parte, dar trebuie să specificăm ce ne așteptăm să găsim în ea.

Pe dos

Either este magie, suntem cu toții de acord cu asta. Deci, decidem să-l folosim pentru calculele noastre incerte. Un scenariu tipic atunci când se face programare funcțională este maparea unei funcții pe un List de elemente, sau pe un Map. Să o facem cu noul nostru proaspăt Either-calcul motorizat …


Huston, avem o „problemă” (ok, nu este o problemă MARE, dar este cam incomodă). Ar fi mai bine să aveți colecția în interiorul Either decât o mulțime de Either în interiorul colecției. Putem lucra la asta.

Listă

Sa incepem cu List. Mai întâi ne motivăm, apoi ne putem juca cu codul.

Trebuie să extragem valoarea din Either, pune-l în List, și puneți lista în interiorul unui Either. Bun imi place.

Ideea este că putem avea un Left sau a Right, deci trebuie să ne ocupăm de ambele cazuri. Până când vom găsi o Right, îi putem pune valoarea într-un nou List. Procedăm astfel acumulând fiecare valoare în nou List.

În cele din urmă vom ajunge la sfârșitul anului List de Either, adică avem un nou List conținând toate valorile. Îl putem împacheta într-un Right și am terminat. Acesta a fost cazul în care calculul nostru nu a returnat un Error în interiorul unui Left.

Dacă se întâmplă acest lucru, înseamnă că ceva nu a funcționat în calculul nostru, astfel încât să putem returna Left cu Error. Avem logica, acum avem nevoie de cod.


Hartă

Lucrul la Map este destul de simplu odată ce am făcut temele pentru List (în ciuda faptului că trebuie să fie generic):

  • Primul pas: transformați Map într-o List de Either care conține tuplul (cheie, valoare).
  • Pasul doi: treceți rezultatul la funcția pe care am definit-o List.
  • Pasul trei: transformați List de tupluri în interiorul Either într-o Map.

Ușor Peasy.


Să devenim clasici: un convertor implicit util

Am introdus Either și am înțeles că este util pentru tratarea erorilor. Ne-am jucat puțin cu proiecțiile. Am văzut cum să trecem de la un Option la un Either. De asemenea, am implementat câteva funcții utile pentru a „extrage” Either din List și Map. Până acum, bine.

Aș dori să închei călătoria noastră în Either monada mergând puțin mai departe. Funcțiile de utilitate pe care le-am definit își fac treaba, dar simt că lipsește ceva …

Ar fi uimitor să facem conversia noastră direct pe colecție. Am avea ceva de genul myList.toEitherList sau myMap.toEitherMap. Mai mult sau mai puțin așa cum facem cu Option.toRight sau Option.toLeft.

Vești bune: o putem face folosind clase implicite!


Folosirea de clase implicite în Scala ne permite să extindem capacitățile unei alte clase.

În cazul nostru, extindem capacitatea de List și Map a „extrage” automat Either. Implementarea conversiei este aceeași pe care am definit-o anterior. Singura diferență este că acum o facem generică. Scala nu este minunat?

Deoarece aceasta poate fi o clasă de utilitate utilă, v-am pregătit un rezumat pe care îl puteți copia și lipi cu ușurință.

object EitherConverter {
  implicit class EitherList[E, A](le: List[Either[E, A]]){
    def toEitherList: Either[E, List[A]] = {
      def helper(list: List[Either[E, A]], acc: List[A]): Either[E, List[A]] = list match {
        case Nil => Right(acc)
        case x::xs => x match {
          case Left(e) => Left(e)
          case Right(v) => helper(xs, acc :+ v)
        }
      }

      helper(le, Nil)
    }
  }

  implicit class EitherMap[K, V, E](me: Map[K, Either[E, V]]) {
    def toEitherMap: Either[E, Map[K, V]] = me.map{
        case (k, Right(v)) => Right(k, v)
        case (_, e) => e
      }.toList.toEitherList.map(l => l.asInstanceOf[List[(K, V)]].toMap)
  }
}

Concluzie

Asta e tot oameni buni. Sper că această scurtă poveste vă poate ajuta să înțelegeți mai bine Either monadă.

Vă rugăm să rețineți că implementarea mea este destul de simplă. Pun pariu că există modalități mai complexe și mai elegante de a face același lucru. Sunt un începător în Scala și îmi place SĂRUT, așa că prefer lizibilitatea în locul complexității (elegante).

Dacă aveți o soluție mai bună, în special pentru clasa de utilități, voi fi bucuros să o văd și să învăț ceva nou! 🙂