14

Scala community.

Currently I'm trying to implement custom model/single parameter validation using cats Validated Monad. But, after removal of Cartesian product since 1.0 I'm unable to use (v1 |@| v2) map (f) and unable to compile my code:

import cats.Semigroupal
import cats.data.Validated.{Invalid, Valid}
import cats.data.{ValidatedNel, _}
import cats.implicits._
import cats.instances.all._

case class FieldErrorInfo(name: String, error: String)

type FieldName = String

type ValidationResult[A] = ValidatedNel[FieldErrorInfo, A]

trait SingleFieldValidationRule[U] extends ((U, FieldName) => ValidationResult[U])

trait ModelValidationRule[M] extends (M => ValidationResult[M])

object ValidateNameRule extends SingleFieldValidationRule[String] {
  override def apply(v1: String, name: String): ValidationResult[String]  = {
    if (v1.contains("cats"))
      v1.validNel
    else
      FieldErrorInfo(name, "Some Error").invalidNel
  }
}

object ValidateQuantityRule extends SingleFieldValidationRule[Int] {
  override def apply(v1: Int, name: String): ValidationResult[Int] =
    if (v1 > 0)
      v1.validNel
    else FieldErrorInfo(name, "Some Error").invalidNel
}

case class SampleModel(name: String, quantity: Int)

object ValidateSampleModel extends ModelValidationRule[SampleModel] {
  override def apply(v1: SampleModel): ValidationResult[SampleModel] = {
    val stage1: ValidatedNel[FieldErrorInfo, String] = ValidateNameRule(v1.name, "name")
    val stage2: ValidatedNel[FieldErrorInfo, Int] = ValidateQuantityRule(v1.quantity, "quantity")

    implicit val sga: Semigroupal[NonEmptyList] = new Semigroupal[NonEmptyList] {
      override def product[A, B](fa: NonEmptyList[A], fb: NonEmptyList[B]): NonEmptyList[(A, B)] = fa.flatMap(a => fb.map(b => a -> b))
    }

    (stage1, stage2).mapN(SampleModel)
  }
}

Compiler says, that

Error:(43, 23) value mapN is not a member of (cats.data.ValidatedNel[FieldErrorInfo,String], cats.data.ValidatedNel[FieldErrorInfo,Int])
    (stage1, stage2).mapN(SampleModel)
                     ^

Point me please how to use new Applicative syntax or what I did wrong...(forgot to create/import some implicits)

Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
dev
  • 145
  • 1
  • 7
  • If you are addressing the "Scala community" in your question, you should definitely tag it as `scala`, not just `scala-2.12` or `functional-programming`. There are much fewer people following specifically the `scala-cats` tag compared to number of people following `scala` in general. – Andrey Tyukin Apr 17 '18 at 09:00
  • @AndreyTyukin Thanks. By adding scala-2.12 tag my intent was to tell people which version of scala I use. – dev Apr 17 '18 at 09:19
  • 1
    This doesn't solve your case, but in case it helps anyone else: removing `import cats.implicits._` and adding `import cats.syntax.apply._` fixed the error for me – Lasf Jul 12 '18 at 13:39

2 Answers2

11

You seem to be missing the following import:

import cats.syntax.apply._

for the mapN.

Please ensure that you have -Ypartial-unification compiler flag activated, otherwise the compiler will have hard time extracting ValidatedNel[FieldErrorInfo, ?] from the types of stage1 and stage2:

libraryDependencies += "org.typelevel" %% "cats-core" % "1.1.0"
scalaVersion := "2.12.5"
scalacOptions += "-Ypartial-unification"

With the above settings, the following works:

import cats.Semigroupal
import cats.data.Validated.{Invalid, Valid}
import cats.data.ValidatedNel
import cats.data.NonEmptyList
import cats.syntax.apply._     // for `mapN`
import cats.syntax.validated._ // for `validNel`

case class FieldErrorInfo(name: String, error: String)

type FieldName = String

type ValidationResult[A] = ValidatedNel[FieldErrorInfo, A]

trait SingleFieldValidationRule[U] extends ((U, FieldName) => ValidationResult[U])

trait ModelValidationRule[M] extends (M => ValidationResult[M])

object ValidateNameRule extends SingleFieldValidationRule[String] {
  override def apply(v1: String, name: String): ValidationResult[String]  = {
    if (v1.contains("cats"))
      v1.validNel
    else
      FieldErrorInfo(name, "Some Error").invalidNel
  }
}

object ValidateQuantityRule extends SingleFieldValidationRule[Int] {
  override def apply(v1: Int, name: String): ValidationResult[Int] =
    if (v1 > 0)
      v1.validNel
    else FieldErrorInfo(name, "Some Error").invalidNel
}

case class SampleModel(name: String, quantity: Int)

object ValidateSampleModel extends ModelValidationRule[SampleModel] {
  override def apply(v1: SampleModel): ValidationResult[SampleModel] = {
    val stage1: ValidatedNel[FieldErrorInfo, String] = ValidateNameRule(v1.name, "name")
    val stage2: ValidatedNel[FieldErrorInfo, Int] = ValidateQuantityRule(v1.quantity, "quantity")

    implicit val sga: Semigroupal[NonEmptyList] = new Semigroupal[NonEmptyList] {
      override def product[A, B](fa: NonEmptyList[A], fb: NonEmptyList[B]): NonEmptyList[(A, B)] = fa.flatMap(a => fb.map(b => a -> b))
    }


    (stage1, stage2).mapN(SampleModel)
  }
}
Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
7

Values stage1 and stage2 must have type ValidationResult[_].

In this case implicit for mapN should work.

 object ValidateSampleModel extends ModelValidationRule[SampleModel] {
  override def apply(v1: SampleModel): ValidationResult[SampleModel] = {
    val stage1: ValidationResult[String] = ValidateNameRule(v1.name, "name")
    val stage2: ValidationResult[Int] = ValidateQuantityRule(v1.quantity, "quantity")

    (stage1, stage2).mapN(SampleModel)
  }
}
  • This is correct, but still took me some time to understand exactly what the answer is saying - the type must be ValidationResult[A] and not ValidationNel[A,B] – sksamuel Jan 02 '19 at 16:30
  • 3
    @monkjack Because `mapN` requires `implicit functor: Functor[F], semigroupal: Semigroupal[F]` where `F` (ValidationResult/ValidationNel) is a container with a single generic param. `ValidationResult[A]` has single param `A`. `ValidationNel[A,B]` has two type params `A` and `B` – Andrii Stefaniv Jan 09 '19 at 13:26
  • Ah that makes sense! – sksamuel Jan 09 '19 at 21:09