Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pattern match on value of Either inside a for comprehension?

I have a for comprehension like this:

for {
      (value1: String, value2: String, value3: String) <- getConfigs(args)
      // more stuff using those values
}

getConfigs returns an Either[Throwable, (Seq[String], String, String)] and when I try to compile I get this error:

value withFilter is not a member of Either[Throwable,(Seq[String], String, String)]

How can I use this method (that returns an Either) in the for comprehension?

like image 755
covfefe Avatar asked Jan 02 '19 23:01

covfefe


People also ask

What is a value matching pattern?

Pattern matching tests whether a given value (or sequence of values) has the shape defined by a pattern, and, if it does, binds the variables in the pattern to the corresponding components of the value (or sequence of values). The same variable name may not be bound more than once in a pattern.

What is for comprehension in Scala?

Scala offers a lightweight notation for expressing sequence comprehensions. Comprehensions have the form for (enumerators) yield e , where enumerators refers to a semicolon-separated list of enumerators. An enumerator is either a generator which introduces new variables, or it is a filter.


2 Answers

Like this:

for {
   tuple <- getConfigs()
} println(tuple)

Joking aside, I think that is an interesting question but it is misnamed a bit. The problem (see above) is not that for comprehensions are not possible but that pattern matching inside the for comprehension is not possible within Either.

There is documentation how for comprehensions are translated but they don't cover each case. This one is not covered there, as far as I can see. So I looked it up in my instance of "Programming in Scala" -- Second Edition (because that is the one I have by my side on dead trees).

Section 23.4 - Translation of for-expressions

There is a subchapter "Translating patterns in generators", which is what is the problem here, as described above. It lists two cases:

Case One: Tuples

Is exactly our case:

for ((x1, …, xn) <- expr1) yield expr2

should translate to expr1.map { case (x1, …, xn) => expr2). Which is exactly what IntelliJ does, when you select the code and do an "Desugar for comprehension" action. Yay! … but that makes it even weirder in my eyes, because the desugared code actually runs without problems.

So this case is the one which is (imho) matching the case, but is not what is happening. At least not what we observed. Hm?!

Case two: Arbitrary patterns

for (pat <- expr1) yield expr2

translates to

expr1 withFilter {
  case pat => true
  case _ => false
} map {
  case pat => expr2
}

where there is now an withFilter method! This case totally explains the error message and why pattern matching in an Either is not possible.

The chapter ultimately refers to the scala language specification (to an older one though) which is where I stop now.

So I a sorry I can't totally answer that question, but hopefully I could hint enough what is the root of the problem here.

Intuition

So why is Either problematic and doesn't propose an withFilter method, where Try and Option do? Because filter removes elements from the "container" and probably "all", so we need something that is representing an "empty container".

That is easy for Option, where this is obviously None. Also easy for e.g. List. Not so easy for Try, because there are multiple Failure, each one can hold a specific exception. However there are multiple failures taking this place:

  • NoSuchElementException and
  • UnsupportedOperationException

and which is why Try[X] runs, but an Either[Throwable, X] does not. It's almost the same thing, but not entirely. Try knows that Left are Throwable and the library authors can take advantage out of it.

However on an Either (which is now right biased) the "empty" case is the Left case; which is generic. So the user determines which type it is, so the library authors couldn't pick generic instances for each possible left.

I think this is why Either doesn't provide an withFilter out-of-the-box and why your expression fails.

Btw. the

expr1.map { case (x1, …, xn)  => expr2) }

case works, because it throws an MatchError on the calling stack and panics out of the problem which… in itself might be a greater problem.

Oh and for the ones that are brave enough: I didn't use the "Monad" word up until now, because Scala doesn't have a datastructure for it, but for-comprehensions work just without it. But maybe a reference won't hurt: Additive Monads have this "zero" value, which is exactly what Either misses here and what I tried to give some meaning in the "intuition" part.

like image 112
tilois Avatar answered Sep 19 '22 00:09

tilois


I guess you want your loop to run only if the value is a Right. If it is a Left, it should not run. This can be achieved really easy:

for {
  (value1, value2, value3) <- getConfigs(args).right.toOption
  // more stuff using those values
}

Sidenote: I don't know whats your exact use case, but scala.util.Try is better suited for cases where you either have a result or a failure (an exception).
Just write Try { /*some code that may throw an exception*/ } and you'll either have Success(/*the result*/) or a Failure(/*the caught exception*/).
If your getConfigs method returns a Try instead of Either, then your above could would work without any changes.

like image 39
Aki Avatar answered Sep 20 '22 00:09

Aki