Switching between EitherT and Validation to accumulate error or traverse
Asked Answered
M

0

6

Say I have the following function:

def getRemoteThingy(id: Id): EitherT[Future, NonEmptyList[Error], Thingy]

Given a List[Id], I can easily easily retrieve a List[Thingy] by using Traverse[List]:

val thingies: EitherT[Future, NonEmptyList[Error], List[Thingy]] = 
  ids.traverseU(getRemoteThingy)

It will use the Applicative instance for EitherT which will be based on flatMap so I will only get the first NonEmptyList[Error], it won't append all of them. Is that correct?

Now, if I actually want to accumulate errors, I can switch between EitherT and Validation. For example:

def thingies2: EitherT[Future, NonEmptyList[Error], List[Thingy]] = 
  EitherT(ids.traverseU(id => getRemoteThingy(id).validation).map(_.sequenceU.disjunction))

It works, I get all the errors at the end, but it is pretty cumbersome. I can make it simpler by using Applicative composition:

type ValidationNelError[A] = Validation[NonEmptyList[Error], A]
type FutureValidationNelError[A] = Future[ValidationNelError[A]]
implicit val App: Applicative[FutureValidationNelError] =
  Applicative[Future].compose[ValidationNelError]

def thingies3: EitherT[Future, NonEmptyList[Error], List[Thingy]] = 
  EitherT(
    ids.traverse[FutureValidationNelError, Thingy](id => 
      getRemoteThingy(id).validation
    ).map(_.disjunction)
  )

Longer than the others but all the plumbing can easily be shared across the code base.

What do you think of my solutions? Is there a more elegant way to solve this problem? How do you usually tackle it?

Thank you very much.

EDIT:

I have kind of a mad man solution using natural transformations to pimp Traversable. You apparently need type aliases for it to work, that's why I redefined getRemoteThingy:

type FutureEitherNelError[A] = EitherT[Future, NonEmptyList[String], A]

def getRemoteThingy2(id: Id): FutureEitherNelError[Thingy] = getRemoteThingy(id)

implicit val EitherTToValidation = new NaturalTransformation[FutureEitherNelError, FutureValidationNelError] {
  def apply[A](eitherT: FutureEitherNelError[A]): FutureValidationNelError[A] = eitherT.validation
}

implicit val ValidationToEitherT = new NaturalTransformation[FutureValidationNelError, FutureEitherNelError] {
  def apply[A](validation: FutureValidationNelError[A]): FutureEitherNelError[A] = EitherT(validation.map(_.disjunction))
}

implicit class RichTraverse[F[_], A](fa: F[A]) {
  def traverseUsing[H[_]]: TraverseUsing[F, H, A] = TraverseUsing(fa)
}

case class TraverseUsing[F[_], H[_], A](fa: F[A]) {
  def apply[G[_], B](f: A => G[B])(implicit GtoH: G ~> H, HtoG: H ~> G, A: Applicative[H], T: Traverse[F]): G[F[B]] =
    HtoG(fa.traverse(a => GtoH(f(a))))
}

def thingies4: FutureEitherNelError[List[Thingy]] = 
  ids.traverseUsing[FutureValidationNelError](getRemoteThingy2)
Mythological answered 30/4, 2015 at 14:2 Comment(2)
Not at all a duplicate, but see my similar question here.Dominick
Thanks @TravisBrown the answer is quite elegant, I like it. Maybe we need something like disjunctionedT or something to make it work with transformers. I'll give it a try.Mythological

© 2022 - 2024 — McMap. All rights reserved.