These answers by Travis Brown include most of what's needed:
but it took me a long time to find those answers, figure out that they were applicable to your problem, and work out the details of combining and applying them.
And I think your question adds value because it demonstrates how this can come up when solving a practical problem, namely validating input. I'll also try to add value below by showing a complete solution including demo code and tests.
Here's generic code for doing the checking:
object Checker {
import shapeless._, poly._, ops.hlist._
object check extends Poly1 {
implicit def apply[T] = at[(T, Pred[T])]{
case (t, pred) => pred(t)
}
}
def apply[L1 <: HList, L2 <: HList, N <: Nat, Z <: HList, M <: HList](input: L1, spec: L2)(
implicit zipper: Zip.Aux[L1 :: L2 :: HNil, Z],
mapper: Mapper.Aux[check.type, Z, M],
length1: Length.Aux[L1, N],
length2: Length.Aux[L2, N],
toList: ToList[M, Boolean]) =
input.zip(spec)
.map(check)
.toList
.forall(Predef.identity)
}
And here's the demo usage code:
object Frank {
import shapeless._, nat._
def main(args: Array[String]) {
val IsString = new Pred[String] { def apply(s: String) = true }
val IsOddNumber = new Pred[Int] { def apply(n: Int) = n % 2 != 0 }
val IsEvenNumber = new Pred[Int] { def apply(n: Int) = n % 2 == 0 }
val spec = IsEvenNumber :: IsString :: IsString :: IsOddNumber :: HNil
val goodInput = 4 :: "foo" :: "" :: 5 :: HNil
val badInput = 4 :: "foo" :: "" :: 4 :: HNil
val malformedInput1 = 4 :: 5 :: "" :: 6 :: HNil
val malformedInput2 = 4 :: "foo" :: "" :: HNil
val malformedInput3 = 4 :: "foo" :: "" :: 5 :: 6 :: HNil
println(Checker(goodInput, spec))
println(Checker(badInput, spec))
import shapeless.test.illTyped
illTyped("Checker(malformedInput1, spec)")
illTyped("Checker(malformedInput2, spec)")
illTyped("Checker(malformedInput3, spec)")
}
}
/*
results when run:
[info] Running Frank
true
false
*/
Note the use of illTyped
to verify that code that should not compile, does not.
Some side notes:
- I initially went down a long garden path with this, where I thought it would be important for the polymorphic function
check
to have a more specific type thanPoly1
, to represent that the return type in all cases is Boolean. So I kept trying to make it work withextends (Id ~>> Boolean)
. But it turns out not to matter whether the type system knows that the result type is the Boolean in every case. It's enough that the only case that we actually have has the right type.extends Poly1
is a marvelous thing. - Value-level
zip
traditionally allows unequal lengths and discards the extras. Miles followed suit in Shapeless's type-levelzip
, so we need a separate check for equal lengths. - It's a bit sad that the call site has to
import nat._
, otherwise the implicit instances forLength
aren't found. One would prefer these details to be handled at the definition site. (A fix is pending.) - If I understand correctly, I can't use
Mapped
(a la https://stackoverflow.com/a/21005225/86485) to avoid the length check, because some of my checkers (e.g.IsString
) have singleton types that are more specific than just e.g.Pred[String]
. - Travis points out that
Pred
could extendT => Boolean
, making it possible to useZipApply
. I leave following this suggestion as an exercise for the reader :-)