Как вернуть кортеж внутри EitherT

Я использую Scalaz 7 EitherT для создания for-computing, которые смешивают State и \ /. Все идет нормально; Я получаю то, что в основном:

State[MyStateType, MyLeftType \/ MyRightType]

и это позволяет мне создавать для понимания, у которых есть хорошие переменные в левой части ‹-.

Но я не могу понять, как вернуть кортежи из действия состояния. Одиночные результаты просто прекрасны - в приведенном ниже коде "val computing" - именно то, что я хочу добиться.

Но все разваливается, когда я хочу вернуть кортеж; "val otherComprehension" не позволит мне сделать

(a, b) <- comprehension

Похоже, он ожидает, что левая часть \ / будет моноидом, и я не понимаю, почему. Что мне не хватает?

(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)
}

Изменить: я добавил доказательства того, что MyLeftType - это монада, хотя это не так. В моем реальном коде MyLeftType - это класс case (называемый EarlyReturn), поэтому я могу указать ноль, но добавление работает только в том случае, если один из аргументов равен нулю:

  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 \/""")
      }
  }

Я не уверен, что это хорошая идея, но она решает проблему.


person James Moore    schedule 02.07.2013    source источник
comment
Что-то странное происходит в том, как 2.10.1+ десахарирует for-понимание здесь - см. этот вопрос для упрощенной версии. того же выпуска.   -  person Travis Brown    schedule 02.07.2013
comment
И для ясности, это происходит потому, что для фильтрации EitherT (или \/) требуется экземпляр моноида для левой стороны, и по какой-то причине 2.10.2 придерживается операции фильтрации в этом for-понимании.   -  person Travis Brown    schedule 02.07.2013


Ответы (2)


Как я отмечаю в комментарии выше, проблема в том, что обессахаренная версия вашего второго for-понимания включает в себя операцию фильтрации в 2.10.2 (и 2.10.1, но не в 2.10.0), и невозможно фильтровать EitherT ( или старый добрый \/) без экземпляра моноида для типа слева.

В следующем примере довольно легко понять, зачем нужен моноид:

val x: String \/ Int = 1.right
val y: String \/ Int = x.filter(_ < 0)

Что такое y? Ясно, что это должно быть какое-то «пустое» String \/ Int, а поскольку \/ смещено вправо, мы знаем, что это не может быть значением с этой стороны. Итак, нам нужен ноль для левой стороны, и экземпляр моноида для String предоставляет это - это просто пустая строка:

assert(y == "".left)

Согласно этому ответу на мой связанный с этим вопрос о шаблонах кортежей в for-computing, поведение, которое вы видите в 2.10.2, является правильным и преднамеренным - очевидно, совершенно ненужный вызов withFilter остается.

Вы можете использовать обходной путь в ответе Петра Пудлака, но также стоит отметить, что следующая версия без сахара также довольно четкая и краткая:

val notAnotherComprehension = comprehension.flatMap {
  case (x, y) => doSomething.map((x, y, _))
}

Это более или менее то, на что я наивно ожидал, что for-понимание обессахаривает в любом случае (а я не единственный).

person Travis Brown    schedule 03.07.2013

Не зная причины, я нашел возможное решение:

for {
  //(x, y) <- comprehension
  p <- comprehension

  z <- doSomething
} yield (p._1, p._2, z)

или, возможно, немного лучше

for {
  //(x, y) <- comprehension
  p <- comprehension
  (x, y) = p

  z <- doSomething
} yield (x, y, z)

Это не очень приятно, но работает.

(Я очень ценю, что вы создали автономный рабочий пример проблемы.)

person Petr    schedule 02.07.2013
comment
Вместо x = p._1; y = p._2; вы можете просто сделать (x, y) = p. Странно, что вам нужно сделать это в другой строке (ссылка и комментарий от @TravisBrown содержат полезную информацию). - person James Moore; 02.07.2013