Search code examples
scalashapelessscala-cats

Scala: How to transform an HList of ValidatedNel into a ValidatedNel of HList


I've been using cats.data.Validated successfully to solve the problem below, but have come into a problem using my existing solution for a case class with more than 22 members (because the constructor cannot be made into a Function).

Here is my goal: generate a bunch of ValidatedNel[E, T], sequence them into a ValidatedNel[E, (T1, T2, ...)], then mapN(DAOClass) (where DAOClass is a case class with the specified arguments). This works for fewer than 22 arguments, but fails with more because of two problems:

  1. (T1, T2, ...) cannot have more than 22 components
  2. DAOClass.apply cannot be transformed into a Function

So I am looking into using shapeless.HList to handle part 1 and having problems. I should be able to use Generic[DAOClass] to satisfactorily handle part 2 when I get to it, or if that doesn't work, use extensible records with a bit more boilerplate.

Here is some small example code (not with 22 components):

package example

import cats.syntax.validated._
import cats.data.ValidatedNel
import cats.sequence._
import shapeless._

case class DAOClass(a: Int, b: Int)

object DAOClass {

  def generate: ValidatedNel[String, DAOClass] = {

    val hlist: ValidatedNel[String, Int] :: ValidatedNel[String, Int] :: HNil =
      1.validNel :: 2.validNel :: HNil

    val hlistSequence: ValidatedNel[String, Int :: Int :: HNil] = hlist.sequence

    hlistSequence.map(Generic[DAOClass].from)

  }

}

This uses the kittens library to sequence the HList.

Unfortunately, this gives me a compile error:

[error] ...src/main/scala/example/DAOClass.scala:17:73: cannot construct sequencer, make sure that every item of your hlist shapeless.:: [cats.data.ValidatedNel[String,Int],shapeless.::[cats.data.ValidatedNel[String,Int],shapeless.HNil]] is an Apply
[error]     val hlistSequence: ValidatedNel[String, ::[Int, ::[Int, HNil]]] = hlist.sequence
[error]                                                                             ^

I have extracted this into a test project; here's my build.sbt:

lazy val root = (project in file(".")).
  settings(
    inThisBuild(List(
      organization := "com.example",
      scalaVersion := "2.12.6",
      version      := "0.1.0-SNAPSHOT"
    )),
    name := "shapeless-validation",
    resolvers ++= Seq(
      Resolver.sonatypeRepo("releases")
    ),
    libraryDependencies ++= Seq(
      "com.chuusai"   %% "shapeless" % "2.3.3",
      "org.scalatest" %% "scalatest" % "3.0.5" % "test",
      "org.typelevel" %% "cats-core" % "1.1.0",
      "org.typelevel" %% "kittens"   % "1.1.0"
    )
  )

What am I missing? Do I need to import more implicits somewhere? Is there a better way to do this?


Solution

  • You forgot to add

    scalacOptions += "-Ypartial-unification"
    

    to build.sbt. For normal work with cats this is usually mandatory.

    Now

    hlistSequence.map(Generic[DAOClass].from)
    

    produces a ValidatedNel[String, DAOClass]:

    println(DAOClass.generate) // Valid(DAOClass(1,2))