I'm using Shapeless 2.0 and I'm trying to use HList to validate input — with as much of the checking as possible performed at compile time.
I have an HList spec
that specifies what type of input I'm expecting (the types should be checked at compile time) and may also include a runtime check to be performed (e.g., to test if a number is even or odd).
Consider the following specification:
trait Pred[T] { def apply(t: T): Boolean }
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
And various sample inputs:
val goodInput = 4 :: "foo" :: "" :: 5 :: HNil
val badInput = 4 :: "foo" :: "" :: 4 :: HNil
val malformedInput = 4 :: 5 :: "" :: 6 :: HNil
How would I make a function where I can effectively do:
input.zip(spec).forall{case (input, test) => test(input)}
So the following would happen:
f(spec, goodInput) // true
f(spec, badInput) // false
f(spec, malformedInput) // Does not compile
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:
check
to have a more specific type than Poly1
, to represent that the return type in all cases is Boolean. So I kept trying to make it work with extends (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.zip
traditionally allows unequal lengths and discards the extras. Miles followed suit in Shapeless's type-level zip
, so we need a separate check for equal lengths.import nat._
, otherwise the implicit instances for Length
aren't found. One would prefer these details to be handled at the definition site. (A fix is pending.)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]
.Pred
could extend T => Boolean
, making it possible to use ZipApply
. I leave following this suggestion as an exercise for the reader :-)If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With