2015-03-05 44 views
7

Say mam trzy funkcje dostępu do bazy danych foo, bar i baz że każdy może powrócić Option[A] gdzie A jest jakiś wzór klasa, a połączenia są od siebie zależne.Unikanie głęboko zagnieżdżone kaskady opcję w Scala

Chciałbym wywołać funkcje sekwencyjnie iw każdym przypadku, zwrócić odpowiedni komunikat o błędzie, jeśli wartość nie zostanie znaleziona (None).

Mój bieżący kod wygląda następująco:

Input is a URL: /x/:xID/y/:yID/z/:zID 

foo(xID) match { 
    case None => Left(s"$xID is not a valid id") 
    case Some(x) => 
    bar(yID) match { 
     case None => Left(s"$yID is not a valid id") 
     case Some(y) => 
     baz(zID) match { 
      case None => Left(s"$zID is not a valid id") 
      case Some(z) => Right(process(x, y, z)) 
     } 
    } 
} 

Jak widać, kod jest źle zagnieżdżone.

Jeśli zamiast tego używam for ze zrozumieniem, nie mogę podać konkretne komunikaty o błędach, bo nie wiem, który to etap nie powiodło się:

(for { 
    x <- foo(xID) 
    y <- bar(yID) 
    z <- baz(zID) 
} yield { 
    Right(process(x, y, z)) 
}).getOrElse(Left("One of the IDs was invalid, but we do not know which one")) 

Jeśli używam map i getOrElse, ja skończyć z kodem prawie jak zagnieżdżony jako pierwszy przykład.

Czy jest to lepszy sposób ułożenia struktury, aby uniknąć zagnieżdżenia, zezwalając na określone komunikaty o błędach?

Odpowiedz

0

wymyśliłem tego rozwiązania (w oparciu o @ rozwiązania Rexa i jego uwagi):

def ifTrue[A](boolean: Boolean)(isFalse: => A): RightProjection[A, Unit.type] = 
    Either.cond(boolean, Unit, isFalse).right 

def none[A](option: Option[_])(isSome: => A): RightProjection[A, Unit.type] = 
    Either.cond(option.isEmpty, Unit, isSome).right 

def some[A, B](option: Option[A])(ifNone: => B): RightProjection[B, A] = 
    option.toRight(ifNone).right 

one wykonać następujące czynności:

  • ifTrue jest używany, gdy funkcja zwraca Boolean, z true jako przypadek "sukcesu" (np. isAllowed(userId)).Zwraca on w rzeczywistości Unit, więc powinien być używany jako _ <- ifTrue(...) { error } w zrozumieniu....
  • jest używany, gdy funkcja zwraca wartość Option z None będącą przypadkiem "sukcesu" (np .: findUser(email) do tworzenia kont z unikatowymi adresami e-mail). W rzeczywistości zwraca Unit, więc powinno być używane jako _ <- none(...) { error } w zrozumieniu for.
  • some jest używany, gdy funkcja zwraca wartość Option z Some() będącą przypadkiem "sukcesu" (np .: findUser(userId) dla GET /users/userId). Zwraca zawartość Some: user <- some(findUser(userId)) { s"user $userId not found" }.

Są one używane w for zrozumieniem:

for { 
    x <- some(foo(xID)) { s"$xID is not a valid id" } 
    y <- some(bar(yID)) { s"$yID is not a valid id" } 
    z <- some(baz(zID)) { s"$zID is not a valid id" } 
} yield { 
    process(x, y, z) 
} 

ta zwraca Either[String, X] gdzie String jest komunikat o błędzie, a X jest wynikiem nazywając process.

2

Zamiast używać Option, zamiast tego użyłbym Try. W ten sposób masz monadyczną kompozycję, którą chciałbyś zmieszać z możliwością zatrzymania błędu.

def myDBAccess(..args..) = 
thingThatDoesStuff(args) match{ 
    case Some(x) => Success(x) 
    case None => Failure(new IdError(args)) 
} 

jestem przy założeniu, że w powyższym nie faktycznie sterować funkcjami i nie byłaby im dać ci nie- Option. Jeśli tak, po prostu zamień Try.

+0

I złożył bilet byłaby kod powrotu 'Either', ale nie miałem czasu, aby nad nim pracować. Mamy wiele takich przypadków. – Ralph

+0

@Ralph Oba też by działały. Lubię "Try", ponieważ mówi "Sukces" i "Niepowodzenie". Użycie polecenia "Spróbuj" nie oznacza, że ​​musisz rzucić wyjątek, ale możesz mieć wyjątek alokujący nieprzydzielony stos, który nie spowalnia procesu. – wheaties

+0

@wheaties - Trochę niebezpiecznie jest mieć niepotrzebne wyjątki, ponieważ jeśli zostaną rzucone, nie masz pojęcia, skąd pochodzą. Lepiej, jeśli to możliwe, przynajmniej użyć wzoru pożyczki podczas pracy z nimi, np. 'getAStacklessException (params) {e => ???/* Nie przechowuj w dowolnym miejscu ani nie korzystaj z kontraktów terminowych! * /} '. –

7

Możesz uzyskać dostęp do pętli for za pomocą prawych rzutów.

Jest to wciąż trochę niezgrabne, ale ma tę zaletę, że jest częścią standardowej biblioteki.

Wyjątki to kolejna droga, ale spowalniają one działanie lot, jeśli przypadki awarii są częste. Używałbym tego tylko wtedy, gdyby niepowodzenie było naprawdę wyjątkowe.

Możliwe jest również stosowanie zwrotów innych niż lokalne, ale jest to trochę niewygodne dla tej konkretnej konfiguracji. Myślę, że trafne są prawidłowe projekcje Either. Jeśli naprawdę lubisz pracować w ten sposób, ale nie lubisz stawiać na całym miejscu, są różne miejsca, w których możesz znaleźć "prawą stronniczość", która domyślnie będzie działać jak prawidłowa projekcja (np. ScalaUtils, Scalaz, itp.).

+0

To prawie dokładnie rozwiązanie, które wymyśliłem po zabawie z kodem :-). – Ralph

+0

Mój konwerter wygląda następująco: "def right [A, B] (opcja: Option [A]) (ifNone: B): RightProjection [B, A] = option.toRight (ifNone) .right' – Ralph

+0

Prawdopodobnie chcesz" ifNone: => B', więc nie musisz budować przypadku awarii za każdym razem, nawet jeśli nie ma awarii. –

1

Wiem, że odpowiedź na to pytanie została udzielona jakiś czas temu, ale chciałem podać alternatywę dla zaakceptowanej odpowiedzi.

Biorąc pod uwagę, że w twoim przykładzie trzy Option s są niezależne, możesz traktować je jako Applicative Funktory i używać ValidatedNel od Cats, aby uprościć i połączyć obsługę nieszczęśliwej ścieżki.

względu na kod:

import cats.data.Validated.{invalidNel, valid} 

    def checkOption[B, T](t : Option[T])(ifNone : => B) : ValidatedNel[B, T] = t match { 
    case None => invalidNel(ifNone) 
    case Some(x) => valid(x) 

    def processUnwrappedData(a : Int, b : String, c : Boolean) : String = ??? 

    val o1 : Option[Int] = ??? 
    val o2 : Option[String] = ??? 
    val o3 : Option[Boolean] = ??? 

Następnie można replikować uzyskać to, co chcesz z:

//import cats.syntax.cartesian._ 
( 
    checkOption(o1)(s"First option is not None") |@| 
    checkOption(o2)(s"Second option is not None") |@| 
    checkOption(o3)(s"Third option is not None") 
) map (processUnwrappedData) 

Takie podejście pozwoli na agregację awarie, co nie było możliwe w roztworze (jak użycie do zrozumienia powoduje wymuszenie oceny sekwencyjnej). Więcej przykładów i dokumentacji można znaleźć pod numerami: here i here.

Wreszcie to rozwiązanie wykorzystuje Koty Validated ale może łatwo zostać przetłumaczony na Scalaz Validation