回答问题一至三: HLists
的主要应用之一是抽象化参数数量。在任何给定的抽象使用点,参数数量通常是静态已知的,但在不同的使用点会有所变化。例如,来自shapeless的examples:
def flatten[T <: Product, L <: HList](t : T)
(implicit hl : HListerAux[T, L], flatten : Flatten[L]) : flatten.Out =
flatten(hl(t))
val t1 = (1, ((2, 3), 4))
val f1 = flatten(t1)
val l1 = f1.toList
val t2 = (23, ((true, 2.0, "foo"), "bar"), (13, false))
val f2 = flatten(t2)
val t2b = f2.tupled
如果不使用HLists
(或等效物)来抽象化元组参数的数量,那么将无法有一个单一的实现可以接受这两种非常不同形状的参数并以类型安全的方式进行转换。
在涉及固定元数的任何地方,抽象化元数的能力可能会引起兴趣:除了上面的元组之外,还包括方法/函数参数列表和案例类。请参见此处,了解如何抽象化任意案例类的元数以几乎自动地获得类型类实例的示例。
case class Foo(i : Int, s : String)
case class Bar(b : Boolean, s : String, d : Double)
implicit def fooIso = Iso.hlist(Foo.apply _, Foo.unapply _)
implicit def barIso = Iso.hlist(Bar.apply _, Bar.unapply _)
implicitly[Monoid[Foo]]
val f = Foo(13, "foo") |+| Foo(23, "bar")
assert(f == Foo(36, "foobar"))
implicitly[Monoid[Bar]]
val b = Bar(true, "foo", 1.0) |+| Bar(false, "bar", 3.0)
assert(b == Bar(true, "foobar", 4.0))
这里没有运行时的迭代,但是有重复的代码,使用HLists
(或类似结构)可以消除重复。当然,如果你对重复的样板代码的容忍度很高,你也可以为每个你关心的形状编写多个实现,从而达到相同的结果。
在第三个问题中,你问:“......如果你在一个hlist上映射的函数f如此通用,以至于它接受所有元素......为什么不通过productIterator.map使用它呢?” 如果你在一个HList上映射的函数确实是Any => T
的形式,那么在productIterator
上进行映射就可以完美地为你服务。但是,Any => T
形式的函数通常并不那么有趣(至少在内部它们不会进行类型转换)。shapeless提供了一种多态函数值的形式,允许编译器按照你怀疑的方式选择特定于类型的情况。例如,
object size extends Poly1 {
implicit def default[T] = at[T](t => 1)
implicit def caseString = at[String](_.length)
implicit def caseList[T] = at[List[T]](_.length)
}
scala> val l = 23 :: "foo" :: List('a', 'b') :: true :: HNil
l: Int :: String :: List[Char] :: Boolean :: HNil =
23 :: foo :: List(a, b) :: true :: HNil
scala> (l map size).toList
res1: List[Int] = List(1, 3, 2, 1)
关于您的第四个问题,涉及用户输入,有两种情况需要考虑。第一种是我们可以动态建立一个上下文,以确保已知的静态条件得以满足的情况。在这些场景中,完全可以应用形状库技术,但是需要注意的是,如果静态条件在运行时不满足,那么我们必须采取替代路径。毫不奇怪的是,敏感于动态条件的方法必须产生可选结果。以下是使用 HList 的示例。
trait Fruit
case class Apple() extends Fruit
case class Pear() extends Fruit
type FFFF = Fruit :: Fruit :: Fruit :: Fruit :: HNil
type APAP = Apple :: Pear :: Apple :: Pear :: HNil
val a : Apple = Apple()
val p : Pear = Pear()
val l = List(a, p, a, p) // Inferred type is List[Fruit]
< p >
l
的类型并不能确定列表的长度或其元素的精确类型。但是,如果我们期望它具有特定的形式(即如果它应该符合某个已知的固定模式),那么我们可以尝试确定这个事实并相应地采取行动。
scala> import Traversables._
import Traversables._
scala> val apap = l.toHList[Apple :: Pear :: Apple :: Pear :: HNil]
res0: Option[Apple :: Pear :: Apple :: Pear :: HNil] =
Some(Apple() :: Pear() :: Apple() :: Pear() :: HNil)
scala> apap.map(_.tail.head)
res1: Option[Pear] = Some(Pear())
有时候,我们可能并不关心给定列表的实际长度,只需要它与其他列表的长度相同。同样,这是shapeless支持的一种情况,既可以完全静态地支持,也可以在上述混合静态/动态环境中支持。请参见此处以获取扩展示例。
正如您所观察到的那样,所有这些机制都需要可用的静态类型信息,至少有条件地,这似乎排除了这些技术在完全由外部提供的未经过类型标记的数据驱动的完全动态环境中可用。但随着Scala 2.10中反射支持运行时编译的到来,即使这个问题也不再是一个难以克服的障碍...我们可以使用运行时编译提供一种轻量级分期的形式,并在响应动态数据时在运行时执行静态类型化:摘自以下内容...请点击链接查看完整示例。
val t1 : (Any, Any) = (23, "foo")
val t2 : (Any, Any) = (true, 2.0)
val c1 = stagedConsumeTuple(t1)
assert(c1 == "23foo")
val c2 = stagedConsumeTuple(t2)
assert(c2 == "+2.0")
我相信
@PLT_Borat会对此发表评论,考虑到他关于依赖类型编程语言的
睿智评论;-)