4

What I have is a List[Validated[NonEmptyList[ErrorType], T]] and I wish to flatten this into a Validated[NonEmptyList[ErrorType], T]

My poor attempt is to collect all the errors and then make a new one.

val errors = validations collect {
   case Invalid(errs) => errs.toList
}
if (errors.nonEmpty) Invalid(NonEmptyList.fromListUnsafe(errors.flatten)) else Valid(t)

I've tried foldMap and traverse but invarialby run into issues that I don't have a Monoid in scope, and I feel like cats probably provides this out of the box in a one liner.

Some of the older answers on this kind of question seem to be out of date (or at least I can't find the functions they're referring to).

Update:

I have fixed the type.

sksamuel
  • 16,154
  • 8
  • 60
  • 108
  • Do you have `cats._; cats.implicits._` in scope? – Yuval Itzchakov Dec 12 '17 at 22:39
  • `cats.implicits._` yes, but what should I be doing to flatten? – sksamuel Dec 12 '17 at 23:19
  • Could you clarify the type of the variable `t` in your example? Is that a single value (something like `Option[T]`)? Is it a `Seq[T]` of values? In the latter case which value of type `T` do you want to be returned? Is it something else? – SergGr Dec 13 '17 at 01:18
  • It's a case class. – sksamuel Dec 13 '17 at 01:43
  • It looks like I miss something. If `t` is just an instance of some case class, where `map` comes from? P.S. If you want some user to be notified about your comment - you could mention name such as @SergGr – SergGr Dec 13 '17 at 04:49
  • Do you mean `List[Validated[ErrorType,NonEmptyList[T]]]` instead? You have this consistently wrong, as `NonEmptyList` takes 1 parameter and `Validated` takes two. – ziggystar Dec 13 '17 at 06:57
  • 1
    @ziggystar, I suspect that it should be `List[Validated[NonEmptyList[ErrorType], T]]`. It makes obvious sense to have a `NonEmptyList[ErrorType]`. Actually there is stanard type alias for such type in cats: `type ValidatedNel[+E, +A] = Validated[NonEmptyList[E], A]` – SergGr Dec 13 '17 at 07:40
  • @ziggystar SergGr is correct, I had put the ] in the wrong place. – sksamuel Dec 14 '17 at 11:24
  • @monkjack, could you also answer my question about the type of `t` and the logic behind `map`? – SergGr Dec 14 '17 at 21:16
  • @SergGr T is a case class, and I validate subsets of its fields independently, and then want to merge. I just realized I had left in the map operation which isn't relevant to this so I have removed it. – sksamuel Dec 15 '17 at 09:45

3 Answers3

1

To answer your question, this example uses kind projector compiler plugin for partial application of type parameters. Depending on how familiar you are with Scala terminology, that may not make complete sense. Basically it means that some Scala methods expect arguments of a certain shape, and you have to help the compiler with that by adding a question mark in the appropriate place.

Also be sure to add some imports.

import cats._
import cats.data._
import cats.implicits._
import Validated.{ valid, invalid }

Then given,

val a = valid[String,Int](1)
val as = List(a)

you can use List.sequence to convert a List[Validated[NonEmptyList[E, T]]] to a Validated[E, List[T]].

as.sequence[Validated[String,?], Int]

In this case if you use a single String as the error type, the Strings that represent the error type will be concatenated together into a single string. This is where the monoid constraint comes from that you mentioned - it helps combine the strings with string concatenation. Other error types may work as well, such as List[String] or List[ErrorType] for the error type instead of a simple String. In the case where you want to accumulate errors, use a List of some error type,

val a = valid[List[String],Int](1)
val b = invalid[List[String], Int]("some".lift[List])
List(a,b).sequence[Validated[List[String],?], Int]

results in

res29: Validated[List[String], List[Int]] = Invalid(List("some"))

The lift operation is needed above to go from a String to a List of String.

Alan Effrig
  • 763
  • 4
  • 10
  • Thank you for the comprehensive answer, but had typed out the wrong type. It should have been `List[Validated[NonEmptyList[ErrorType], T]]`. Really sorry for the confusion! Upvoted anyway. – sksamuel Dec 14 '17 at 11:26
1

If you're using sbt-partial-unification or just -Ypartial-unification compiler flag (which you should), you can use:

validation.sequence.map(_.head)

This is unsafe (it only works if the list of validation was not empty). Now, if you construct the instance first and then validate it later:

val t: T = ???

val validation: List[Validated[NonEmptyList[ErrorType], T]] = 
   List(checkQuantities, checkName, checkConsisteny).map(f => f(t))

And you still have it in scope, you can provide it directly:

validation.sequence_.as(t)
Oleg Pyzhcov
  • 7,323
  • 1
  • 18
  • 30
1

I literally just ran into a similar issue yesterday. This is how I solved it (I'm using Scala 2.12 and Cats 1.1.0):

import cats.Traverse
import cats.data.{NonEmptyList, Validated}

val list: List[Validated[NonEmptyList[ErrorType], T]] = ...

import cats.instances.list._
val validated: Validated[NonEmptyList[ErrorType], List[T]] = Traverse[List].sequence(list)

It's not quite what you want as this is flattening the structure into a List[A] instead of A. In order to flatten A to the same type, you'll probably need to provide a Monoid[A] somewhere that implements the combine method.

Luis Medina
  • 1,112
  • 1
  • 11
  • 20