问题
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?
回答1:
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
andUnsupportedOperationException
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.
回答2:
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.
回答3:
I think the part you may find surprising is that the Scala compiler emits this error because you deconstruct the tuple in place. This is surprisingly forces the compiler to check for withFilter
method because it looks to the compilers like an implicit check for the type of the value inside the container and checks on values are implemented using withFilter
. If you write your code as
for {
tmp <- getConfigs(args)
(value1: Seq[String], value2: String, value3: String) = tmp
// more stuff using those values
}
it should compile without errors.
回答4:
You can do this using Oleg's better-monadic-for compiler plugin:
build.sbt
:
addCompilerPlugin("com.olegpy" %% "better-monadic-for" % "0.2.4")
And then:
object Test {
def getConfigs: Either[Throwable, (String, String, String)] = Right(("a", "b", "c"))
def main(args: Array[String]): Unit = {
val res = for {
(fst, snd, third) <- getConfigs
} yield fst
res.foreach(println)
}
}
Yields:
a
This works because the plugin removes the unnecessary withFilter
and unchecked
while desugaring and uses a .map
call. Thus, we get:
val res: Either[Throwable, String] =
getConfigs
.map[String](((x$1: (String, String, String)) => x$1 match {
case (_1: String, _2: String, _3: String)
(String, String, String)((fst @ _), (snd @ _), (third @ _)) => fst
}));
来源:https://stackoverflow.com/questions/54014563/pattern-match-on-value-of-either-inside-a-for-comprehension