Как вернуть кортеж внутри EitherT
Я использую EitherT в Scalaz 7 для создания сложных понятий, которые смешивают State и \/. Все идет нормально; Я получаю то, что в основном:
State[MyStateType, MyLeftType \/ MyRightType]
и это позволяет мне создавать для-понимания, которые имеют хорошие переменные в левой части <-.
Но я не могу понять, как вернуть кортежи из государственного действия. Одиночные результаты просто хороши - в приведенном ниже коде "понимание в val" - именно то, чего я хочу достичь.
Но вещи рушатся, когда я хочу вернуть кортеж; "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 \/""")
}
}
Я не уверен, что это хорошая идея, но она решает проблему.
2 ответа
Как я отмечаю в комментарии выше, проблема в том, что версия вашего второго for
-conprehension включает в себя операцию фильтрации в 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
-понимания, поведение, которое вы видите в 2.10.2, является правильным и предназначенным - очевидно, совершенно ненужный вызов withFilter
здесь, чтобы остаться.
Вы можете использовать обходной путь в ответе Петра Пудлака, но также стоит отметить, что следующая версия без сахара также довольно ясна и лаконична:
val notAnotherComprehension = comprehension.flatMap {
case (x, y) => doSomething.map((x, y, _))
}
Это более или менее то, что я наивно ожидал for
-понимание, чтобы desugar, в любом случае (и я не единственный).
Не зная причину, я нашел возможный обходной путь:
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)
Это не очень хорошо, но делает работу.
(Я действительно ценю, что вы сделали автономный, рабочий пример проблемы.)