2013-10-08 10 views
10

ho una collezione di metodi che restituiscono tipi diversi:Combinando Futures, eithers e opzioni in espressioni for

Either[ErrorResponse, X] 
Future[Either[ErrorResponse, X]] 
Option[ErrorResponse] 

Questi metodi hanno bisogno il risultato di un metodo precedente per svolgere il loro calcolo. I metodi:

type Parameters = Map[String, String] 

// allows me to flatmap on an either 
implicit def toRightProjection[Failure, Success](e: Either[Failure, Success]) = 
    e.right 

// converts anything to a future 
implicit def toFuture[T](t: T) = 
    Future.successful(t) 

// retrieves the request paramters from the given request 
def requestParameters(request: RequestHeader): Either[ErrorResponse, Parameters] = ??? 

// retrieves the response type from the given parameters 
def responseType(p: Parameters): Either[ErrorResponse, String] = ??? 

// retrieves the client id from the given parameters 
def clientId(p: Parameters): Either[ErrorResponse, String] = ??? 

// retrieves the client using the given client id 
def client(clientId: String): Future[Either[ErrorResponse, Client]] = ??? 

// validates the response type of the client 
def validateResponseType(client: Client, responseType: String): Option[ErrorResponse] = ??? 

posso filo insieme con quanto segue per comprensione (nota che ho annotato alcuni tipi di chiarire il contenuto di parti specifiche del calcolo).

val result: Either[ErrorResponse, Future[Either[ErrorResponse, Client]]] = 
    for { 
    parameters <- requestParameters(request) 
    clientId <- clientId(parameters) 
    responseType <- responseType(parameters) 
    } yield { 
    val result: Future[Either[ErrorResponse, Either[ErrorResponse, Client]]] = 
     for { 
     errorOrClient <- client(clientId) 
     client <- errorOrClient 
     } yield validateResponseType(client, responseType).toLeft(client) 

    result.map(_.joinRight) 
    } 

val wantedResult: Future[Either[ErrorResponse, Client]] = 
    result.left.map(Future successful Left(_)).merge 

Il codice sopra riportato è piuttosto disordinato e ritengo che ciò possa essere fatto in modo diverso. Ho letto di monadi e trasformatori monadi. Il concetto di quelli è molto nuovo per me e non riesco a capirlo.

La maggior parte degli esempi riguarda solo due tipi di risultati: Either[X, Y] e Future[Either[X, Y]]. Trovo ancora molto difficile piegare la mente intorno ad esso.

Come posso scrivere un testo piacevole e pulito per la comprensione che sostituisce il precedente?

Qualcosa di simile sarebbe fantastico (non sono sicuro se questo è ancora possibile):

val result: Future[Either[ErrorResponse, Client]] = 
    for { 
    parameters <- requestParameters(request) 
    clientId <- clientId(parameters) 
    responseType <- responseType(parameters) 
    client <- client(clientId) 
    _ <- validateResponseType(client, responseType) 
    } 
+0

maggiore documentazione su questo argomento [trasformatori Monade e monadi libero] (https://github.com/EECOLOR/scala-clean-code-patterns/blob/master/src/main/scala/processes/ README.md) – EECOLOR

risposta

12

OK, qui è il mio tentativo di questo:

import scalaz._, Scalaz._ 

implicit val futureMonad = new Monad[Future] { 
    override def point[A](a: ⇒ A): Future[A] = future(a) 

    override def bind[A, B](fa: Future[A])(f: A ⇒ Future[B]): Future[B] = 
    fa.flatMap(f) 
} 

import EitherT._ 
val result: EitherT[Future, ErrorResponse, Client] = 
    for { 
    parameters <- fromEither(Future(requestParameters(request))) 
    clientId <- fromEither(Future(clientId(parameters))) 
    responseType <- fromEither(Future(responseType(parameters))) 
    client <- fromEither(client(clientId)) 
    response <- fromEither[Future, ErrorResponse, Client](Future(validateResponseType(client, responseType).toLeft(client))) 
    } yield response 

val x: Future[\/[ErrorResponse, Client]] = result.run 
+0

L'approccio più bello che ho visto finora. Mi chiedo, ha un impatto significativo sulle prestazioni? – Tvaroh

+1

Non penso che ci siano problemi di prestazioni significativi qui, dipende da cosa è "significativo" per la tua app. Una cosa che aumenterà il tempo di compilazione sta importando tutto lo scalaz come nel mio esempio. È meglio restringere quelle importazioni per aiutare a compilare il tempo. –

0

Non c'è modo veramente pulito di fare comprensioni su più tipi monade. In ScalaZ c'è OptionT che potrebbe aiutare, vale la pena provarlo. Potresti anche trasformare i tuoi Either in Opzioni o viceversa ed essere in grado di avere un po 'meno di confusione. Una terza opzione potrebbe essere quella di creare il proprio tipo di wrapper che combini Future [Either | Option] nella stessa monade e quindi comprenderlo.

Per riferimento ho chiesto aboutish la stessa domanda sul gioco quadro mailing list di recente e ottenuto alcuni buoni collegamenti nelle risposte: https://groups.google.com/d/topic/play-framework/JmCsXNDvAns/discussion

2

scala.util.Either non è una Monade, ma la libreria scalaz ha una grande implementazione.

object Test extends ToIdOps { 

import scalaz.{ Monad, Functor, EitherT, \/, -\/, \/- } 
import scalaz.syntax.ToIdOps 

implicit val FutureFunctor = new Functor[Future] { 
    def map[A, B](a: Future[A])(f: A => B): Future[B] = a map f 
} 

implicit val FutureMonad = new Monad[Future] { 
    def point[A](a: => A): Future[A] = Future(a) 
    def bind[A, B](fa: Future[A])(f: (A) => Future[B]): Future[B] = fa flatMap f 
} 
def someMethod: Future[\/[InvalidData, ValidData]] = { 
    // things went well 
    ValidData.right // this comes from ToIdOps 
    // or something went wrong 
    InvalidData.left 
} 
def someOtherMethod: Future[\/[InvalidData, ValidData]] // same as above 
val seq = for { 
    d <- EitherT(someMethod) 
    y <- EitherT(someOtherMethod) 
} yield { // whatever} 
// you can now Await.result(seq.run, duration) 
// you can map or match etc with \/- and -\/ 
val result = seq.run map { 
    case -\/(left) => // invalid data 
    case \/-(right) => // game on 
} 
}