HList 只是一种编写元组的复杂方式吗? [英] Are HLists nothing more than a convoluted way of writing tuples?

查看:27
本文介绍了HList 只是一种编写元组的复杂方式吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我真的很想找出差异在哪里,更一般地说,确定不能使用 HList 的规范用例(或者更确切地说,与常规列表相比,不会产生任何好处).

(我知道 Scala 中有 22 个(我相信)TupleN,而一个只需要一个 HList,但这不是我感兴趣的那种概念差异.)

我在下面的文本中标记了几个问题.实际上可能没有必要回答它们,它们更多的是指出我不清楚的事情,并在某些方向上引导讨论.

动机

我最近在 SO 上看到了一些人们建议使用 HLists 的答案(例如,由 Shapeless),包括对 这个问题.它引发了这个讨论,进而引发了这个问题.>

介绍

在我看来,只有当您静态地知道元素的数量及其精确类型时,hlist 才有用.数字实际上并不重要,但您似乎不太可能需要生成一个包含可变但静态精确已知类型的元素的列表,但您并不静态地知道它们的数量.问题 1:您甚至可以在循环中编写这样的示例吗?我的直觉是,拥有一个静态精确的 hlist 和静态未知数量的任意元素(相对于给定的类层次结构是任意的)是不兼容的.

HLists 与元组

如果这是真的,即您静态地知道数字和类型 - 问题 2: 为什么不只使用 n 元组?当然,您可以类型安全地映射和折叠 HList(您也可以,但不能 类型安全地,在 productIterator 的帮助下对元组进行处理),但是由于 number 和元素的类型是静态已知的,您可能可以直接访问元组元素并执行操作.

另一方面,如果您映射到 hlist 的函数 f 是如此通用以至于它接受所有元素 - 问题 3: 为什么不通过 productIterator.map?好吧,一个有趣的区别可能来自方法重载:如果我们有几个重载的 f ,拥有由 hlist 提供的更强的类型信息(与 productIterator 相比)可以允许编译器选择一个更具体的f.但是,我不确定这是否真的适用于 Scala,因为方法和函数不一样.

HList 和用户输入

基于相同的假设,即您需要静态了解元素的数量和类型 - 问题 4: 可以在元素依赖于任何类型的用户交互的情况下使用 hlist?例如,想象用循环内的元素填充 hlist;从某处(UI、配置文件、actor 交互、网络)读取元素,直到某个条件成立.hlist 的类型是什么?类似于接口规范 getElements: HList[...] 应该处理静态未知长度的列表,并且允许系统中的组件 A 从组件 B 获取这样的任意元素列表.

解决方案

解决一到三个问题:HLists 的主要应用之一是对元数进行抽象.Arity 通常在抽象的任何给定使用站点是静态已知的,但因站点而异.拿这个,来自 shapeless 的 例子,

def flatten[T <: Product, L <: HList](t : T)(隐式 hl : HListerAux[T, L], flatten : Flatten[L]) : flatten.Out =压平(hl(t))val t1 = (1, ((2, 3), 4))val f1 = flatten(t1)//推断类型为 Int :: Int :: Int :: Int :: HNilval l1 = f1.toList//推断类型为 List[Int]val t2 = (23, ((true, 2.0, "foo"), "bar"), (13, false))val f2 = 展平(t2)val t2b = f2.tupled//t2b 的推断类型是 (Int, Boolean, Double, String, String, Int, Boolean)

如果不使用 HLists(或类似的东西)来抽象 flatten 元组参数的数量,就不可能有一个单一的实现来接受这两种截然不同的形状并以类型安全的方式转换它们.

在涉及固定元数的任何地方,对元数进行抽象的能力可能会引起人们的兴趣:以及元组,如上所述,包括方法/函数参数列表和案例类.请参阅此处我们如何抽象任意 case 类的数量以几乎自动获得类型类实例的示例,

//一对任意 case 类case class Foo(i : Int, s : String)case class Bar(b : Boolean, s : String, d : Double)//发布他们的`HListIso`隐式 def fooIso = Iso.hlist(Foo.apply _, Foo.unapply _)隐式 def barIso = Iso.hlist(Bar.apply _, Bar.unapply _)//现在它们是幺半群...隐含地[Monoid[Foo]]val f = Foo(13, "foo") |+|富(23,酒吧")断言(f == Foo(36,foobar"))隐式[Monoid[Bar]]val b = Bar(true, "foo", 1.0) |+|酒吧(假,酒吧",3.0)assert(b == Bar(true, "foobar", 4.0))

这里没有运行时迭代,但是有重复,使用HLists(或等效结构)可以消除这种重复.当然,如果您对重复样板的容忍度很高,您可以通过为您关心的每个形状编写多个实现来获得相同的结果.

在问题三中,您问...如果您映射到 hlist 的函数是如此通用以至于它接受所有元素...为什么不通过 productIterator.map 使用它?".如果您映射到 HList 的函数确实是 Any =>T 然后映射到 productIterator 将为您提供完美的服务.但是 Any => 形式的函数T 通常不是那么有趣(至少,除非它们在内部进行类型转换,否则它们不会).shapeless 提供了一种多态函数值的形式,它允许编译器以您所怀疑的方式选择特定于类型的情况.例如,

//大小是从任意类型的值到大小"的函数,它是//通过类型特定的情况定义对象大小扩展 Poly1 {隐式 def default[T] = at[T](t => 1)隐式 def caseString = at[String](_.length)隐式 def caseList[T] = at[List[T]](_.length)}标度>val l = 23 :: "foo" :: List('a', 'b') :: true :: HNill: Int :: String :: List[Char] :: Boolean :: HNil =23 :: foo :: List(a, b) :: true :: HNil标度>(l 地图大小).toListres1: List[Int] = List(1, 3, 2, 1)

关于您的问题四,关于用户输入,有两种情况需要考虑.第一种情况是我们可以动态建立上下文以保证获得已知的静态条件.在这些场景中,完全有可能应用无形技术,但很明显,如果静态条件没有在运行时获得,那么我们必须遵循替代路径.不出所料,这意味着对动态条件敏感的方法必须产生可选的结果.这是一个使用 HLists,

的例子

特质果实案例类 Apple() 扩展 Fruit案例类 Pear() 扩展 Fruit类型 FFFF = 水果 :: 水果 :: 水果 :: 水果 :: HNil键入 APAP = Apple :: Pear :: Apple :: Pear :: HNilval a : Apple = Apple()val p : 梨=梨()val l = List(a, p, a, p)//推断类型为 List[Fruit]

l 的类型不捕获列表的长度或其元素的精确类型.但是,如果我们期望它具有特定的形式(即,如果它应该符合某些已知的固定模式),那么我们可以尝试建立该事实并采取相应的行动,

scala>导入 Traversables._导入 Traversables._标度>val apap = l.toHList[Apple :: Pear :: Apple :: Pear :: HNil]res0:选项[苹果::梨::苹果::梨:: HNil] =Some(Apple() :: Pear() :: Apple() :: Pear() :: HNil)标度>apap.map(_.tail.head)res1: Option[Pear] = Some(Pear())

在其他情况下,我们可能不关心给定列表的实际长度,除了它与其他列表的长度相同.同样,这是无形支持的东西,无论是完全静态的,还是在上面的混合静态/动态环境中.请参阅此处一个扩展的例子.

正如您所观察到的,确实,所有这些机制都需要静态类型信息可用,至少有条件地可用,这似乎排除了这些技术在完全由外部提供的完全动态环境中可用的可能性无类型数据.但是随着 2.10 支持运行时编译作为 Scala 反射组件的出现,即使这也不再是不可逾越的障碍……我们可以使用运行时编译来提供一种形式的 轻量级暂存 并在运行时执行我们的静态类型以响应动态数据:摘自以下内容...点击链接获取完整示例,

val t1 : (Any, Any) = (23, "foo")//特定元素类型被擦除val t2 : (Any, Any) = (true, 2.0)//擦除特定元素类型//运行时在静态类型上选择的类型类实例!val c1 = stagedConsumeTuple(t1)//使用 intString 实例断言(c1 ==23foo")val c2 = stagedConsumeTuple(t2)//使用 booleanDouble 实例断言(c2 ==+2.0")

我确信 @PLT_Borat 会对此有话要说,因为他的 关于依赖类型编程语言的明智评论 ;-)

I am really interested in finding out where the differences are, and more generally, to identify canonical use cases where HLists cannot be used (or rather, don't yield any benefits over regular lists).

(I am aware that there are 22 (I believe) TupleN in Scala, whereas one only needs a single HList, but that is not the kind of conceptual difference I am interested in.)

I've marked a couple of questions in the text below. It might not actually be necessary to answer them, they are more meant to point out things that are unclear to me, and to guide the discussion in certain directions.

Motivation

I've recently seen a couple of answers on SO where people suggested to use HLists (for example, as provided by Shapeless), including a deleted answer to this question. It gave rise to this discussion, which in turn sparked this question.

Intro

It seems to me, that hlists are only useful when you know the number of elements and their precise types statically. The number is actually not crucial, but it seems unlikely that you ever need to generate a list with elements of varying but statically precisely known types, but that you don't statically know their number. Question 1: Could you even write such an example, e.g., in a loop? My intuition is that having a statically precise hlist with a statically unknown number of arbitrary elements (arbitrary relative to a given class hierarchy) just isn't compatible.

HLists vs. Tuples

If this is true, i.e, you statically know number and type - Question 2: why not just use an n-tuple? Sure, you can typesafely map and fold over an HList (which you can also, but not typesafely, do over a tuple with the help of productIterator), but since number and type of the elements are statically known you could probably just access the tuple elements directly and perform the operations.

On the other hand, if the function f you map over an hlist is so generic that it accepts all elements - Question 3: why not use it via productIterator.map? Ok, one interesting difference could come from method overloading: if we had several overloaded f's, having the stronger type information provided by the hlist (in contrast to the productIterator) could allow the compiler to choose a more specific f. However, I am not sure if that would actually work in Scala, since methods and functions are not the same.

HLists and user input

Building on the same assumption, namely, that you need to know number and types of the elements statically - Question 4: can hlists be used in situations where the elements depend on any kind of user interaction? E.g., imagine populating an hlist with elements inside a loop; the elements are read from somewhere (UI, config file, actor interaction, network) until a certain condition holds. What would the type of the hlist be? Similar for an interface specification getElements: HList[...] that should work with lists of statically unknown length, and that allows component A in a system to get such a list of arbitrary elements from component B.

解决方案

Addressing questions one to three: one of the main applications for HLists is abstracting over arity. Arity is typically statically known at any given use site of an abstraction, but varies from site to site. Take this, from shapeless's 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)     // Inferred type is Int :: Int :: Int :: Int :: HNil
val l1 = f1.toList       // Inferred type is List[Int]

val t2 = (23, ((true, 2.0, "foo"), "bar"), (13, false))
val f2 = flatten(t2)
val t2b = f2.tupled
// Inferred type of t2b is (Int, Boolean, Double, String, String, Int, Boolean)

Without using HLists (or something equivalent) to abstract over the arity of the tuple arguments to flatten it would be impossible to have a single implementation which could accept arguments of these two very different shapes and transform them in a type safe way.

The ability to abstract over arity is likely to be of interest anywhere that fixed arities are involved: as well as tuples, as above, that includes method/function parameter lists, and case classes. See here for examples of how we might abstract over the arity of arbitrary case classes to obtain type class instances almost automatically,

// A pair of arbitrary case classes
case class Foo(i : Int, s : String)
case class Bar(b : Boolean, s : String, d : Double)

// Publish their `HListIso`'s
implicit def fooIso = Iso.hlist(Foo.apply _, Foo.unapply _)
implicit def barIso = Iso.hlist(Bar.apply _, Bar.unapply _)

// And now they're monoids ...

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

There's no runtime iteration here, but there is duplication, which the use of HLists (or equivalent structures) can eliminate. Of course, if your tolerance for repetitive boilerplate is high, you can get the same result by writing multiple implementations for each and every shape that you care about.

In question three you ask "... if the function f you map over an hlist is so generic that it accepts all elements ... why not use it via productIterator.map?". If the function you map over an HList really is of the form Any => T then mapping over productIterator will serve you perfectly well. But functions of the form Any => T aren't typically that interesting (at least, they aren't unless they type cast internally). shapeless provides a form of polymorphic function value which allows the compiler to select type-specific cases in exactly the way you're doubtful about. For instance,

// size is a function from values of arbitrary type to a 'size' which is
// defined via type specific cases
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)

With respect to your question four, about user input, there are two cases to consider. The first is situations where we can dynamically establish a context which guarantees that a known static condition obtains. In these kinds of scenarios it's perfectly possible to apply shapeless techniques, but clearly with the proviso that if the static condition doesn't obtain at runtime then we have to follow an alternative path. Unsurprisingly, this means that methods which are sensitive to dynamic conditions have to yield optional results. Here's an example using HLists,

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]

The type of l doesn't capture the length of the list, or the precise types of its elements. However, if we expect it to have a specific form (ie. if it ought to conform to some known, fixed schema), then we can attempt to establish that fact and act accordingly,

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

There are other situations where we might not care about the actual length of a given list, other than that it is the same length as some other list. Again, this is something that shapeless supports, both fully statically, and also in a mixed static/dynamic context as above. See here for an extended example.

It is true, as you observe, that all of these mechanism require static type information to be available, at least conditionally, and that would seem to exclude these techniques from being usable in a completely dynamic environment, fully driven by externally provided untyped data. But with the advent of the support for runtime compilation as a component of Scala reflection in 2.10, even this is no longer an insuperable obstacle ... we can use runtime compilation to provide a form of lightweight staging and have our static typing performed at runtime in response to dynamic data: excerpt from the preceding below ... follow the link for the full example,

val t1 : (Any, Any) = (23, "foo") // Specific element types erased
val t2 : (Any, Any) = (true, 2.0) // Specific element types erased

// Type class instances selected on static type at runtime!
val c1 = stagedConsumeTuple(t1) // Uses intString instance
assert(c1 == "23foo")

val c2 = stagedConsumeTuple(t2) // Uses booleanDouble instance
assert(c2 == "+2.0")

I'm sure @PLT_Borat will have something to say about that, given his sage comments about dependently typed programming languages ;-)

这篇关于HList 只是一种编写元组的复杂方式吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆