it-roy-ru.com

Использование Either для обработки сбоев в коде Scala

Option monad - отличный выразительный способ иметь дело с чем-то или ничего в Scala. Но что, если нужно зарегистрировать сообщение, когда «ничего» не происходит? Согласно документации Scala API,

Тип Either часто используется как альтернатива scala.Option где слева представляет сбой (по соглашению) и Право сродни некоторым.

Тем не менее, мне не повезло найти лучшие практики, использующие Either или хорошие примеры из реальной жизни, в которых используется Either для обработки сбоев. Наконец, я придумал следующий код для моего собственного проекта:

    def logs: Array[String] = {
        def props: Option[Map[String, Any]] = configAdmin.map{ ca =>
            val config = ca.getConfiguration(PID, null)
            config.properties getOrElse immutable.Map.empty
        }
        def checkType(any: Any): Option[Array[String]] = any match {
            case a: Array[String] => Some(a)
            case _ => None
        }
        def lookup: Either[(Symbol, String), Array[String]] =
            for {val properties <- props.toRight('warning -> "ConfigurationAdmin service not bound").right
                 val logsParam <- properties.get("logs").toRight('debug -> "'logs' not defined in the configuration").right
                 val array <- checkType(logsParam).toRight('warning -> "unknown type of 'logs' confguration parameter").right}
            yield array

        lookup.fold(failure => { failure match {
            case ('warning, msg) => log(LogService.WARNING, msg)
            case ('debug, msg) =>   log(LogService.DEBUG, msg)
            case _ =>
        }; new Array[String](0) }, success => success)
    }

(Обратите внимание, что это фрагмент из реального проекта, поэтому он не будет компилироваться самостоятельно)

Буду признателен за информацию о том, как вы используете Either в своем коде, и/или за лучшие идеи по рефакторингу приведенного выше кода.

49
Alexander Azarov

Любой из них используется для возврата одного из двух возможных значимых результатов, в отличие от Option, который используется для возврата одного значимого результата или ничего.

Простой для понимания пример приведен ниже (он был распространен в списке рассылки Scala некоторое время назад):

def throwableToLeft[T](block: => T): Either[Java.lang.Throwable, T] =
  try {
    Right(block)
  } catch {
    case ex => Left(ex)
  }

Как следует из названия функции, если выполнение «block» будет успешным, оно вернет «Right (<результат>)». В противном случае, если выбрасывается Throwable, он вернет «Left (<throwable>)». Используйте сопоставление с шаблоном для обработки результата:

var s = "hello"
throwableToLeft { s.toUpperCase } match {
  case Right(s) => println(s)
  case Left(e) => e.printStackTrace
}
// prints "HELLO"

s = null
throwableToLeft { s.toUpperCase } match {
  case Right(s) => println(s)
  case Left(e) => e.printStackTrace
}
// prints NullPointerException stack trace

Надеюсь, это поможет.

48
Walter Chang

В библиотеке Scalaz есть что-то похожее, называемое Validation. Это более идиоматично, чем Either для использования в качестве «получить либо действительный результат, либо сбой».

Проверка также позволяет накапливать ошибки.

Редактирование: "alike". Либо полностью ложно, потому что Валидация является аппликативным функтором, а скалярный вариант "Либо" с именем \/(произносится как "дизъюнкция" или "либо") является монадой. Тот факт, что санкционирование может накапливать ошибки, является следствием такой природы. С другой стороны,/имеет природу «остановка раньше», останавливаясь на первой - \/(читайте «слева» или «ошибка»), с которой он сталкивается. Здесь есть идеальное объяснение: http://typelevel.org/blog/2014/02/21/error-handling.html

Смотрите: http://scalaz.googlecode.com/svn/continuous/latest/browse.sxr/scalaz/example/ExampleValidation.scala.html

Согласно запросу, скопируйте/вставьте ссылку выше (некоторые строки удалены):

// Extracting success or failure values
val s: Validation[String, Int] = 1.success
val f: Validation[String, Int] = "error".fail

// It is recommended to use fold rather than pattern matching:
val result: String = s.fold(e => "got error: " + e, s => "got success: " + s.toString)

s match {
  case Success(a) => "success"
  case Failure(e) => "fail"
}

// Validation is a Monad, and can be used in for comprehensions.
val k1 = for {
  i <- s
  j <- s
} yield i + j
k1.toOption assert_≟ Some(2)

// The first failing sub-computation fails the entire computation.
val k2 = for {
  i <- f
  j <- f
} yield i + j
k2.fail.toOption assert_≟ Some("error")

// Validation is also an Applicative Functor, if the type of the error side of the validation is a Semigroup.
// A number of computations are tried. If the all success, a function can combine them into a Success. If any
// of them fails, the individual errors are accumulated.

// Use the NonEmptyList semigroup to accumulate errors using the Validation Applicative Functor.
val k4 = (fNel <**> fNel){ _ + _ }
k4.fail.toOption assert_≟ some(nel1("error", "error"))
13
fanf42

Фрагмент, который вы разместили, кажется очень надуманным. Вы используете либо в ситуации, когда:

  1. Недостаточно просто знать, что данные недоступны.
  2. Вам нужно вернуть один из двух разных типов.

Превращение исключения в левое является действительно распространенным случаем использования. Преимущество перед try/catch заключается в сохранении кода вместе, что имеет смысл, если исключение составляет ожидаемый результат . Наиболее распространенный способ обработки Either - сопоставление с образцом:

result match {
  case Right(res) => ...
  case Left(res) => ...
}

Другой интересный способ обработки Either - это когда он появляется в коллекции. При создании карты для коллекции исключение может быть нежизнеспособным, и вы можете захотеть вернуть некоторую информацию, отличную от «невозможна». Использование Either позволяет вам сделать это без перегрузки алгоритма:

val list = (
  library 
  \\ "books" 
  map (book => 
    if (book \ "author" isEmpty) 
      Left(book) 
    else 
      Right((book \ "author" toList) map (_ text))
  )
)

Здесь мы получаем список всех авторов в библиотеке, плюс список книг без автора. Таким образом, мы можем затем обработать его соответствующим образом:

val authorCount = (
  (Map[String,Int]() /: (list filter (_ isRight) map (_.right.get))) 
   ((map, author) => map + (author -> (map.getOrElse(author, 0) + 1)))
  toList
)
val problemBooks = list flatMap (_.left.toSeq) // thanks to Azarov for this variation

Итак, базовое использование любого из этих вариантов происходит следующим образом. Это не особенно полезный класс, но если бы это было так, вы бы видели его раньше. С другой стороны, это также не бесполезно.

7
Daniel C. Sobral

У Cats есть хороший способ создать код Either from бросающий исключение:

val either: Either[NumberFormatException, Int] =
  Either.catchOnly[NumberFormatException]("abc".toInt)
// either: Either[NumberFormatException,Int] = Left(Java.lang.NumberFormatException: For input string: "abc")

inhttps://typelevel.org/cats/datatypes/either.html#working-with-exception-y-code

0
vlfig