42 votes

C'est quoi le problème avec tous les Either cruft ?

La classe Either semble utile et les façons de l'utiliser sont assez évidentes. Mais ensuite, je regarde la documentation de l'API et je suis déconcerté :

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.

Que dois-je faire avec une projection et comment puis-je même invoquez les jointures ?

Google me renvoie simplement à la documentation de l'API.

Il s'agit peut-être simplement d'un cas où l'on ne prête pas attention à l'homme derrière le rideau, mais je ne le pense pas. Je pense que c'est important.

51voto

Didier Dupont Points 18256

left y right sont les plus importantes. Either est utile sans projections (vous faites surtout du filtrage), mais les projections méritent qu'on s'y attarde, car elles offrent une API beaucoup plus riche. Vous utiliserez beaucoup moins les jointures.

Either est souvent utilisé pour signifier "une valeur correcte ou une erreur". À cet égard, il s'agit d'une extension de Option . Lorsqu'il n'y a pas de données, au lieu de None vous avez une erreur. Option dispose d'une riche API. La même chose peut être mise à disposition sur Either à condition que nous sachions, dans l'un ou l'autre cas, lequel est le résultat et lequel est l'erreur.

left y right La projection dit juste cela. C'est le Either plus la connaissance supplémentaire que la valeur est respectivement à gauche ou à droite, et l'autre est l'erreur.

Par exemple, dans Option vous pouvez cartographier, donc opt.map(f) renvoie un Option avec f appliquée à la valeur de opt s'il en a un, et toujours None si opt était None . Sur une projection à gauche, il s'appliquera f sur la valeur à gauche si c'est un Left et le laisser inchangé s'il s'agit d'un Right . Observez les signatures :

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

left y right sont simplement le moyen de dire quel côté est considéré comme la valeur lorsque vous voulez utiliser l'une des routines habituelles de l'API.

Les alternatives auraient pu être :

  • établir une convention, comme en Haskell, où il y avait de fortes raisons syntaxiques de mettre la valeur à droite. Quand on veut appliquer une méthode de l'autre côté (on peut très bien vouloir changer l'erreur avec un map par exemple), faire un swap avant et après.
  • postfixer les noms de méthodes avec Left ou Right (peut-être juste L et R). Cela empêcherait de les utiliser pour la compréhension. Avec for compréhensions ( flatMap en fait, mais la notation for est assez pratique) Either est une alternative aux exceptions (vérifiées).

Maintenant, les jointures. Gauche et Droite signifient la même chose que pour les projections, et elles sont étroitement liées à flatMap . Considérons joinLeft . La signature peut être déroutante :

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

A1 y B1 sont nécessaires d'un point de vue technique, mais ne sont pas indispensables à la compréhension.

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

Ce que l'implicite signifie est que la méthode ne peut être appelée que si A est un Either[C,B] . Cette méthode n'est pas disponible sur un Either[A,B] en général, mais seulement sur un Either[Either[C,B], B] . Comme pour la projection à gauche, on considère que la valeur est à gauche (ce qui serait à droite pour les joinRight ). Ce que la jointure fait, c'est aplatir cela (pensez à flatMap ). Quand on joint, on ne se soucie pas de savoir si l'erreur (B) est à l'intérieur ou à l'extérieur, on veut juste Soit [C,B]. Donc Left(Left(c)) donne Left(c), Left(Right(b)) et Right(b) donnent tous deux Right(b). La relation avec flatMap est la suivante :

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

Le site Option L'équivalent fonctionnerait sur un Option[Option[A]] , Some(Some(x)) donnerait Some(x) les deux Some(None) y None donnerait None . On peut l'écrire o.flatMap(identity). Notez que Option[A] est isomorphe à Either[A,Unit] (si vous utilisez des projections et des jointures à gauche) et aussi à Either[Unit, A] (en utilisant les projections de droite).

21voto

Kevin Wright Points 31665

Ignorant les jointures pour le moment, les projections sont un mécanisme permettant d'utiliser une Either comme une monade. Il s'agit d'extraire le côté gauche ou le côté droit dans un fichier Option mais sans perdre l'autre côté

Comme toujours, cela a probablement plus de sens avec un exemple. Imaginez donc que vous avez un Either[Exception, Int] et que vous voulez convertir le Exception à un String (si présent)

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

Ceci va recouvrir le côté gauche du résultat, vous donnant un Either[String,Int]

15voto

Ben James Points 41165

joinLeft y joinRight vous permettent d'"aplatir" une image imbriquée. 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)

Edita: Ma réponse à cette question montre un exemple de la façon dont vous pouvez utiliser les projections, dans ce cas pour assembler une séquence de Either sans filtrage ou appel isLeft o isRight . Si vous êtes familier avec l'utilisation de Option sans correspondre ou appeler isDefined c'est une analogie.


En regardant curieusement l'actuel source d'Either J'ai vu que joinLeft y joinRight sont mises en œuvre avec une correspondance de motifs. Cependant, je suis tombé sur ceci une version plus ancienne de la source et a vu qu'il implémentait les méthodes de jointure en utilisant des projections :

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

1voto

Rich Oliver Points 1942

Je vous suggère d'ajouter les éléments suivants à votre paquetage d'utilitaires :

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

D'après mon expérience, la seule méthode utile fournie est le pli. Vous n'utilisez pas vraiment isLeft ou isRight dans du code fonctionnel. joinLeft et joinRight pourraient être utiles en tant que fonctions d'aplatissement comme l'explique Dider Dupont, mais je n'ai pas eu l'occasion de les utiliser de cette façon. L'exemple ci-dessus utilise Either comme biaisé à droite, ce qui, je pense, est la façon dont la plupart des gens les utilisent. C'est comme une option avec une valeur d'erreur au lieu de None.

Voici un peu de mon propre code. Je vous prie de m'excuser, ce n'est pas un code très abouti, mais c'est un exemple d'utilisation de Either dans un for comprehension. L'ajout des méthodes map et flatMap à Either nous permet d'utiliser la syntaxe spéciale des for comprehensions. Il analyse les en-têtes HTTP et renvoie soit une réponse de page d'erreur Http et Html, soit un objet de requête HTTP personnalisé analysé. Sans l'utilisation du for comprehension, le code serait très difficile à comprendre.

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X