Что за дело со всем этим?

Класс Either кажется полезным, и способы его использования довольно очевидны. Но потом я смотрю на документацию 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.

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

4 ответа

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 с помощью Left или Right (возможно, только L и R). Это помешало бы использовать для понимания. С for понимания (flatMap на самом деле, но для обозначения вполне удобно) Either является альтернативой (проверено) исключений.

Теперь присоединяется. Левый и правый означает то же самое, что и для проекций, и они тесно связаны с 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(личность). Обратите внимание, что Option[A] изоморфен Either[A,Unit] (если вы используете левые проекции и соединения), а также для Either[Unit, A] (используя правильные проекции).

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

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

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

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

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)

Я предлагаю добавить следующее в ваш пакет утилит:

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.

Вот мой собственный код. Извиняет, что это не отточенный код, а пример использования Either in a для понимания. Добавление методов map и flatMap к Either позволяет нам использовать специальный синтаксис для понимания. Это парсинг 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))
  }   
}
Другие вопросы по тегам