Scala цепочка преобразований останавливается при ошибке - PullRequest
0 голосов
/ 02 мая 2018

Я хочу применить последовательность преобразований к строке, но остановлюсь при возникновении ошибки. Это пример более общего шаблона (своего рода шаблон цепочки ответственности или шаблон посетителя)

Если это возможно, я хочу сейчас избегать использования Cats или Scalaz. Если вы знаете, как это сделать на обычном Scala, а также на Cats / Scalaz , я буду рад увидеть код в ответе;)

Итак, мой подход (утверждения в конце кода) , но Он не останавливается при обнаружении ошибки . В основном пропускает выполнение преобразования X раз.

type Error = String

sealed trait Transformer {
  def transform(txt:String) : Either[Error, String]
}

object Transformer1 extends Transformer {
  override def transform(txt: String): Either[Error, String] = Right(s"${txt}_One")
}

object Transformer2 extends Transformer {
  override def transform(txt: String): Either[Error, String] = Right(s"${txt}_Two")
}

object Transformer3 extends Transformer {
  override def transform(txt: String): Either[Error, String] = Right(s"${txt}_Three")
}

object TransformerError extends Transformer {
  override def transform(txt: String): Either[Error, String] = Left("Error!!!!")
}

def transform(txt: String, transformers: Seq[Transformer]): Either[Error, String] =
  transformers.foldLeft(Right(txt):Either[Error, String])( (result, t) =>  result match {
    case Right(txt) => t.transform(txt)
    case error => error
  } )


val tOk = Seq(Transformer1, Transformer2, Transformer3)
val tError = Seq(Transformer1, TransformerError, Transformer3)

assert(transform("Whatever", tOk) == Right("Whatever_One_Two_Three"))
assert(transform("Whatever", tError) == Left("Error!!!!"))

Есть предложения?

Спасибо !!

Ответы [ 3 ]

0 голосов
/ 02 мая 2018

При обработке коллекции, если вы хотите досрочного завершения, вам часто приходится обращаться к рекурсии.

def transform(txt :String
             ,transformers :Seq[Transformer]
             ): Either[Error, String] = transformers match {
  case Seq() => Right(txt)
  case hd +: tl => hd.transform(txt).fold(Left(_), transform(_, tl))
}

Хвосто-рекурсивная версия также возможна, если она немного менее лаконична.

@tailrec
def transform(txt :String
             ,transformers :Seq[Transformer]
             ): Either[Error, String] = transformers match {
  case Seq() => Right(txt)
  case hd +: tl =>
    val rslt = hd.transform(txt)
    if (rslt.isLeft) rslt else transform(rslt.toSeq.head, tl)
}
0 голосов
/ 02 мая 2018

Чистая Скала

Вероятно, самый простой способ сделать ваш код коротким, это использовать оператор return. Возвращает результат из самой внутренней функции , в которой она содержится:

def transform(txt: String, transformers: Seq[Transformer]): Either[Error, String] =
  transformers.foldLeft(Right(txt):Either[Error, String])( (result, t) =>
    result match {
      case Right(txt) => t.transform(txt)
      case error => return error
  } )

Таким образом, оператор return error в этом коде немедленно вернет первый Left, обнаруженный в функции transform.

Кошки

У кошек на самом деле не нужно делать ничего особенного. Он автоматически замыкает определенные вызовы для некоторых монад, потому что монады должны реализовывать tailRecM, а некоторые монады (включая Either) реализуют его ленивым образом, чтобы избежать бесполезных flatMap с.

import cats.implicits._

def transformCats(txt: String, transformers: List[Transformer]): Either[Error, String] = {
  // It seems this is needed to help Scala with type inference.
  type Result[T] = Either[Error, T] 
  // foldLeftM is implemented in terms of tailRecM, 
  // and thus is short-circuiting for Either
  transformers.foldLeftM(txt)((result, tf) => tf.transform(result): Result[String])
}
0 голосов
/ 02 мая 2018

В Scala 2.12, Either смещен вправо, поэтому for-yield справится с задачей.

for {
  v1 <- Transformer1.transform("Whatever")
  v2 <- Transformer2.transform(v1)
  v3 <- Transformer3.transform(v2)
} yield {
  v3
}

оценивается как Right(Whatever_One_Two_Three), а

for {
  v1 <- Transformer1.transform("Whatever")
  v2 <- TransformerError.transform(v1)
  v3 <- Transformer3.transform(v2)
} yield {
  v3
}

оценивается как Left(Error!!!!)

Однако, если вы хотите вернуть результат со всеми примененными преобразованиями, пока не будет достигнута ошибка, то есть

  assert(transform("Whatever", tError) == Right("Whatever_One"))

тогда может сработать следующий рефакторинг функции transform:

  def transform(txt: String, transformers: Seq[Transformer]): Either[Error, String] = {

    type Current = Either[Error, String]
    type Previous = Either[Error, String]

    def foldLeftWithEarlyReturn: Tuple2[Current, Previous] = {
      transformers.foldLeft[Tuple2[Current, Previous]](Right(txt) , Right(txt)){
        (result, t)  => result match {
          case ( Right(txt)  , Right(previousTxt)  )   =>        ( t.transform(txt)  , Right(txt)  )
          case ( Left(error) , Right(previousTxt)  )   => return ( Right(previousTxt), Left(error) )
          case e => e
        }
      }
    }

    if (foldLeftWithEarlyReturn._1.isLeft)
      foldLeftWithEarlyReturn._2 // this means last transformation in sequence resulted in Left, so return previous
    else
      foldLeftWithEarlyReturn._1

  }
...