2

With Futures there is an easy way to transform Seq[Future] to a Future[Seq]:

Future.sequence(seqOfFutures)

I could not find an analog thing with Try.

It works with foldLeft but what I really like would have something like Try.sequence(seqOfTry).

Is there a reason that such a function is not provided?

How is this done properly?

Semantics:

A List of the values on Success: Success(Seq(1,2,3,4))

For Failure there are 2 possibilities:

  • Fails on the fist Failure and returns it. This is handled by this question: listtryt-to-trylistt-in-scala

  • Gathers all Failures and returns a 'compound' Failure.

Is there also a solution for the 'compound' Failure?

pme
  • 14,156
  • 3
  • 52
  • 95
  • 1
    I'd question the general utility of throwing out every `Success` because of a single `Failure`. – jwvh Sep 24 '19 at 07:18
  • What would be the semantics of such a function? – Yuval Itzchakov Sep 24 '19 at 07:35
  • @YuvalItzchakov I added the semantics to my question - I hope this is what you meant. – pme Sep 24 '19 at 07:49
  • 2
    @jwvh The same reasoning should apply to `Future`. – Alexey Romanov Sep 24 '19 at 07:58
  • 2
    Possible duplicate of [List\[Try\[T\]\] to Try\[List\[T\]\] in Scala](https://stackoverflow.com/questions/57516234/listtryt-to-trylistt-in-scala) – Tim Sep 24 '19 at 08:28
  • @AlexeyRomanov; True enough for success/failure, but `Future.sequence` also has the added meaning "not-complete-until-all-complete", which seems pretty useful. – jwvh Sep 24 '19 at 18:57

3 Answers3

5

As per Luis' suggestion Validated is designed for error accumulation so consider traverse like so

la.traverse(_.toEither.toValidatedNec)
lb.traverse(_.toEither.toValidatedNec)

which outputs

res2: cats.data.ValidatedNec[Throwable,List[Int]] = Invalid(Chain(java.lang.RuntimeException: boom, java.lang.RuntimeException: crash))
res3: cats.data.ValidatedNec[Throwable,List[Int]] = Valid(List(1, 2, 3))

where

import cats.syntax.traverse._
import cats.instances.list._
import cats.syntax.either._
import scala.util.{Failure, Success, Try}

val la: List[Try[Int]] = List(Success(1), Success(2), Failure(new RuntimeException("boom")), Success(3), Failure(new RuntimeException("crash")))
val lb: List[Try[Int]] = List(Success(1), Success(2), Success(3))

Without error accumulation we could just sequence like so

import cats.implicits._
la.sequence 

which outputs

res0: scala.util.Try[List[Int]] = Failure(java.lang.RuntimeException: boom)
Mario Galic
  • 47,285
  • 6
  • 56
  • 98
  • 2
    I just want to add that `Seq` (as used in the question) doesn't have a `Traverse` instance. If `List` is fine, then this is the way to go. – slouc Sep 24 '19 at 08:16
  • Is there also a version in cats that creates a compound Error? – pme Sep 24 '19 at 08:44
  • @pme could you turn the Trys into Eithers and compound all of the Lefts? – James Whiteley Sep 24 '19 at 11:43
  • 1
    @pme yes, but you would need to change your **Try** to **Validated**, or to **Either** and use its `Parallel` instance _(which in turn will use **Validated**)_. - Also, your error type should form a **Semigroup**, for it to be able to combine the errors. – Luis Miguel Mejía Suárez Sep 24 '19 at 11:53
  • 1
    @MarioGalic IMHO, it would be better to `x.toEither.toValidatedNec`. The `bimap` is a little bit too verbose, in general errors should be **NonEmpty** in case there is an invalid and finally the concatenation of **Lists** is terrible. Thus `NonEmptyChain` is _(for me)_ the best type to use. – Luis Miguel Mejía Suárez Sep 24 '19 at 12:13
  • @LuisMiguelMejíaSuárez In what sense is `List` concatenation terrible? – Mario Galic Sep 24 '19 at 17:00
  • Would also recommend `sequence` as @MarioGalic describes. It also works in scalaz. If you have a `Seq` you may need `.toList`. Here's a function I've with scalaz `ValidationNel` that collects separate validation errors with sequence. `def combine_validations(validations: ValidationNel[Throwable, Any]* ) = validations.toList.sequence[({type l[a] = ValidationNel[Throwable, a]})#l, Any].map(_ => ())`. The unusual map is there because I only want the failures. – jq170727 Sep 24 '19 at 17:05
  • @MarioGalic Sorry, I should not have used such a strong word without more context, I was referring to performance / complexity. In general terms `a ++ b` is **O(N)**, but since the list of errors are usually small, this should not matter a lot. However, `chain` concatenation is **O(1)**, which is pretty great. – Luis Miguel Mejía Suárez Sep 24 '19 at 19:05
5

This is a solution to the second question.

case class CompoundError(errs: List[Throwable]) extends Throwable

def toTry[T](list: List[Try[T]]): Try[List[T]] =
  list.partition(_.isSuccess) match {
    case (res, Nil) =>
      Success(res.map(_.get))
    case (_, errs) =>
      Failure(CompoundError(errs.collect { case Failure(e) => e }))
  }

The partition operation separates the successes and failures, and the match returns the appropriate value depending on whether there are any failures or not.


Previous solution:

case class CompoundError(errs: List[Throwable]) extends Throwable

def toTry[T](list: List[Try[T]]): Try[List[T]] = {
  val (res, errs) = list.foldLeft((List.empty[T], List.empty[Throwable])) {
    case ((res, errs), item) =>
      item match {
        case Success(t) => (t :: res, errs)
        case Failure(e) => (res, e :: errs)
      }
  }

  errs match {
    case Nil => Success(res.reverse)
    case _ => Failure(CompoundError(errs.reverse))
  }
}
Tim
  • 26,753
  • 2
  • 16
  • 29
0

Another version in case you want to return failure on the first error encountered, sharing my teammates util function here

 // Reduces many Trys into a single Try by transforming a Seq[Try[_ <: T]] into a Try[Seq[T]].
  // If any of the Trys are Failure, then this returns a Failure with the first error encountered
  def sequenceTrys[T](trySequence: Seq[_ <: Try[_ <: T]]): Try[Seq[T]] = {
    trySequence.foldLeft(Try(Seq.empty[T])) {
      (acc, tryElement) => acc.flatMap(accSeq => tryElement.map(success => accSeq :+ success))
    }
  }
raksja
  • 3,969
  • 5
  • 38
  • 44