I recently asked Map and reduce/fold over HList of scalaz.Validation and got a great answer as to how to transform a fixed sized tuple of Va[T]
(which is an alias for scalaz.Validation[String, T]
) into a scalaz.ValidationNel[String, T]
. I've since then been studying Shapeless and type level programming in general to try to come up with a solution that works on tuples of any size.
This is what I'm starting out with:
import scalaz._, Scalaz._, shapeless._, contrib.scalaz._, syntax.std.tuple._
type Va[A] = Validation[String, A]
// only works on pairs of Va[_]
def validate[Ret, In1, In2](params: (Va[In1], Va[In2]))(fn: (In1, In2) => Ret) = {
object toValidationNel extends Poly1 {
implicit def apply[T] = at[Va[T]](_.toValidationNel)
}
traverse(params.productElements)(toValidationNel).map(_.tupled).map(fn.tupled)
}
so then validate
is a helper I call like this:
val params = (
postal |> nonEmpty[String]("no postal"),
country |> nonEmpty[String]("no country") >=> isIso2Country("invalid country")
)
validate(params) { (postal, country) => ... }
I started out by taking any Product
instead of a pair and constraining its contents to Va[T]
:
// needs to work with a tuple of Va[_] of arbitrary size
def validateGen[P <: Product, F, L <: HList, R](params: P)(block: F)(
implicit
gen: Generic.Aux[P, L],
va: UnaryTCConstraint[L, Va],
fp: FnToProduct.Aux[F, L => R]
) = ???
I do have the feeling that simply adding the constraint only makes sure the input is valid but doesn't help at all with implementing the body of the function but I don't know how to go about correcting that.
traverse
then started complaining about a missing evidence so I ended up with:
def validateGen[P <: Product, F, L <: HList, R](params: P)(block: F)(
implicit
gen: Generic.Aux[P, L],
va: UnaryTCConstraint[L, Va],
tr: Traverser[L, toValidationNel.type],
fp: FnToProduct.Aux[F, L => R]
) = {
traverse(gen.to(params): HList)(toValidationNel).map(_.tupled).map(block.toProduct)
}
The compiler however continued to complain about a missing Traverser[HList, toValidationNel.type]
implicit parameter even though it's there.
Which additional evidence do I need to provide to the compiler in order for the traverse
call to compile? Has it got to do with the UnaryTCConstraint
not being declared in a way that is useful to the traverse
call, i.e. it cannot apply toValidationNel
to params
because it cannot prove that params
contains only Va[_]
?
P.S. I also found leftReduce Shapeless HList of generic types and tried to use foldRight
instead of traverse
to no avail; the error messages weren't too helpful when trying to diagnose which evidence the compiler was really lacking.
UPDATE:
As per what lmm has pointed out, I've removed the cast to HList
, however, the problem's now that, whereas in the non-generic solution I can call .map(_.tupled).map(block.toProduct)
on the result of the traverse
call, I'm now getting:
value map is not a member of shapeless.contrib.scalaz.Out
How come it's possible that it was possible on the result of the traverse(params.productElements)(toValidationNel)
call and not the generic traverse?
UPDATE 2:
Changing the Traverser[...]
bit to Traverser.Aux[..., Va[L]]
helped the compiler figure out the expected result type of the traversal, however, this only makes the validateGen
function compile successfully but yields another error at the call site:
[error] could not find implicit value for parameter tr: shapeless.contrib.scalaz.Traverser.Aux[L,toValidationNel.type,Va[L]]
[error] validateGen(params) { (x: String :: String :: HNil) => 3 }
[error] ^
I'm also getting the feeling here that the UnaryTCConstraint
is completely unnecessary — but I'm still too new to Shapeless to know if that's the case.
UPDATE 3:
Having realized the type that comes out of the traverser cannot be Va[L]
because L
itself is already a hlist of Va[_]
, I've split the L
type parameter to In
and Out
:
def validateGen[P <: Product, F, In <: HList, Out <: HList, R](params: P)(block: F)(
implicit
gen: Generic.Aux[P, In],
va: UnaryTCConstraint[In, Va], // just for clarity
tr: Traverser.Aux[In, toValidationNel.type, Va[Out]],
fn: FnToProduct.Aux[F, Out => R]
): Va[R] = {
traverse(gen.to(params))(toValidationNel).map(block.toProduct)
}
this compiles well — I'd be curious to find out how come the previous version with Va[L]
being the return value (i.e. the 3rd param to Traverser.Aux
) even compiled — however, at the call site, I now get:
Unspecified value parameters tr, fn