Какая сделка со всеми Либо крутыми?

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

def joinLeft [A1 >: A, B1 >: B, C] (implicit ev: <:<[A1, Either[C, B1]]):
         Either[C, B1]
   Joins an Either through Left.

def joinRight [A1 >: A, B1 >: B, C] (implicit ev: <:<[B1, Either[A1, C]]):
         Either[A1, C]
   Joins an Either through Right.

def left : LeftProjection[A, B]
   Projects this Either as a Left.

def right : RightProjection[A, B]
   Projects this Either as a Right.

Что мне делать с проекцией и как я даже вызываю соединения?

Google просто указывает мне на документацию по API.

Это может быть случай "не обращать внимания на человека за занавеской", но я так не думаю. Я думаю, что это важно.

Ответ 1

left и right являются важными. Either полезен без прогнозов (в основном, для сопоставления шаблонов), но прогнозы заслуживают внимания, поскольку они дают гораздо более богатый API. Вы будете использовать соединения гораздо меньше.

Either часто используется для обозначения "правильного значения или ошибки". В этом отношении он похож на расширенный Option. Если данных нет, вместо None у вас есть ошибка. Option имеет богатый API. То же самое можно сделать доступным на Either, если мы знаем, в любом случае, какой из них является результатом, а какой - ошибкой.

left и right прогнозы говорят именно об этом. Это Either, плюс добавленное знание о том, что значение соответственно слева или справа, а другое - ошибка.

Например, в Option вы можете сопоставить, так что opt.map(f) возвращает Option с f, примененный к значению opt, если он имеет один, а еще None, если opt было None. На левом проецировании он применит f к значению слева, если это left, и оставьте его неизменным, если это right. Соблюдайте подписи:

  • В LeftProjection[A,B], map[C](f: A => C): Either[C,B]
  • В RightProjection[A,B], map[C](f: B => C): Either[A,C].

left и right - это просто способ сказать, какая сторона считается значением, когда вы хотите использовать одну из обычных API-подпрограмм.

Альтернативы могли быть:

  • установить соглашение, как в Haskell, где были сильные синтаксические причины, чтобы поставить значение справа. Если вы хотите применить метод с другой стороны (возможно, вы захотите изменить ошибку с помощью map), выполните swap до и после.
  • имена методов postfix с левым или правым (возможно, только L и R). Это предотвратило бы использование для понимания. При использовании for понятий (flatMap на самом деле, но для обозначения довольно удобно) Either является альтернативой (отмеченным) исключениям.

Теперь соединяется. Left и Right означает то же, что и для проекций, и они тесно связаны с flatMap. Рассмотрим joinLeft. Подпись может быть озадачивающей:

joinLeft [A1 >: A, B1 >: B, C] (implicit ev: <:<[A1, Either[C, B1]]):
         Either[C, B1]

A1 и B1 являются технически необходимыми, но не критичными для понимания, упрощают

joinLeft[C](implicit ev: <:<[A, Either[C, B])

Что подразумевается в том, что метод можно вызвать только в том случае, если A является Either[C,B]. Этот метод недоступен в Either[A,B] вообще, но только на Either[Either[C,B], B]. Как и в случае левой проекции, мы считаем, что значение находится слева (это было бы правильно для joinRight). То, что делает соединение, сглаживает это (подумайте flatMap). Когда вы присоединяетесь, вам все равно, является ли ошибка (B) внутри или снаружи, мы просто хотим либо [C, B]. Итак, Left (Left (c)) дает Left (c), оба Left (Right (b)) и Right (b) дают Right (b). Связь с flatMap выглядит следующим образом:

joinLeft(e) = e.left.flatMap(identity)
e.left.flatMap(f) = e.left.map(f).joinLeft

Эквивалент Option будет работать на Option[Option[A]], Some(Some(x)) даст Some(x) как Some(None), так и None даст None. Его можно написать o.flatMap(identity). Заметим, что Option[A] изоморфен Either[A,Unit] (если вы используете левые проекции и объединения), а также в Either[Unit, A] (используя правильные проекции).

Ответ 2

Игнорируя соединения на данный момент, прогнозы - это механизм, позволяющий использовать использование Either в качестве монады. Подумайте об этом, как извлечение левой или правой стороны в Option, но без потери другой стороны

Как всегда, это, вероятно, имеет больше смысла с примером. Итак, представьте, что у вас есть Either[Exception, Int] и вы хотите преобразовать Exception в String (если есть)

val result = opReturningEither
val better = result.left map {_.getMessage}

Это будет отображаться по левой стороне результата, давая вам Either[String,Int]

Ответ 3

joinLeft и joinRight позволяют "сгладить" вложенный Either:

scala> val e: Either[Either[String, Int], Int] = Left(Left("foo"))
e: Either[Either[String,Int],Int] = Left(Left(foo))

scala> e.joinLeft
res2: Either[String,Int] = Left(foo)

Изменить: Мой ответ на этот вопрос показывает один пример того, как вы можете использовать проекции, в этом случае складывать последовательность Either без соответствия шаблону или вызов isLeft или isRight. Если вы знакомы с тем, как использовать Option без соответствия или вызова isDefined, это аналогично.


В то время как с любопытством глядя на текущий источник Либо, я увидел, что joinLeft и joinRight реализованы с использованием сопоставления с образцом. Тем не менее, я наткнулся на эту более старую версию источника и увидел, что он использовал для реализации методов объединения с использованием прогнозов:

def joinLeft[A, B](es: Either[Either[A, B], B]) =
  es.left.flatMap(x => x)

Ответ 4

Мое предложение добавляет следующее в ваш служебный пакет:

implicit class EitherRichClass[A, B](thisEither: Either[A, B])
{
   def map[C](f: B => C): Either[A, C] = thisEither match
   {
     case Left(l) => Left[A, C](l)
     case Right(r) => Right[A, C](f(r))
   }
   def flatMap[C](f: B => Either[A, C]): Either[A, C] = thisEither match
   {
     case Left(l) => Left[A, C](l)
     case Right(r) => (f(r))
   }
}   

По моему опыту, единственный полезный метод - сгиб. Вы действительно не используете isLeft или isRight в функциональном коде. joinLeft и joinRight могут быть полезны как сглаженные функции, как объяснил Дидер Дюпон, но у меня не было возможности использовать их таким образом. Вышесказанное использует Either как правое предвзятое отношение, которое я подозреваю, как большинство людей используют их. Его как вариант со значением ошибки вместо None.

Вот некоторые из моих собственных кодов. Извините его не отполированный код, но его пример использования Либо в понимании. Добавление методов map и flatMap в Либо позволяет нам использовать специальный синтаксис для понимания. Его разбор HTTP-заголовков, либо возврат ответа страницы Http и Html, либо анализируемый пользовательский объект HTTP-запроса. Без использования понимания для понимания кода было бы очень сложно понять.

object getReq
{      
  def LeftError[B](str: String) = Left[HResponse, B](HttpError(str))
  def apply(line1: String, in: java.io.BufferedReader): Either[HResponse, HttpReq] = 
  {
    def loop(acc: Seq[(String, String)]): Either[HResponse, Seq[(String, String)]] =
    {
      val ln = in.readLine
      if (ln == "")
        Right(acc)         
      else
        ln.splitOut(':', s => LeftError("400 Bad Syntax in Header Field"), (a, b) => loop(acc :+ Tuple2(a.toLowerCase, b)))
    }

    val words: Seq[String] = line1.lowerWords

    for
    {
      a3 <- words match
      {
        case Seq("get", b, c) => Right[HResponse, (ReqType.Value, String, String)]((ReqType.HGet, b, c))
        case Seq("post", b, c) => Right[HResponse, (ReqType.Value, String, String)]((ReqType.HPost, b, c))
        case Seq(methodName, b, c) => LeftError("405" -- methodName -- "method not Allowed")
        case _ => LeftError("400 Bad Request: Bad Syntax in Status Line")
      }
      val (reqType, target, version) = a3
      fields <- loop(Nil)
      val optLen = fields.find(_._1 == "content-length")
      pair <- optLen match
      {
        case None => Right((0, fields))
        case Some(("content-length", second)) => second.filterNot(_.isWhitespace) match
        {
          case s if s.forall(_.isDigit) => Right((s.toInt, fields.filterNot(_._1 == "content-length")))
          case s => LeftError("400 Bad Request: Bad Content-Length SyntaxLine")
        }
      }
      val (bodyLen, otherHeaderPairs) = pair
      val otherHeaderFields = otherHeaderPairs.map(pair => HeaderField(pair._1, pair._2))
      val body = if (bodyLen > 0) (for (i <- 1 to bodyLen) yield in.read.toChar).mkString else ""         
    }      
    yield (HttpReq(reqType, target, version, otherHeaderFields, bodyLen, body))
  }   
}