How to return a tuple inside an EitherT
Asked Answered
C

2

9

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.

Corset answered 2/7, 2013 at 5:43 Comment(2)
There's something strange going on in the way 2.10.1+ desugars the for-comprehension here—see this question for a simplified version of the same issue.Loganiaceous
And to be clear, this is happening because filtering EitherT (or \/) requires a monoid instance for the left side, and for some reason 2.10.2 is sticking a filter operation in this for-comprehension.Loganiaceous
L
6

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

Loganiaceous answered 3/7, 2013 at 0:58 Comment(0)
E
6

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

Ellata answered 2/7, 2013 at 8:52 Comment(1)
Instead of the x = p._1; y = p._2; you can just do (x, y) = p. Feels strange that you need to do it on another line (link and comment from @TravisBrown has useful info).Corset
L
6

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

Loganiaceous answered 3/7, 2013 at 0:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.