Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to return a tuple inside an EitherT

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.

like image 799
James Moore Avatar asked Jul 02 '13 05:07

James Moore


2 Answers

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

like image 180
Travis Brown Avatar answered Oct 13 '22 12:10

Travis Brown


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

like image 5
Petr Avatar answered Oct 13 '22 12:10

Petr