Глава 7: Тип Either
================================================

В предыдущей главе мы узнали как обрабатываются исключения в функциональном стиле. 
Мы узнали о тип `Try`.  Он появился в Scala с версии 2.10.  Также я упомянул и о типе `Either`. 
В этой главе мы остановимся на нём по-подробнее.  Мы узнаем как и где он используется и о некоторых
неприятных особенностях типа `Either`.  

На данный момент в типе `Either` есть несколько серьёзных недостатков, о которых стоит знать.  
Они настолько серьёзны, что необходимость использования `Either` может вызывать сомнения.  
А нужен ли он нам? 

Во первых, до появления `Try` для обработки исключений использовался тип `Either` и не
все разработчики перешли на новый способ.  Поэтому нам стоит разобраться в тонкостях и этого типа. 

Кроме того, `Try` не равносилен `Either`.  `Try` используется только для обработки исключений
в функциональном стиле.  На практике `Try` и `Either` дополняют друг друга.  И несмотря на все
недостатки `Either`, есть проблемы, с которыми он справляется очень хорошо. 

Семантика
-----------------------------------------

Как `Try` и `Option` тип `Either` является контейнером.  Только он принимает два параметра, а не один: 
значение типа `Either[A, B]` может содержать значение типа `A` или значение типа `B`.  В этом отличие от `Tuple[A, B]`,
который одновременно содержит два значения,  типов `A` и `B`. 

От `Either` наследуют всего два класса: `Left` и `Right`.  Если значение `Either[A, B]` содержит 
значение типа `A`,  тогда `Either` содержит `Left`, иначе оно содержит значение типа `B` 
обёрнутое в класс `Right`. 

В семантике типа ничто не указывает на то, что один или другой тип представляет ошибку
или успешное выполнение.  На самом деле, `Either` обозначает тип общего назначения, 
представляющий результат, в котором для значений есть две возможные альтернативы.  
Но несмотря на это, чаще всего он используется для обработки исключений, и по соглашению
`Left` отвечает за ошибки/исключения, а `Right` &mdash; за успешно вычисленное значение. 

Создание значения типа `Either`
------------------------------------------

Значения типа `Either` создаются очень просто.  И `Left` и `Right` являются `case`-классами.  
Так если мы хотим реализовать непробиваемую систему интернет-цензуры, мы можем сделать это так:

~~~scala
import scala.io. Source
import java.net. URL

def getContent(url: URL): Either[String, Source] =
  if (url.getHost.contains("google"))
    Left("Requested URL is blocked for the good of the people!") 
  else
    Right(Source.fromURL(url))
~~~

Теперь, если мы вызовем `getContent(new URL("http://danielwestheide.com"))`, то мы получим
`scala.io. Source` обёрнутый в `Right`.  Если мы попробуем обратиться по `new URL("https://plus.google.com")`,
результат будет содержать `Left` со строкой. 

Использование `Either`
----------------------------------------------

Некоторые совсем простые вещи работают в `Either` точно так же как и в `Try`.  У нас есть методы `isLeft`
и `isRight`.  Также мы можем выполнять сопоставление с образцом, этот способ работы с типом `Either`
наиболее удобен:

~~~scala
getContent(new URL("http://google.com")) match {
  case Left(msg) => println(msg)
  case Right(source) => source.getLines.foreach(println)
}
~~~

### Проекции

Мы не можем работать с `Either` как с коллекциями, также как и в `Option` или `Try`.  
Это решение вызвано тем, что `Either` не должно отдавать предпочтение одной или другой альтернативе. 

В `Try` мы концентрируемся на типе результата, успешно вычисленного значения, для него определены
`map`, `flatMap` и другие методы.  Все они предполагают, что `Try` содержит `Success`, и если это
не так, они ничего не делают, передавая далее `Failure`.  

Поскольку в `Either` альтернативы равнозначны, мы сначала должны определиться с какой веткой 
мы хотим работать, вызовом методов `left` или `right` на значении типа `Either`.  После этого
мы получим одну из проекций `LeftProjection` или `RightProjection`, которые концентрируются на 
левой и правой альтернативе соответственно.  

#### Преобразование

После того как у нас есть проекция, мы можем преобразовать её элемент:

~~~scala
val content: Either[String, Iterator[String]] =
  getContent(new URL("http://danielwestheide.com")).right.map(_.getLines())
// content содержит Right со строчками из Source, который был получен с помощью getContent

val moreContent: Either[String, Iterator[String]] =
  getContent(new URL("http://google.com")).right.map(_.getLines)
// moreContent содержит Left, полученный из getContent
~~~

Что бы не содержало значение `Either[String, Source]` в этом примере, `Left` или `Right`, оно будет
преобразовано в `Either[String, Iterator[String]]`.  Если оно содержит `Right`, то значение будет 
преобразовано, если оно содержит `Left`, значение останется без изменений. 

То же самое мы можем выполнить и для `LeftProjection`:

~~~scala
val content: Either[Iterator[String], Source] =
  getContent(new URL("http://danielwestheide.com")).left.map(Iterator(_))
// content содержит Right с Source, в том виде, в котором он был получен из getContent

val moreContent: Either[Iterator[String], Source] =
  getContent(new URL("http://google.com")).left.map(Iterator(_))
// moreContent содержит Left с msg, полученым из getContent в Iterator'е
~~~

Теперь, если `Either` содержит `Left`, результат будет преобразован, а в случае
`Right` оставлен без изменений.  И в том и в другом случае результат значения будет
`Either[Iterator[String], Source]`

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

#### Метод `flatMap`

Для проекций также определён метод `flatMap`, который позволяет избежать проблемы вложенных структур
при вызове `map`.  

Прошу потерпеть, но сейчас мы рассмотрим очень надуманный пример.  Допустим нам хочется
узнать среднее число строк для двух моих статей.  Нам ведь давно хотелось узнать об этом, не так ли? 
Мы можем решить эту сложнейшую задачу, примерно так:

~~~scala
val part5 = new URL("http://t.co/UR1aalX4")
val part6 = new URL("http://t.co/6wlKwTmu")
val content = getContent(part5).right.map(a =>
  getContent(part6).right.map(b =>
    (a.getLines().size + b.getLines().size) / 2))
~~~

В результате мы получим значение типа `Either[String, Either[String, Int]]`, что содержит вложенный `Either`
в правой альтернативе.  Мы можем избавиться от этой вложенности вызовом метода `joinRight`, также
определён и метод `joinLeft`.  

Однако мы можем не создавать вложенную структуру изначально.  Если мы вызовем `flatMap` на `RightProjection`, то
мы полуим более приятный результат.  Это приведёт к тому что, мы избавимся от `Right` во внутреннем `Either`.  

~~~scala
val content = getContent(part5).right.flatMap(a =>
  getContent(part6).right.map(b =>
    (a.getLines().size + b.getLines().size) / 2))
~~~

Теперь переменная content имеет тип `Either[String, Int]` и с ней гораздо приятнее работать,
например, при сопоставлении с образцом.  

#### For-генераторы

Скорее всего, `for`-генераторы уже успели вам понравиться.  Они предоставляют набор простых средств
для работы с различными типами.  Также мы можем использовать `for`-генераторами для проекций,
но, к сожалению, это будет не так элегантно, придётся иметь дело с корявыми костылями.  

Давайте перепишем наш пример с `flatMap` через генераторы:

~~~scala
def averageLineCount(url1: URL, url2: URL): Either[String, Int] =
  for {
    source1 <- getContent(url1).right
    source2 <- getContent(url2).right
  } yield (source1.getLines().size + source2.getLines().size) / 2
~~~

Не так плохо.  Обратите внимание на вызов `right` в каждом из генераторов.  

Теперь немного перепишем это выражение.  Поскольку возвращаемый результат слишком
многословен, давайте определим локальные переменные для его упрощения:

~~~scala
def averageLineCountWontCompile(url1: URL, url2: URL): Either[String, Int] =
  for {
    source1 <- getContent(url1).right
    source2 <- getContent(url2).right
    lines1 = source1.getLines().size
    lines2 = source2.getLines().size
  } yield (lines1 + lines2) / 2
~~~

Но это выражение не скомпилируется!  Причина станет понятной если мы избавимся от
синтаксического сахара `for`-генераторов.  Исходное выражение превратится в нечто
менее наглядное:

~~~scala
def averageLineCountDesugaredWontCompile(url1: URL, url2: URL): Either[String, Int] =
  getContent(url1).right.flatMap { source1 =>
    getContent(url2).right.map { source2 =>
      val lines1 = source1.getLines().size
      val lines2 = source2.getLines().size
      (lines1, lines2)
    }.map { case (x, y) => x + y / 2 }
  }
~~~

Проблема в том, что при определении переменных внутри `for`-генератора появляется
ещё один промежуточный вызов `map`.  Он вызывается на результате предыдущего вызова `map`,
который вернул `Either`, а не `RightProjection`.  Но для `Ether` метод `map` не определён,
на что и пожаловался компилятор.   

И вот из `Either` показалась корявая усмешка тролля.  В этом примере, мы можем обойтись 
и без локальных переменных.  Если они нам действительно нужна, мы можем обойти проблему,
заменив простые переменные генераторами:

~~~scala
def averageLineCount(url1: URL, url2: URL): Either[String, Int] =
  for {
    source1 <- getContent(url1).right
    source2 <- getContent(url2).right
    lines1 <- Right(source1.getLines().size).right
    lines2 <- Right(source2.getLines().size).right
  } yield (lines1 + lines2) / 2
~~~

Об этой проблеме необходимо помнить.   Она не ставит крест на `Either`, но может доставить
массу хлопот, если вы не понимаете причину её появления. 

#### Другие методы

На проекциях определены и другие полезные методы:

Мы можем преобразовать `Either` в `Option` вызовом метода `toOption` на одной из проекций. 
К примеру, у нас есть значение `e` типа `Either[A, B]`.  Выражение `e.right.toOption` вернёт
`Option[B]`.  Если `Either[A, B]` содержит `Right`, результат окажется `Some`.  Если `Left`, то
мы получим `None`.  Аналогично и для `LeftProjection`.  Для преобразование в последовательность 
из одного элемента воспользуйтесь методом `toSeq`. 


Свёртка
--------------------------------

Для преобразования `Either` в независимости от того какую альтернативу оно содержит,
мы можем воспользоваться методом `fold`, принимающим две функции с одним и тем же 
типом для результата.  Первая функции вызывается, если значение содержит `Left` и вторая
функция &mdash; в случае `Right`.  

Рассмотри на примере объединения двух операций, определённых на левой и правой проекции:

~~~scala
val content: Iterator[String] =
  getContent(new URL("http://danielwestheide.com")).fold(Iterator(_), _.getLines())

val moreContent: Iterator[String] =
  getContent(new URL("http://google.com")).fold(Iterator(_), _.getLines())
~~~

В этом примере мы преобразуем `Either[String, Source]` в `Iterator[String]`, вне зависимости
от значения `Either`.  Также мы могли вернуть из функций `Either` или выполнить побочные эффекты 
с результатом `Unit` для двух функций.  Метод `fold` является отличной альтернативой сопоставлению 
с образцом. 


Когда использовать Either
---------------------------------

Теперь, когда мы узнали как использовать `Either`, давайте рассмотрим несколько частных
случаев его применения. 

### Обработка исключений

Мы можем пользоваться `Either` для обработки исключений вместо `Try`.  У `Either` есть
одно небольшое преимущество в том, что мы можем определить тип исключений более точно,
в то время как в `Try` всё время используется самый общий тип `Throwable`. 
Поэтому `Either` хорошо применять, когда тип исключений заранее известен. 

Необходимо определить специальный метод, воспользовавшись очень полезным объектом
`exception` из пакета `scala.util.control`:

~~~scala
import scala.util.control.Exception.catching
def handling[Ex <: Throwable, T](exType: Class[Ex])(block: => T): Either[Ex, T] =
  catching(exType).either(block).asInstanceOf[Either[Ex, T]]
~~~

Нам нужен этот метод из-за того, что несмотря на то, что методы, определённые в `scala.util. Exception`,
позволяют нам обрабатывать специфические типы исключений, результирующий на этапе компиляции 
тип исключения всегда будет `Throwable`. 

С помощью этого метода мы можем указывать на тип исключения в `Either`:

~~~scala
import java.net. MalformedURLException
def parseURL(url: String): Either[MalformedURLException, URL] =
  handling(classOf[MalformedURLException])(new URL(url))
~~~

Также наши исключения не обязательно могут быть вызваны отваливающимися функциями из чужих библиотек,
как в примере выше.  В тех примерах мы лишь перехватывали исключения, которые случались 
в библиотечных функциях, и оборачивали их в `Left`.  Вместо этого мы можем определить свой тип ошибок,
предпочтительно в виде `case`-класса.  После чего мы можем возвращать его из функции, поместив в 
конструктор `Left`, при выполнении некоторого условия.  

Приведём пример:

~~~scala
case class Customer(age: Int)

class Cigarettes

case class UnderAgeFailure(age: Int, required: Int)

def buyCigarettes(customer: Customer): Either[UnderAgeFailure, Cigarettes] =
  if (customer.age < 16) Left(UnderAgeFailure(customer.age, 16))
  else Right(new Cigarettes)
~~~

Для обработки неожиданных исключений лучше использовать `Try`.  Он лишён всех недостатков `Either`. 


### Обработка коллекций

Обычно, `Either` хорошо подходит для работы с коллекциями, если в какой-то момент
что-то может пойти не так, но при этом не возникает ошибка, исключающая дальнейшую
обработку элементов коллекции.   

Предположим, что для нашего интернет-цензора определён чёрный список:

~~~scala
type Citizen = String
case class BlackListedResource(url: URL, visitors: Set[Citizen])

val blacklist = List(
  BlackListedResource(new URL("https://google.com"), Set("John Doe", "Johanna Doe")),
  BlackListedResource(new URL("http://yahoo.com"), Set.empty),
  BlackListedResource(new URL("https://maps.google.com"), Set("John Doe")),
  BlackListedResource(new URL("http://plus.google.com"), Set.empty)
)
~~~

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

Теперь мы хотим найти проблемных пользователей, то есть тех, что пытались попасть 
на запрещённые страницы.  В то же время мы хотим найти подозрительные страницы &mdash; 
те страницы, в которые никто не обращался.  Мы предполагаем, что пользователи
могут обойти наш фильтр и нам нужно разобраться с этой проблемой:

Мы можем обработать наш список следующим образом: 

~~~scala
val checkedBlacklist: List[Either[URL, Set[Citizen]]] =
  blacklist.map(resource =>
    if (resource.visitors.isEmpty) Left(resource.url)
    else Right(resource.visitors))
~~~

Мы создали последовательность значений `Either`.  Левые альтернативы представляют подозрительные адреса 
и правые &mdash; множества проблемных пользователей.  Теперь мы можем совсем просто установить и проблемных пользователей
и подозрительные страницы: 

~~~scala
val suspiciousResources = checkedBlacklist.flatMap(_.left.toOption)
val problemCitizens = checkedBlacklist.flatMap(_.right.toOption).flatten.toSet
~~~

Мы видим, что `Either` отлично справляется с более общими задачами, которые
выходят за рамки обработки исключений. 

Итоги
-------------------------------------------

Мы узнали как и где использовать `Either`, а также о его недостатках.  И `Either` совсем
не лишён недостатков, но итоговое решение, нужен ли этот тип в Вашем коде или нет, всегда
остаётся за Вами. 

На практике, благодаря `Try`, для `Either` остаётся не так много работы.  Но всё-таки
нам важно знать и об этом типе.  Могут случиться и ситуации, в которых он станет вашим 
незаменимым помощником, а также это полезно для понимания кода Scala для версий ниже 2.10,
там где он использовался для обработки исключений. 


----------------------------------------------------

* <= [Глава 6: Обработка исключений с помощью Try](https://github.com/anton-k/ru-neophyte-guide-to-scala/blob/master/src/p06-error-handling.md)

* => [Глава 8: Добро пожаловать в будущее](https://github.com/anton-k/ru-neophyte-guide-to-scala/blob/master/src/p08-future.md)

* [Содержание](https://github.com/anton-k/ru-neophyte-guide-to-scala#%D0%9F%D1%83%D1%82%D0%B5%D0%B2%D0%BE%D0%B4%D0%B8%D1%82%D0%B5%D0%BB%D1%8C-%D0%BD%D0%B5%D0%BE%D1%84%D0%B8%D1%82%D0%B0-%D0%BF%D0%BE-scala)
