Что такое 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\]

Типичные сценарии

  1. Валидация пользовательского ввода

  2. Работа с API, где возможен отказ

  3. Парсинг данных

  4. Пошаговая логика с возможными ошибками

  5. Альтернатива исключениям в функциональном стиле

Реализация собственной ошибки

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 в контексте эффектов.