如何使用HList验证输入?

时间:2014-03-20 20:58:39

标签: 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

各种样本输入:

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

1 个答案:

答案 0 :(得分: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来验证不应编译的代码。

一些旁注:

  • 我最初用这个走了很长的花园小路,在那里我认为多态函数checkPoly1具有更具体的类型更重要,以表示返回类型所有情况都是布尔值。所以我一直试图让它与extends (Id ~>> Boolean)一起使用。但事实证明类型系统是否知道结果类型在每种情况下都是布尔值并不重要。这足以让我们实际拥有的唯一案例具有正确的类型。 extends Poly1是一件了不起的事。
  • 价值等级zip传统上允许不等长并丢弃附加内容。 Miles在Shapeless的类型zip中效仿,所以我们需要单独检查相同的长度。
  • 调用网站必须import nat._,这有点令人遗憾,否则无法找到Length的隐含实例。人们希望在定义站点处理这些细节。 (A fix is pending.
  • 如果我理解正确,我无法使用Mapped(la https://stackoverflow.com/a/21005225/86485)来避免长度检查,因为我的一些检查员(例如IsString)有单身人士比例更具体的类型Pred[String]
  • Travis指出Pred可以延长T => Boolean,从而可以使用ZipApply。我留下这个建议作为读者的练习: - )