如何使用HList来验证输入?

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
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的类型级别zip中也遵循了这个规则,因此我们需要单独检查长度是否相等。 - 有点遗憾的是,调用方必须import nat._,否则Length的隐式实例就找不到了。我们希望在定义站点处理这些细节。(修复正在进行中。) - 如果我理解正确,我无法像https://dev59.com/0XvZa4cB1Zd3GeqP-hLL#21005225中的那样使用Mapped来避免长度检查,因为我的某些检查器(如IsString)具有比例如Pred[String]更具体的单例类型。 - Travis指出,Pred可以扩展T => Boolean,从而使使用ZipApply成为可能。我留给读者跟进此建议的练习 :-)

这真的很令人印象深刻,我还在努力掌握Shapeless,像这样的帖子对我帮助很大,谢谢。我正在尝试操作它,并删除了对Check.apply的隐式调用,期望Checker(malformedInput, spec)能够编译通过。它确实可以,但是Checker(badInput, spec)返回true。有什么想法吗?我无法解决这个问题。我原本以为只是删除了一些类型安全性,而不是改变了运行时行为。 - Noel M
正如Miles在Twitter上指出的那样,您现在可以编写(goodInput zipWith spec)(check) - 但要注意它不需要长度匹配。 - Travis Brown
@NoelM:听起来像是 https://github.com/milessabin/shapeless/pull/56。 你可以尝试在最新的快照版本中使用它吗? - Seth Tisue
@TravisBrown:糟糕,我的原始代码也没有要求长度匹配。现在已经更新并修复了(使用Length;添加了关于为什么不使用Mapped的说明)。 - Seth Tisue

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接