Search code examples
scalavalidationparameterizedscala-cats

Scala Cats Validated with Parameterized Validation Result


I am following the example in https://typelevel.org/cats/datatypes/validated.html but with a twist: the type of the elements in the NEL is not a DomainValidation but a DomainValidation[A] and I'm not sure how to do this. Below is my attempt but am having trouble with the types. This is probably just as much a Scala question as it is a Cats validated question.

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

sealed trait Widget
case object WidgetA extends Widget
case object WidgetB extends Widget

object Widget{
  val values = List(WidgetA, WidgetB)
  def fromString(s: String): Option[Widget] = values.filter(_.toString == s).headOption
}

case class Order( quantity: Float, widget: Widget )

trait MyValidation[A] {
  def errorMessage: String
  def is: A
}

type ValidationResult[A,B] = ValidatedNel[MyValidation[A], B]

case class WidgetQuantityIsNegative(is: Float) extends MyValidation[Float] {
  def errorMessage = s"$is widget quantity is negative"
}

case class WidgetTypeIsInvalid(is: String) extends MyValidation[String] {
  def errorMessage = s"$is is not a valid widget type"
}

def validateQuantity(q: Float): ValidationResult[Float, Float] =
  if(q >= 0) q.validNel else WidgetQuantityIsNegative(q).invalidNel

/*
Here I would like to validate the string.
If it represents a valid Widget, return the valid Widget
else return the invalid */
def validateWidgetType(s: String) =
  Widget.fromString(s) map {widget => widget.validNel } getOrElse WidgetTypeIsInvalid.invalidNel

def validateOrder( quantity: Float, s: String ): ValidationResult[MyValidation[Any], Order] = // NOT SURE IF ANY IS THE RIGHT TYPE
  (
    (validateQuantity(quantity)),
    (validateWidgetType(s))
  ).mapN[Order](Order.apply) // THIS DOES NOT COMPILE

Solution

  • I tweaked your code a bit and this compiles:

    import cats.data._
    import cats.implicits._
    
    sealed trait Widget
    case object WidgetA extends Widget
    case object WidgetB extends Widget
    
    object Global {
    
      object Widget {
        val values = List(WidgetA, WidgetB)
    
        def fromString(s: String): Option[Widget] = values.find(_.toString == s)
      }
    
      case class Order(quantity: Float, widget: Widget)
    
      trait MyValidation[+A] {
        def errorMessage: String
    
        def is: A
      }
    
      type ValidationResult[A, B] = ValidatedNel[MyValidation[A], B]
    
      case class WidgetQuantityIsNegative(is: Float) extends MyValidation[Float] {
        def errorMessage = s"$is widget quantity is negative"
      }
    
      case class WidgetTypeIsInvalid(is: String) extends MyValidation[String] {
        def errorMessage = s"$is is not a valid widget type"
      }
    
      def validateQuantity(q: Float): ValidationResult[Float, Float] =
        if (q >= 0) q.validNel else WidgetQuantityIsNegative(q).invalidNel
    
      /*
    Here I would like to validate the string.
    If it represents a valid Widget, return the valid Widget
    else return the invalid string */
      def validateWidgetType(s: String): ValidationResult[String, Widget] =
        Widget.fromString(s) map { widget => widget.validNel } getOrElse WidgetTypeIsInvalid(s).invalidNel
    
      def validateOrder(quantity: Float, s: String): ValidationResult[Any, Order] = 
        (
          validateQuantity(quantity),
          validateWidgetType(s)
        ).mapN[Order](Order(_, _)) // Order.apply without _ works too but would add one more red line in intellij
    
    } 
    

    Essentially all I did was mark MyValidation is covariant and fixed both validateWidgetType and validateQuantity to return the same type


    NOTE on the covariance:

    when we remove the covariance annotation then the following compiler message appears:

    Error:(50, 18) type mismatch;
     found   : cats.data.Validated[cats.data.NonEmptyList[cats.Global.MyValidation[_ >: String with Float]],cats.Global.Order]
     required: cats.Global.ValidationResult[Any,cats.Global.Order]
        (which expands to)  cats.data.Validated[cats.data.NonEmptyList[cats.Global.MyValidation[Any]],cats.Global.Order]
    Note: cats.data.NonEmptyList[cats.Global.MyValidation[_ >: String with Float]] <: Any, but class Validated is invariant in type A.
    You may wish to define A as +A instead. (SLS 4.5)
        ).mapN[Order](Order.apply)
    

    which more or less says that a concrete type which is both a subtype of Float and String is not exactly an Any but a subtype of Any