I'm using Scalaz 7's EitherT to construct for-comprehensions that blend State and \/. So far so good; I get something that's basically:
State[MyStateType, MyLeftType \/ MyRightType]
and that allows me to build for-comprehensions that have nice variables on the left side of the <-.
But I can't figure out how to return tuples from a state action. Single results are just fine - in the code below, "val comprehension" is exactly what I want to happen.
But things fall apart when I want to return a tuple; "val otherComprehension" won't let me do
(a, b) <- comprehension
It looks like it expects the left side of the \/ to be a Monoid and I don't understand why. What am I missing?
(Scalaz 7 2.0.0-SNAPSHOT, Scala 2.10.2)
object StateProblem {
case class MyStateType
case class MyRightType
case class MyLeftType
type StateWithFixedStateType[+A] = State[MyStateType, A]
type EitherTWithFailureType[F[+_], A] = EitherT[F, MyLeftType, A]
type CombinedStateAndFailure[A] = EitherTWithFailureType[StateWithFixedStateType, A]
def doSomething: CombinedStateAndFailure[MyRightType] = {
val x = State[MyStateType, MyLeftType \/ MyRightType] {
case s => (s, MyRightType().right)
}
EitherT[StateWithFixedStateType, MyLeftType, MyRightType](x)
}
val comprehension = for {
a <- doSomething
b <- doSomething
} yield (a, b)
val otherComprehension = for {
// this gets a compile error:
// could not find implicit value for parameter M: scalaz.Monoid[com.seattleglassware.StateProblem.MyLeftType]
(x, y) <- comprehension
z <- doSomething
} yield (x, y, z)
}
Edit: I've added evidence that MyLeftType is a monad, even though it's not. In my real code, MyLeftType is a case class (called EarlyReturn), so I can provide a zero, but append only works if one of the arguments is a zero:
implicit val partialMonoidForEarlyReturn = new Monoid[EarlyReturn] {
case object NoOp extends EarlyReturn
def zero = NoOp
def append(a: EarlyReturn, b: => EarlyReturn) =
(a, b) match {
case (NoOp, b) => b
case (a, NoOp) => a
case _ => throw new RuntimeException("""this isnt really a Monoid, I just want to use it on the left side of a \/""")
}
}
I'm not convinced this is a good idea, but it's solving the problem.
As I note in a comment above, the problem is that the desugared version of your second for
-comprehension involves a filtering operation in 2.10.2 (and 2.10.1, but not 2.10.0), and it's not possible to filter EitherT
(or plain old \/
) without a monoid instance for the type on the left side.
It's pretty easy to see why the monoid is necessary in the following example:
val x: String \/ Int = 1.right
val y: String \/ Int = x.filter(_ < 0)
What is y
? It's clear that it has to be some kind of "empty" String \/ Int
, and since \/
is right-biased, we know that it can't be a value on that side. So we need a zero for the left side, and the monoid instance for String
provides this—it's just the empty string:
assert(y == "".left)
According to this answer to my related question about tuple patterns in for
-comprehensions, the behavior you're seeing in 2.10.2 is correct and intended—the apparently completely unnecessary call to withFilter
is here to stay.
You can use the workaround in Petr Pudlák's answer, but it's also worth noting that the following sugar-free version is also pretty clear and concise:
val notAnotherComprehension = comprehension.flatMap {
case (x, y) => doSomething.map((x, y, _))
}
This is more or less what I would naïvely expect the for
-comprehension to desugar to, anyway (and I'm not the only one).
Without knowing the cause, I found a possible workaround:
for {
//(x, y) <- comprehension
p <- comprehension
z <- doSomething
} yield (p._1, p._2, z)
or perhaps slightly better
for {
//(x, y) <- comprehension
p <- comprehension
(x, y) = p
z <- doSomething
} yield (x, y, z)
It's not very nice, but does the job.
(I really appreciate that you made a self-contained, working example of the problem.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With