Search code examples
scalafunctional-programmingscala-cats

How to sum a Set[ValidatedNel[String, Double]]?


I have this:

Set[ValidatedNel[String, Double]] 

and I would like to sum the Doubles in it to get:

ValidatedNel[String, Double]

If some elements in the values are then I would like to have matching strings.

I played with Set.sum and Numeric to no avail...

Here is the test of what I would like to achieve:

  test("Summing ValidatedNel works") {
    val val1: ValidatedNel[String, Double] = Valid(1.0)
    val val2: ValidatedNel[String, Double] = Valid(2.0)
    val values: Set[ValidatedNel[String, Double]] = Set(val1, val2)

    val validatedNelNumeric: Numeric[ValidatedNel[String, Double]] = ???
    val sum = values.sum(validatedNelNumeric)

    assert(sum == Valid(3.0))
  }

I don't manage to create the validatedNelNumeric...


Solution

  • To start with: it feels a little weird to use a set in this case (for a collection of Validated[..., Double] values). What part of the Set semantics do you care about? The unorderedness? Uniqueness?

    In general the most straightforward way to sum up elements that have a Monoid instance is to use the combineAll method for things with a Foldable instance—for example a List (but not Set).

    import cats.data.{ Validated, ValidatedNel }
    import cats.instances.double._, cats.instances.list._
    import cats.syntax.foldable._
    // or just import cats.implicits._
    
    val val1: ValidatedNel[String, Double] = Validated.valid(1.0)
    val val2: ValidatedNel[String, Double] = Validated.valid(2.0)
    val bad1: ValidatedNel[String, Double] = Validated.invalidNel("foo")
    val bad2: ValidatedNel[String, Double] = Validated.invalidNel("bar")
    
    val values = Set(val1, val2)
    val withSomeBadOnes = Set(val1, bad1, val2, bad2)
    

    And then:

    scala> values.toList.combineAll
    res0: cats.data.ValidatedNel[String,Double] = Valid(3.0)
    
    scala> withSomeBadOnes.toList.combineAll
    res1: cats.data.ValidatedNel[String,Double] = Invalid(NonEmptyList(foo, bar))
    

    I'm guessing that's what you mean by "If some elements in the values are then I would like to have matching strings"?

    You could also use SortedSet, since Cats provides a Foldable instance for SortedSet, but it's not as convenient:

    scala> import cats.implicits._
    import cats.implicits._
    
    scala> import scala.collection.immutable.SortedSet
    import scala.collection.immutable.SortedSet
    
    scala> (SortedSet.empty[ValidatedNel[String, Double]] ++ values).combineAll
    res2: cats.data.ValidatedNel[String,Double] = Valid(3.0)
    
    scala> (SortedSet.empty[ValidatedNel[String, Double]] ++ withSomeBadOnes).combineAll
    res3: cats.data.ValidatedNel[String,Double] = Invalid(NonEmptyList(bar, foo))
    

    You could also use the standard fold and the |+| operator for monoids:

    scala> values.fold(Validated.valid(0.0))(_ |+| _)
    res4: cats.data.ValidatedNel[String,Double] = Valid(3.0)
    

    To sum up: you can't call combineAll directly on your Set, since Cats doesn't provide a Foldable for Set. I'd suggest carefully reconsidering your use of the Set in any case, but if you decide to stick with it, you have a few options: convert to List or SortedSet like I have above, use the standard fold on Set, or finally write your own Foldable[Set] or use the one from alleycats.