Как накапливать Throwable в scalaz`е/нотации

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

val a: List[ Throwable \/ A] = ...
val b: List[ Throwable \/ B] = ...

и у меня есть функция, которая вычисляет конечный результат, например

def calc(a: A, b: B): Throwable \/ C = ...

Мне нужно рассчитать все результаты для каждого a и b, а также накопить Throwable, если они есть. Есть ли какой-нибудь элегантный способ, подобный аппликативному стилю <*>?

UPDT: результат должен быть следующим

val c: List[ Throwable \/ C] ..

Лучшее решение, к которому я пришел, это

def combine[A, B, C](f: (A, B) => Throwable \/ C, a: Throwable \/ A, b: Throwable \/ B): List[Throwable] \/ C = (a, b) match{
    case ( -\/(errA), \/-(_)) => -\/(List(errA))
    case (\/-(_), -\/(errB)) => -\/(List(errB))
    case (-\/(errA), -\/(errB)) => -\/(List(errA, errB))
    case (\/-(valA), \/-(valB)) => f(valA, valB).leftMap( List(_))
  }

И тогда

val result = (a |@| b)(combine(calc, _, _))

Но в этом случае у меня есть некоторые дополнительные результаты. И список результатов имеет тип List[ List[Throwable] \/ C]


person ponkin    schedule 05.12.2015    source источник


Ответы (2)


Я не уверен, что точно понимаю вопрос, но я попытаюсь ответить на него и, надеюсь, буду достаточно близок, чтобы вы могли экстраполировать. Предположим, у вас есть список значений типа Throwable \/ A и другого типа Throwable \/ B, и вы хотите объединить их попарно с функцией (A, B) => Throwable \/ C либо в список накопленных ошибок, либо в список C. Я напишу это так:

import scalaz._, Scalaz._

def process[A, B, C](
  as: List[Throwable \/ A],
  bs: List[Throwable \/ B]
)(f: (A, B) => Throwable \/ C): NonEmptyList[Throwable] \/ List[C] = ???

Есть много способов реализовать это, но основная идея заключается в том, что нам нужно временно преобразовать дизъюнкции в проверки, чтобы получить желаемое накопление ошибок (см. мой вопрос здесь для обсуждения того, почему это необходимо).

def process[A, B, C](
  as: List[Throwable \/ A],
  bs: List[Throwable \/ B]
)(f: (A, B) => Throwable \/ C): NonEmptyList[Throwable] \/ List[C] =
  as.zip(bs).traverseU {
    case (a, b) =>
      val validatedA: ValidationNel[Throwable, A] = a.validation.toValidationNel
      val validatedB: ValidationNel[Throwable, B] = b.validation.toValidationNel

      validatedA.tuple(validatedB).disjunction.flatMap {
        case (a, b) => f(a, b).leftMap(NonEmptyList(_))
      }.validation
  }.disjunction

Здесь мы преобразуем каждую пару значений в проверки, используем аппликатив tuple, чтобы объединить их при накоплении ошибок, преобразуем обратно в дизъюнкт, чтобы мы могли связать с f, обратно в проверку, чтобы мы могли аппликативно упорядочить с traverseU, а затем обратно к дизъюнкции.

(Обратите внимание, что я предполагаю, что списки имеют одинаковую длину или что вы не возражаете против игнорирования дополнительных результатов в любом из них, но это только ради простоты — было бы легко настроить, если бы вы хотели другое поведение. )

person Travis Brown    schedule 05.12.2015
comment
Идея состоит в том, чтобы объединить все хорошие значения из a со всеми хорошими значениями из b в функции calc, чтобы результатом был List[Throwable \/ C], и добавить все плохие значения из a и все плохие значения из b в окончательный список результатов. - person ponkin; 06.12.2015

Наконец я нашел подходящее решение. Идея состоит в том, чтобы использовать преобразователи монад EitherT.

val a: List[Throwable \/ A] = ...
val b: List[Throwable \/ B] = ...
val aT = EitherT.eitherT(a)
// aT: scalaz.EitherT[List,Throwable, A] = ...
val bT = EitherT.either(b)
// bT: scalaz.EitherT[List,Throwable, B] = ...

Далее идет магия:

val result = (aT |@| bT)(calc( _, _ ))
// c: scalaz.EitherT[[+A]List[A],Throwable,C] = ...

Здесь мы получаем все «хорошие» * значения из «а» и все «хорошие» значения из «б», как обычный вызов функции в аппликативном стиле. И никаких дополнительных функций или дополнительных данных в результате.

'*' - термин "хороший" означает не "выбрасываемый"

person ponkin    schedule 07.12.2015