Scala Callback Pyramid of Doom

5

Vorrei sollecitare alcuni principi generali di progettazione e best practice per evitare di creare una callback piramide di sventura in particolare nella lingua Scala .

Considera il seguente snippet di codice rudimentale e immaginario:

val response: Future[Either[ApiError, Option[CogWidget]]] = services.findCogWidget("Some-UUID-42")
response.flatMap {
  case Right(cogWidget) =>
    val validationResponse: Future[Either[ApiError, ValidationResult]] = services.validateCogWidget(cogWidget)

    validationResponse.flatMap {
      case Right(validationResult) =>
        val registerResult: Future[Either[ApiError, RegistrationTicket]] = services.cogWidgetRegisterRequest(cogWidget, validationResult)
        registerResult.map {
          case Right(ticket) =>
            Some(":D" -> ticket)

          case _ => None
        }

      case x @ Left(_) => Future.successful(x)
    }      

  case x @ Left(_) => Future.successful(x)
}

Come potresti riscriverlo per evitare / eliminare l'effetto della piramide di callback?

    
posta Jonathan Neufeld 04.11.2016 - 22:12
fonte

1 risposta

3

Prima di tutto, ti incoraggio a provarlo senza Eithers se possibile. Futures codifica già uno stato di errore, e questo è sufficiente nella maggior parte dei casi reali. Questo ti porta a una singola monade, che ti permette di usare delle comprensioni come questa (ho fatto alcune mod per rendere l'esempio autonomo e compilabile):

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

object PyramidOfDoomNoEither {
  case class Ticket()
  def findCogWidget(cog: String): Future[Option[Int]] =
    Future.successful(Some(1))
  def validateCogWidget(widget: Option[Int]): Future[Boolean] =
    Future.successful(true)
  def cogWidgetRegisterRequest(widget: Option[Int], valid: Boolean): Future[Option[Ticket]] =
    Future.successful(Some(Ticket()))

  def processWidget(cog: String): Future[Option[Ticket]] = {
    for {
      widget <- findCogWidget(cog)
      valid  <- validateCogWidget(widget)
      ticket <- cogWidgetRegisterRequest(widget, valid)
    } yield ticket
  }
}

Un'altra raccomandazione è quella di pensare alla corrispondenza dei modelli come ultima risorsa. È uno strumento molto generico, ed è per questo che penso che molti programmatori funzionali si comportino come un farmaco, ma lo rendono anche una sorta di strumento contundente. Se scavi più a fondo, di solito c'è uno strumento per scopi speciali più preciso che può svolgere il lavoro in modo più elegante.

Se insisti a mantenere il Either , una cosa che ti aiuterà sarà usare le proiezioni giuste (ora il default in 2.12) e la mappatura del cortocircuito invece della corrispondenza del modello. Tuttavia, non otterrete la brevità ideale senza scavare nei trasformatori monad. Scalaz ha un bel set di quelli già pronti, anche se il loro Either fastidiosamente passa dal simbolo \/ .

Fondamentalmente, il problema qui è che le comprensioni possono solo <- su una monade. È bello quando hai appena Future come il mio esempio sopra, ma hai chiesto come <- su entrambe le Future e le Either monad allo stesso tempo. La soluzione è usare EitherT per eliminare il Future e Either in una grande FutureEither monad, e tutti sono contenti. Tutto questo sembra un po 'spaventoso, ma ricorda, vogliamo solo introdurre tutte queste cose monad se e quando semplifica il nostro codice, e in questo caso, spero tu sia d'accordo:

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scalaz.EitherT
import scalaz.std.scalaFuture.futureInstance
import scalaz.{\/,-\/,\/-}

object PyramidOfDoom {
  case class Ticket()
  def findCogWidget(cog: String): Future[String \/ Option[Int]] =
    Future.successful(\/-(Some(1)))
  def validateCogWidget(widget: Option[Int]): Future[String \/ Boolean] =
    Future.successful(\/-(true))
  def cogWidgetRegisterRequest(widget: Option[Int], valid: Boolean): Future[String \/ Option[Ticket]] =
    Future.successful(\/-(Some(Ticket())))

  def processWidget(cog: String): Future[String \/ Option[Ticket]] = {
    val ticket = for {
      widget <- EitherT(findCogWidget(cog))
      valid  <- EitherT(validateCogWidget(widget))
      ticket <- EitherT(cogWidgetRegisterRequest(widget, valid))
    } yield ticket
    ticket.run
  }
}

In realtà, la parte più difficile qui è sapere che EitherT esiste, che è applicabile nella semplificazione di questo tipo di situazione e nella capacità di decifrare la documentazione concisa. Più facile a dirsi che a farsi, ma ho scoperto che una volta che ho visto alcuni esempi che mi hanno mostrato ciò che era possibile, sono riuscito a scorrere il codice finché non si è avvicinato, anche se all'inizio ci sono voluti un po '.

    
risposta data 05.11.2016 - 06:53
fonte

Leggi altre domande sui tag