6

我正在使用 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

以及各种样本输入:

val goodInput = 4 :: "foo" :: "" :: 5 :: HNil
val badInput = 4 :: "foo" :: "" :: 4 :: HNil
val malformedInput = 4 :: 5 :: "" :: 6 :: HNil

我将如何制作一个我可以有效执行的功能:

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

所以会发生以下情况:

f(spec, goodInput) // true
f(spec, badInput) // false
f(spec, malformedInput) // Does not compile
4

1 回答 1

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)
}

这是演示使用代码:

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
*/

请注意使用illTyped来验证不应编译的代码不编译。

一些旁注:

  • 我最初为此走了一条长长的花园小路,我认为多态函数check具有比 更具体的类型Poly1来表示在所有情况下的返回类型都是布尔值是很重要的。所以我一直试图让它与extends (Id ~>> Boolean). 但事实证明,类型系统是否知道结果类型在每种情况下都是布尔值。我们实际拥有的唯一案例具有正确的类型就足够了。extends Poly1是一件了不起的事情。
  • 价值级别zip传统上允许不等长度并丢弃额外内容。Miles 在 Shapeless 的 type-level 中效仿zip,所以我们需要单独检查相等的长度。
  • 调用站点必须这样做有点难过,否则找不到import nat._隐式实例。Length人们希望在定义站点处理这些细节。(正在等待修复。
  • 如果我理解正确,我不能使用Mapped(a la https://stackoverflow.com/a/21005225/86485)来避免长度检查,因为我的一些检查器(例如IsString)具有比只是更具体的单例类型例如Pred[String]
  • Travis 指出,它Pred可以扩展T => Boolean,使其可以使用ZipApply。我把这个建议作为练习留给读者:-)
于 2014-03-20T21:26:05.920 回答