Scala Callback Pyramid of Doom

5

Eu gostaria de solicitar alguns princípios gerais de design e melhores práticas para evitar a criação de um retorno de chamada pirâmide da destruição particularmente na linguagem Scala .

Considere o seguinte snippet de código rudimentar e imaginário:

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)
}

Como você pode reescrever isso para evitar / eliminar a pirâmide de retorno do efeito doom?

    
por Jonathan Neufeld 04.11.2016 / 22:12
fonte

1 resposta

3

Antes de tudo, incentivo você a experimentar sem o Eithers , se possível. Futures já codifica um estado de erro, e isso é suficiente na grande maioria dos casos do mundo real. Isso leva você a uma única mônada, que permite usar para compreensões como essa (fiz alguns mods para tornar o exemplo auto-contido e compilável):

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
  }
}

Outra recomendação é pensar em correspondência de padrões como último recurso. É uma ferramenta muito genérica, e é por isso que muitos programadores funcionais fazem isso como uma droga, mas também faz com que seja uma espécie de instrumento contundente. Se você se aprofundar, geralmente há uma ferramenta de finalidade especial mais precisa que pode fazer o trabalho com mais elegância.

Se você insistir em manter o Either , uma coisa que ajudará é usar projeções corretas (agora o padrão em 2.12) e mapeamento de curto-circuito em vez de correspondência de padrões. No entanto, você não obterá a brevidade ideal sem cavar em transformadores monad. Scalaz tem um bom conjunto de ready-made, apesar de seu Either passar pelo símbolo \/ .

Basicamente, o problema aqui é que, para as compreensões, só é possível usar <- em uma mônada. Isso é legal quando você acabou de ter o Future como meu exemplo acima, mas perguntou como <- entre asFuture% e Either monads ao mesmo tempo. A solução é usar EitherT para esmagar os Future e Either em uma grande FutureEither monad e todos estão felizes. Tudo isso soa meio assustador, mas lembre-se, nós só queremos apresentar todas essas coisas de mônadas se e quando simplificar nosso código, e neste caso, eu espero que você concorde:

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
  }
}

Realmente, a parte mais difícil aqui é saber que EitherT existe, que é aplicável para simplificar esse tipo de situação e ser capaz de decifrar a documentação concisa. Mais fácil falar do que fazer, mas descobri que, depois de ver alguns exemplos que me mostraram o que era possível, consegui iterar no meu código até ficar bem perto, mesmo que demorou bastante no início.

    
por 05.11.2016 / 06:53
fonte