Shapeless HList类型检查

9
我正在使用Shapeless,以下是我用来计算两个HList之间差异的方法:
  def diff[H <: HList](lst1: H, lst2:H):List[String] = (lst1, lst2) match {
    case (HNil, HNil)                 => List()
    case (h1::t1, h2::t2) if h1 != h2 => s"$h1 -> $h2" :: diff(t1, t2)
    case (h1::t1, h2::t2)             => diff(t1, t2)
    case _                            => throw new RuntimeException("something went very wrong")
  }

由于该方法的两个参数都采用了H,因此我预期不同类型的HList在此处无法编译通过。例如:

diff("a" :: HNil, 1 :: 2 :: HNil)

虽然不应该编译通过,但它确实编译通过了,并且会产生运行时错误:java.lang.RuntimeException: something went very wrong。有没有什么方法可以对类型参数进行操作,使得这个方法只能接受两个具有相同类型的边?


你似乎没有处理lst1lst2其中一个为空的情况,这很可能是导致错误的原因。 - Régis Jean-Gilles
我理解这个错误,但我想要一个编译错误,而不是运行时错误。 - triggerNZ
1
哦,我明白你想要实现什么。不幸的是,基本的 HList 特质没有参数化,所以在你的方法调用中 H 只会被解析为 Hlist(无论具体元素类型如何,它确实是任何 Hlist 的超类型)。请看我的回答。 - Régis Jean-Gilles
3个回答

10

其他答案没有涉及的一个问题是,这完全是一种类型推断问题,可以通过将参数列表分成两部分来解决:

def diff[H <: HList](lst1: H)(lst2: H): List[String] = (lst1, lst2) match {
  case (HNil, HNil)                 => List()
  case (h1::t1, h2::t2) if h1 != h2 => s"$h1 -> $h2" :: diff(t1)(t2)
  case (h1::t1, h2::t2)             => diff(t1)(t2)
  case _                            => throw new RuntimeException("bad!")
}

这给了我们想要的结果:

scala> diff("a" :: HNil)(1 :: 2 :: HNil)
<console>:15: error: type mismatch;
 found   : shapeless.::[Int,shapeless.::[Int,shapeless.HNil]]
 required: shapeless.::[String,shapeless.HNil]
       diff("a" :: HNil)(1 :: 2 :: HNil)
                           ^

这段代码可以正常工作(编译时不会出错并在运行时崩溃),因为Scala对于方法的类型推断是基于每个参数列表来进行的。如果lst1lst2在同一个参数列表中,H将被推断为它们的最小上界,一般来说这不是你想要的。

如果你把lst1lst2放在不同的参数列表中,那么当编译器看到lst1时就会决定H是什么。如果lst2的类型不相同,程序会崩溃(这正是我们想要的)。

你仍然可以通过显式设置HHList来破坏这个规则,但这就要由你自己承担风险了。


是的,这是一个类型推断问题。而且引入第二个参数列表是一种克服这个问题的方法。实际上,这正是我的解决方案所利用的(在这种情况下,第二个参数列表包含隐式证据,而不是lst2)。你的解决方案稍微简单一些,而我的则稍微更自然易用(我猜)。 - Régis Jean-Gilles

7

很不幸,基础的HList特质没有参数化,在你的方法调用中,H只会被解析为Hlist(无论具体元素类型如何,它确实是任何Hlist的超类型)。

为了修复这个问题,我们需要改变定义,并依赖于广义类型约束:

def diff[H1 <: HList, H2 <: HList](lst1: H1, lst2: H2)(implicit e: H1 =:= H2): List[String] = (lst1, lst2) match {
  case (HNil, HNil)                 => List()
  case (h1::t1, h2::t2) if h1 != h2 => s"$h1 -> $h2" :: diff(t1, t2)
  case (h1::t1, h2::t2)             => diff(t1, t2)
  case _                            => throw new RuntimeException("something went very wrong")
}

让我们来检查一下:

scala> diff("a" :: HNil, 1 :: 2 :: HNil)
<console>:12: error: Cannot prove that shapeless.::[String,shapeless.HNil] =:= shapeless.::[Int,shapeless.::[Int,shapele
              diff("a" :: HNil, 1 :: 2 :: HNil)
                  ^

scala> diff("a" :: HNil, "b" :: HNil)
res5: List[String] = List(a -> b)

scala> diff("a" :: 1 :: HNil, "b" :: 2 :: HNil)
res6: List[String] = List(a -> b, 1 -> 2)

现在我们仍然可以“欺骗”,并将H1和H2明确设置为HList,这样我们又回到了原点。

scala> diff[HList, HList]("a" :: HNil, 1 :: 2 :: HNil)
java.lang.RuntimeException: something went very wrong
  at .diff(<console>:15)
  at .diff(<console>:13)

很遗憾,我认为这不容易解决(当然,它是可以解决的,但我没有快速的解决方案)。


谢谢!这正是我想要的。 - triggerNZ
有一个稍微简单一些的解决方案:https://dev59.com/d4zda4cB1Zd3GeqPoZUr#31192042。 - Travis Brown

1
我可以提供一个更严格的变体,它不能通过显式类型参数来欺骗。
object diff {
    class Differ[T <: HList](val diff: (T, T) => List[String])

    def apply[T <: HList](l1: T, l2: T)(implicit differ: Differ[T]): List[String] = differ.diff(l1, l2)

    implicit object NilDiff extends Differ[HNil]((_, _) => Nil)

    implicit def ConsDiff[H, T <: HList : Differ] = new Differ[H :: T]({
      case (h1 :: t1, h2 :: t2) if h1 != h2 => s"$h1 -> $h2" :: diff(t1, t2)
      case (h1 :: t1, h2 :: t2) => diff(t1, t2)
    })
  }

这肯定比上面那个复杂得多,我试过使用多态函数,但无法编译出合适的递归。

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