如何使用HList验证输入?

Fra*_*can 6 scala shapeless

我正在使用Shapeless 2.0,我正在尝试使用HList来验证输入 - 尽可能在编译时执行检查.

我有一个HList spec,它指定我期望的输入类型(应该在编译时检查类型),还可以包括要执行的运行时检查(例如,测试数字是偶数还是奇数).

请考虑以下规范:

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
Run Code Online (Sandbox Code Playgroud)

各种样本输入:

val goodInput = 4 :: "foo" :: "" :: 5 :: HNil
val badInput = 4 :: "foo" :: "" :: 4 :: HNil
val malformedInput = 4 :: 5 :: "" :: 6 :: HNil
Run Code Online (Sandbox Code Playgroud)

如何才能有效地完成一项功能:

input.zip(spec).forall{case (input, test) => test(input)}
Run Code Online (Sandbox Code Playgroud)

所以会发生以下情况:

f(spec, goodInput) // true
f(spec, badInput) // false
f(spec, malformedInput) // Does not compile
Run Code Online (Sandbox Code Playgroud)

Set*_*sue 7

Travis Brown的这些答案包括了大部分需要:

但是我花了很长时间才找到答案,弄清楚它们适用于你的问题,并弄清楚合并和应用它们的细节.

我认为你的问题增加了价值,因为它证明了在解决实际问题时如何实现这一点,即验证输入.我还将尝试通过展示包括演示代码和测试的完整解决方案来增加下面的价值.

这是执行检查的通用代码:

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)
}
Run Code Online (Sandbox Code Playgroud)

这是演示使用代码:

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
*/
Run Code Online (Sandbox Code Playgroud)

注意使用illTyped来验证不应该编译的代码.

一些旁注:

  • 我最初用这个走了很长的花园路径,在那里我认为多态函数check有一个更具体的类型比Poly1表示所有情况下的返回类型都是布尔值更重要.所以我一直试着让它合作extends (Id ~>> Boolean).但事实证明类型系统是否知道结果类型在每种情况下都是布尔值并不重要.这足以让我们实际拥有的唯一案例具有正确的类型.extends Poly1是一件了不起的事.
  • zip传统上,价值水平允许不相等的长度并丢弃额外的东西.Miles在Shapeless的类型级别中效仿zip,因此我们需要单独检查相同的长度.
  • 调用站点必须有点悲伤import nat._,否则Length找不到隐式实例.人们希望在定义站点处理这些细节.(修复正在等待.)
  • 如果我理解正确,我不能使用Mapped(la /sf/answers/1470365781/)来避免长度检查,因为我的一些检查器(例如IsString)具有更具体的单例类型例如Pred[String].
  • 特拉维斯指出,Pred可以扩展T => Boolean,使其成为可能ZipApply.我留下这个建议作为读者的练习:-)