Is there a function that transforms/maps both Either's Left and Right cases taking two transformation functions respectively?

Viewed 892

I have not found a function in Scala or Haskell that can transform/map both Either's Left and Right cases taking two transformation functions at the same time, namely a function that is of the type

(A => C, B => D) => Either[C, D]

for Either[A, B] in Scala, or the type

(a -> c, b -> d) -> Either a b -> Either c d

in Haskell. In Scala, it would be equivalent to calling fold like this:

def mapLeftOrRight[A, B, C, D](e: Either[A, B], fa: A => C, fb: B => D): Either[C, D] =
  e.fold(a => Left(fa(a)), b => Right(fb(b)))

Or in Haskell, it would be equivalent to calling either like this:

mapLeftOrRight :: (a -> c) -> (b -> d) -> Either a b -> Either c d
mapLeftOrRight fa fb = either (Left . fa) (Right . fb)

Does a function like this exist in the library? If not, I think something like this is quite practical, why do the language designers choose not to put it there?

4 Answers

Don't know about Scala, but Haskell has a search engine for type signatures. It doesn't give results for the one you wrote, but that's just because you take a tuple argument while Haskell functions are by convention curried. https://hoogle.haskell.org/?hoogle=(a -> c) -> (b -> d) -> Either a b -> Either c d does give matches, the most obvious being:

mapBoth :: (a -> c) -> (b -> d) -> Either a b -> Either c d

...actually, even Google finds that, because the type variables happen to be exactly as you thought. (Hoogle also finds it if you write it (x -> y) -> (p -> q) -> Either x p -> Either y q.)

But actually, as Martijn said, this behaviour for Either is only a special case of a bifunctor, and indeed Hoogle also gives you the more general form, which is defined in the base library:

bimap :: Bifunctor p => (a -> b) -> (c -> d) -> p a c -> p b d

TBH I'm a bit disappointed that Hoogle doesn't by itself figure out to curry the signature or to swap arguments. Pretty sure it actually used to do that automatically, but at some point they simplified the algorithm because with the huge number of libraries the time it took and number of results got out of hand.

Cats provides Bifunctor, for example

import cats.implicits._

val e: Either[String, Int] = Right(41)
e.bimap(e => s"boom: $e", v => 1 + v)
// res0: Either[String,Int] = Right(42)

The behaviour you are talking about is a bifunctor behaviour, and would commonly be called bimap. In Haskell, a bifunctor for either is available: https://hackage.haskell.org/package/bifunctors-5/docs/Data-Bifunctor.html

Apart from the fold you show, another implementation in scala would be either.map(fb).left.map(fa)

There isn't such a method in the scala stdlib, probably because it wasn't found useful or fundamental enough. I can somewhat relate to that: mapping both sides in one operation instead of mapping each side individually doesn't come across as fundamental or useful enough to warrant inclusion in the scala stdlib to me either. The bifunctor is available in Cats though.

In Haskell, the method exists on Either as mapBoth and BiFunctor is in base.

In Haskell, you can use Control.Arrow.(+++), which works on any ArrowChoice:

(+++) :: (ArrowChoice arr) => arr a b -> arr c d -> arr (Either a c) (Either b d)
infixr 2 +++

Specialised to the function arrow arr ~ (->), that is:

(+++) :: (a -> b) -> (c -> d) -> Either a c -> Either b d

Hoogle won’t find +++ if you search for the type specialised to functions, but you can find generalised operators like this by replacing -> in the signature you want with a type variable: x a c -> x b d -> x (Either a b) (Either c d).

An example of usage:

renderResults
  :: FilePath
  -> Int
  -> Int
  -> [Either String Int]
  -> [Either String String]
renderResults file line column
  = fmap ((prefix ++) +++ show)
  where
    prefix = concat [file, ":", show line, ":", show column, ": error: "]
renderResults "test" 12 34 [Right 1, Left "beans", Right 2, Left "bears"]
  ==
  [ Right "1"
  , Left "test:12:34: error: beans"
  , Right "2"
  , Left "test:12:34: error: bears"
  ]

There is also the related operator Control.Arrow.(|||) which does not tag the result with Either:

(|||) :: arr a c -> a b c -> arr (Either a b) c
infixr 2 |||

Specialised to (->):

(|||) :: (a -> c) -> (b -> c) -> Either a b -> c

Example:

assertRights :: [Either String a] -> [a]
assertRights = fmap (error ||| id)
sum $ assertRights [Right 1, Right 2]
  ==
  3

sum $ assertRights [Right 1, Left "oh no"]
  ==
  error "oh no"

(|||) is a generalisation of the either function in the Haskell Prelude for matching on Eithers. It’s used in the desugaring of if and case in arrow proc notation.

Related