What's the deal with all the Either cruft?

2020-02-16 08:25发布

问题:

The Either class seems useful and the ways of using it are pretty obvious. But then I look at the API documentation and I'm baffled:

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.

What do I do with a projection and how do I even invoke the joins?

Google just points me to the API documentation.

This might just be a case of "paying no attention to the man behind the curtain", but I don't think so. I think this is important.

回答1:

left and right are the important ones. Either is useful without projections (mostly you do pattern matching), but projections are quite worthy of attention, as they give a much richer API. You will use joins much less.

Either is often used to mean "a proper value or an error". In this respect, it is like an extended Option . When there is no data, instead of None, you have an error. Option has a rich API. The same can be made available on Either, provided we know, in Either, which one is the result and which one is the error.

left and right projection says just that. It is the Either, plus the added knowledge that the value is respectively at left or at right, and the other one is the error.

For instance, in Option, you can map, so opt.map(f) returns an Option with f applied to the value of opt if it has a one, and still None if opt was None. On a left projection, it will apply f on the value at left if it is a Left, and leave it unchanged if it is a Right. Observe the signatures:

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

left and right are simply the way to say which side is considered the value when you want to use one of the usual API routines.

Alternatives could have been:

  • set a convention, as in Haskell, where there were strong syntactical reasons to put the value at right. When you want to apply a method on the other side (you may well want to change the error with a map for instance), do a swap before and after.
  • postfix method names with Left or Right (maybe just L and R). That would prevent using for comprehension. With for comprehensions (flatMap in fact, but the for notation is quite convenient) Either is an alternative to (checked) exceptions.

Now the joins. Left and Right means the same thing as for the projections, and they are closely related to flatMap. Consider joinLeft. The signature may be puzzling:

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

A1 and B1 are technically necessary, but not critical to the understanding, let's simplify

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

What the implicit means is that the method can only be called if A is an Either[C,B]. The method is not available on an Either[A,B] in general, but only on an Either[Either[C,B], B]. As with left projection, we consider that the value is at left (that would be right for joinRight). What the join does is flatten this (think flatMap). When one join, one does not care whether the error (B) is inside or outside, we just want Either[C,B]. So Left(Left(c)) yields Left(c), both Left(Right(b)) and Right(b) yield Right(b). The relation with flatMap is as follows:

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

The Option equivalent would work on an Option[Option[A]], Some(Some(x)) would yield Some(x) both Some(None) and None would yield None. It can be written o.flatMap(identity). Note that Option[A] is isomorphic to Either[A,Unit] (if you use left projections and joins) and also to Either[Unit, A] (using right projections).



回答2:

Ignoring the joins for now, projections are a mechanism allowing you to use use an Either as a monad. Think of it as extracting either the left or right side into an Option, but without losing the other side

As always, this probably makes more sense with an example. So imagine you have an Either[Exception, Int] and want to convert the Exception to a String (if present)

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

This will map over the left side of result, giving you an Either[String,Int]



回答3:

joinLeft and joinRight enable you to "flatten" a nested 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)

Edit: My answer to this question shows one example of how you can use the projections, in this case to fold together a sequence of Eithers without pattern matching or calling isLeft or isRight. If you're familiar with how to use Option without matching or calling isDefined, it's analagous.


While curiously looking at the current source of Either, I saw that joinLeft and joinRight are implemented with pattern matching. However, I stumbled across this older version of the source and saw that it used to implement the join methods using projections:

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


回答4:

My suggestion is add the following to your utility package:

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))
   }
}   

In my experience the only useful provided method is fold. You don't really use isLeft or isRight in functional code. joinLeft and joinRight might be useful as flatten functions as explained by Dider Dupont but, I haven't had occasion to use them that way. The above is using Either as right biased, which I suspect is how most people use them. Its like an Option with an error value instead of None.

Here's some of my own code. Apologies its not polished code but its an example of using Either in a for comprehension. Adding the map and flatMap methods to Either allows us to use the special syntax in for comprehensions. Its parsing HTTP headers, either returning an Http and Html error page response or a parsed custom HTTP Request object. Without the use of the for comprehension the code would be very difficult to comprehend.

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))
  }   
}


标签: scala