Что такое Either, Left, Right и зачем они нужны?
В Scala Either, Left и Right используются для представления результатов вычислений, которые могут завершиться как успешно, так и с ошибкой. Это альтернатива выбрасыванию исключений, особенно в функциональном программировании, где цель — избегать побочных эффектов и делать ошибки частью типа результата.
Что такое Either
Either — это контейнер, который содержит одно из двух значений: либо Left, либо Right. Определение:
sealed abstract class Either\[+A, +B\]
case class Left\[+A\](value: A) extends Either\[A, Nothing\]
case class Right\[+B\](value: B) extends Either\[Nothing, B\]
- Left традиционно используется для ошибок или **негативных исходов
** - Right — для **успешных результатов
**
Таким образом, Either — это более информативная альтернатива Option, где Left позволяет указать причину неудачи.
Пример использования
def divide(a: Int, b: Int): Either\[String, Int\] = {
if (b == 0) Left("Cannot divide by zero")
else Right(a / b)
}
Вызов:
val result = divide(10, 0) // Left("Cannot divide by zero")
val result2 = divide(10, 2) // Right(5)
Обработка значения
result match {
case Right(value) => println(s"Result is: $value")
case Left(error) => println(s"Error: $error")
}
Или с функциями высшего порядка:
val message = result.fold(
err => s"Failed: $err",
value => s"Success: $value"
)
fold принимает два аргумента: функцию для Left, и функцию для Right.
Зачем нужен Either
-
Позволяет явно моделировать ошибки как часть типа результата.
-
Позволяет возвращать полезные сообщения об ошибках, в отличие от Option, где None не объясняет, что пошло не так.
-
Избегает исключений (исключения — это побочный эффект и плохо вписываются в функциональный стиль).
-
Поддерживает map, flatMap, filterOrElse — можно строить цепочки вычислений, которые "прерываются" при первом Left.
Работа с map и flatMap
val r: Either\[String, Int\] = Right(5)
val r2 = r.map(_ \* 2) // Right(10)
val l: Either\[String, Int\] = Left("Error")
val l2 = l.map(_ \* 2) // Left("Error"), map не применяется
map применяется только к Right. flatMap позволяет связывать цепочки Either:
def parse(s: String): Either\[String, Int\] =
try Right(s.toInt)
catch {
case \_: NumberFormatException => Left("Not a number")
}
def reciprocal(n: Int): Either\[String, Double\] =
if (n == 0) Left("Division by zero")
else Right(1.0 / n)
val result = parse("5").flatMap(reciprocal) // Right(0.2)
val fail = parse("abc").flatMap(reciprocal) // Left("Not a number")
Использование for-comprehension
Either можно удобно использовать в for-выражениях, если привести к Right-ориентированной логике:
val res = for {
n <- parse("5")
r <- reciprocal(n)
} yield r
Если хотя бы один шаг вернёт Left, вся цепочка прервётся и вернёт Left.
Предпочтения: Right-biased и Left-biased
В Scala 2.12 и ниже Either — не biased, и приходится использовать .right:
Right(10).right.map(_ \* 2) // Right(20)
В Scala 2.13 и выше Either — right-biased по умолчанию, что означает:
-
map, flatMap, filterOrElse, и т.д. — применяются напрямую к Right
-
Left просто "протаскивается" дальше
Полезные методы
-
.isRight, .isLeft — проверка типа
-
.getOrElse(default) — вернуть значение или подставить значение по умолчанию
-
.orElse(anotherEither) — если Left, вернуть другой Either
-
.swap — меняет Left и Right местами
-
.toOption — превращает Right(value) → Some(value), Left(_) → None
Option vs Either
Характеристика | Option | Either |
---|---|---|
Значение | Some / None | Right / Left |
--- | --- | --- |
Ошибка с описанием? | Нет | Да (Left содержит ошибку) |
--- | --- | --- |
Обработка цепочек | Да | Да |
--- | --- | --- |
Типобезопасность | Да | Да |
--- | --- | --- |
Типовой шаблон: Either[Error, Value]
-
Тип Left — всегда тип ошибки (например, String, Throwable, CustomError)
-
Тип Right — значение, которое получилось
type Result\[T\] = Either\[String, T\]
Типичные сценарии
-
Валидация пользовательского ввода
-
Работа с API, где возможен отказ
-
Парсинг данных
-
Пошаговая логика с возможными ошибками
-
Альтернатива исключениям в функциональном стиле
Реализация собственной ошибки
sealed trait AppError
case class ValidationError(msg: String) extends AppError
case class DBError(reason: String) extends AppError
def loadUser(id: Int): Either\[AppError, User\]
Теперь можно обрабатывать ошибки по типу:
result match {
case Right(user) => // ОК
case Left(ValidationError(msg)) => // Обработка валидации
case Left(DBError(reason)) => // Ошибка базы
}
Совместимость с Future
Можно комбинировать Future[Either[A, B]], обрабатывая и асинхронность, и ошибки:
val result: Future\[Either\[String, Int\]\] = Future {
if (System.currentTimeMillis() % 2 == 0) Right(42)
else Left("Odd time!")
}
Для таких случаев можно использовать библиотеки вроде Cats или ZIO, которые упрощают работу с типами Either в контексте эффектов.