6

I'm trying to change code that uses cats validation, something like:

  case class Example(text: String, image: String)
  case class ValidExample(text: String, image: String)

  import cats.data.Validated._
  import cats.implicits._

  def validText(text: String) = if (text.nonEmpty) text.valid else invalid(-1)
  def validImage(image: String) = if (image.endsWith(".png")) image.valid else invalid(-1)
  val e = Example("test", "test.png")
  (validText(e.text), validImage(e.image)).mapN(ValidExample)

Which works fine.

But my change requires the image field to be an Option, like:

  case class Example(text: String, image: Option[String])
  case class ValidExample(text: String, image: Option[String])

  import cats.data.Validated._
  import cats.implicits._

  def validText(text: String) = if (text.nonEmpty) text.valid else invalid(-1)
  def validImage(image: String) = if (image.endsWith(".png")) image.valid else invalid(-1)
  val e = Example("test", Some("test.png"))
  (validText(e.text), e.image.map(validImage)).mapN(ValidExample)

With this the mapN fails because the types suddenly differ, it says:

value mapN is not a member of (cats.data.Validated[Int,String], Option[cats.data.Validated[Int,String]])

I want it to only validate if the value exists. So it should be part of the validation result if value is present but ignore the field otherwise. I know there are some ways of combining validations but in my real code that would be much more involved than something like this.

Is there a way to do this in a simple manner? I haven't been able to find anything about it in the docs or in search.

Thanks for help!

ChillyPro
  • 172
  • 7

2 Answers2

10

The answer is traverse, as usual :):

scala> val exampleWithImage = Example("test", Some("test.png"))
exampleWithImage: Example = Example(test,Some(test.png))

scala> val exampleWithoutImage = Example("test", None)
exampleWithoutImage: Example = Example(test,None)

scala> val badExampleWithImage = Example("test", Some("foo"))
badExampleWithImage: Example = Example(test,Some(foo))

scala> exampleWithImage.image.traverse(validImage)
res1: cats.data.Validated[Int,Option[String]] = Valid(Some(test.png))

scala> exampleWithoutImage.image.traverse(validImage)
res2: cats.data.Validated[Int,Option[String]] = Valid(None)

scala> badExampleWithImage.image.traverse(validImage)
res3: cats.data.Validated[Int,Option[String]] = Invalid(-1)

So it looks like traverse on Option does what you want: it validates the contents of Some, and ignores None (i.e. passes it through as valid).

So you could write the following, replacing map with traverse:

scala> (validText(e.text), e.image.traverse(validImage)).mapN(ValidExample)
res4: cats.data.Validated[Int,ValidExample] = Valid(ValidExample(test,Some(test.png)))

And you're done.

Travis Brown
  • 138,631
  • 12
  • 375
  • 680
  • i am getting a compile fail on cats 2.1.1 cmd28.sc:1: type mismatch; found : String => cats.data.Validated[Int,String] required: String => G[B] val res28 = exampleWithImage.image.traverse(validImage) ^ cmd28.sc:1: Could not find an instance of Applicative for G val res28 = exampleWithImage.image.traverse(validImage) – Tony Z Aug 07 '20 at 22:37
  • i see it, i need to add `interp.configureCompiler(_.settings.YpartialUnification.value = true)` – Tony Z Aug 08 '20 at 07:40
3

Call sequence on Option[Validated[String]], at place e.image.map(validImage)

import cats.data._
import cats.data.Validated._
import cats.implicits._

case class Example(text: String, image: Option[String])
case class ValidExample(text: String, image: Option[String])

def validText(text: String) = if (text.nonEmpty) text.valid else invalid(-1)
def validImage(image: String) = if (image.endsWith(".png")) image.valid else invalid(-1)
val e = Example("test", Some("test.png"))
println((validText(e.text), e.image.map(validImage).sequence).mapN(ValidExample))

produces

Valid(ValidExample(test,Some(test.png)))
Ivan Stanislavciuc
  • 7,140
  • 15
  • 18