Использование Либо для обработки отказов в коде Scala
Option
Монада - отличный выразительный способ справиться с чем-то или чем-либо в Scala. Но что, если нужно регистрировать сообщение, когда "ничего" не происходит? Согласно документации Scala API,
Любой тип часто используется как альтернатива Scala.Option, где Left представляет собой отказ (по соглашению) и Правильно сродни Некоторым.
Однако мне не повезло найти лучшие практики с использованием Либо или хороших примеров реального мира, связанных с Либо для обработки сбоев. Наконец, я придумал следующий код для своего собственного проекта:
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
в своем коде и/или более совершенные идеи по рефакторингу вышеуказанного кода.
Ответы
Ответ 1
Либо используется для возврата одного из возможных двух значимых результатов, в отличие от Option, который используется для возврата одного значимого результата или ничего.
Ниже приводится простой для понимания пример (распространенный в списке рассылки Scala a назад):
def throwableToLeft[T](block: => T): Either[java.lang.Throwable, T] =
try {
Right(block)
} catch {
case ex => Left(ex)
}
Как следует из названия функции, если выполнение "блока" будет успешным, оно вернет "Right (<result> )". В противном случае, если выбрано 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
Надеюсь, что это поможет.
Ответ 2
Библиотека Scalaz имеет нечто похожее. Это более идиоматично, чем Либо для использования как "получить либо действительный результат, либо неудачу".
Валидация также позволяет накапливать ошибки.
Edit: "alike". Либо полностью ложно, потому что Validation является прикладным функтором, а scalaz. Или, названный \/(произносится как "disjonction" или "or" ), является монадой.
Тот факт, что валидация может накапливать ошибки, объясняется именно этим. С другой стороны,/имеет "стоп-ранний" характер, останавливаясь при первом - \/(читайте "слева" или "ошибка" ), с которым он сталкивается. Здесь есть полное объяснение: 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"))
Ответ 3
Отсканированный фрагмент кажется очень надуманным. Вы используете Либо в ситуации, когда:
- Недостаточно просто знать, что данные недоступны.
- Вам нужно вернуть один из двух разных типов.
Превращение исключения в левое является, действительно, обычным прецедентом. По сравнению с try/catch у него есть преимущество в том, чтобы держать код вместе, что имеет смысл, если исключение является ожидаемым результатом. Наиболее распространенным способом обработки данных является выбор шаблона:
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
Итак, базовое Либо использование идет так. Это не особо полезный класс, но если бы вы это видели раньше. С другой стороны, это тоже не бесполезно.
Ответ 4
У 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")
в https://typelevel.org/cats/datatypes/either.html#working-with-exception-y-code