How to use HList to validate input?

I am using Shapeless 2.0 and I am trying to use HList to validate input - with the same validation as possible at compile time.

I have an HList spec that defines the type of input that I expect (types must be checked at compile time), and can also include a check at runtime (for example, to check 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 can I make a function where I can effectively do:

 input.zip(spec).forall{case (input, test) => test(input)} 

So the following will happen:

 f(spec, goodInput) // true f(spec, badInput) // false f(spec, malformedInput) // Does not compile 
+6
source share
1 answer

These Travis Brown answers include most of the required:

but it took me a long time to find the answers to them, to find out that they are applicable to your problem, and the details of their combination and application.

And I think that your question adds value, because it demonstrates how this can arise when solving a practical problem, namely checking input. I will also try adding the value below, showing the complete solution, including demo code and tests.

Here is the general code to check:

 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 is 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 be compiled does not work.

Some notes:

  • At first I went down a long garden path, where it seemed to me that it was important for the polymorphic check function to have a more specific type than Poly1 , to imagine that the return type was logical in all cases, so I continued to try to get it to work with extends (Id ~>> Boolean) . But it turns out it doesn't matter if the type system knows that the type of the result is Boolean in each case. It is enough that the only case we have is of the correct type. extends Poly1 is a wonderful thing.
  • The zip value level traditionally allows for unequal lengths and discards additional features. Miles followed the Shapeless zip example, so we need a separate check for equal length.
  • It is a little sad that the call site has import nat._ , otherwise implicit instances for Length not found. Prefer this data to be processed on the definition site. ( Bug fix. )
  • If I understand correctly, I can not use Mapped (a la fooobar.com/questions/696451 / ... ) to avoid length checking, because some of my checkers (for example, IsString ) have singleton types that are more specific, than just eg Pred[String] .
  • Travis points out that Pred can extend T => Boolean , which allows the use of ZipApply . I leave this sentence as an exercise for the reader :-)
+7
source

All Articles