我的一个朋友上周提出了一个看似无害的Scala语言问题,我没有一个很好的答案:是否有一种简单的方法来声明属于某些常见类型类的东西的集合。当然,Scala中没有一流的“类型类”概念,因此我们必须从特征和上下文界限(即暗示)来考虑这一点。
具体地说,给定一些表示类型类的特征T[_]
,并键入A
,B
和C
,并在范围T[A]
中显示相应的含义,{{ 1}}和T[B]
,我们希望声明类似T[C]
的内容,我们可以将List[T[a] forAll { type a }]
,A
和B
的实例放入其中而不受惩罚。这当然在Scala中不存在; question last year更深入地讨论了这一点。
自然的后续问题是“Haskell是如何做到的?”好吧,GHC特别有一个名为impredicative polymorphism的类型系统扩展,在"Boxy Types"论文中有描述。简而言之,给定类型类C
,可以合法地构建列表T
。给定这种形式的声明,编译器会执行一些字典传递魔术,它允许我们在运行时保留与列表中每个值的类型相对应的类型类实例。
事实上,“字典传递魔法”听起来很像“vtables”。在像Scala这样的面向对象语言中,子类型是一种比“Boxy类型”方法更简单,更自然的机制。如果我们的[forall a. T a => a]
,A
和B
都延伸了特征C
,那么我们就可以简单地声明T
并感到高兴。同样,正如Miles在下面的评论中指出的那样,如果它们都扩展了特征List[T]
,T1
和T2
,那么我可以使用T3
作为不可信的Haskell {{ 1}}。
然而,与类型类相比,子类型的主要众所周知的缺点是紧耦合:我的List[T1 with T2 with T3]
,[forall a. (T1 a, T2 a, T3 a) => a]
和A
类型必须具有B
行为我们假设这是一个主要的交易破坏者,我不能使用子类型。因此,Scala的中间地位是皮条客^ H ^ H ^ H ^ H ^隐藏转换:在隐式范围内给出一些C
,T
和A => T
,我可以再次愉快地填充B => T
C => T
,List[T]
和A
值......
......直到我们想要B
。此时,即使我们有隐式转换C
,List[T1 with T2 with T3]
和A => T1
,我们也无法将A => T2
放入列表中。我们可以将隐式转换重构为字面上提供A => T3
,但我以前从未见过任何人这样做,而且它似乎是另一种形式的紧耦合。
好吧,所以我的问题最终是,我想,这里先前提到的几个问题的组合:"why avoid subtyping?"和"advantages of subtyping over typeclasses" ...是否有一些统一的理论说明了不可预测的多态性和亚型多态性是一样的吗?隐含的转换是不是两个人的秘密爱情孩子?并且有人可以在Scala中表达一个好的,干净的模式来表达多个边界(如上面的例子中所示)吗?
答案 0 :(得分:21)
你会将具有存在主义类型的impredicative类型混淆。 Impredicative类型允许您将多态值放在数据结构中,而不是任意具体的值。换句话说,[forall a. Num a => a]
表示您有一个列表,其中每个元素都可以作为任何数字类型使用,因此您不能放置,例如类型Int
列表中的Double
和[forall a. Num a => a]
,但您可以在其中添加0 :: Num a => a
之类的内容。不可能的预测类型不是你想要的。
你想要的是存在类型,即[exists a. Num a => a]
(不是真正的Haskell语法),它表示每个元素都是一些未知的数字类型。但是,要在Haskell中编写它,我们需要引入一个包装器数据类型:
data SomeNumber = forall a. Num a => SomeNumber a
请注意从exists
到forall
的更改。那是因为我们正在描述构造函数。我们可以在中放置任何数字类型,但是类型系统“忘记”它的类型。一旦我们把它取回(通过模式匹配),我们所知道的是它是一些数字类型。发生了什么事情,SomeNumber
类型包含一个隐藏字段,它存储类型类字典(又名.vtable / implicit),这就是我们需要包装类型的原因。
现在我们可以使用类型[SomeNumber]
来获取任意数字的列表,但是我们需要在路上包装每个数字,例如[SomeNumber (3.14 :: Double), SomeNumber (42 :: Int)]
。查找每种类型的正确字典,并在我们包装每个数字的位置自动存储在隐藏字段中。
存在类型和类型类的组合在某些方面类似于子类型,因为类型类和接口之间的主要区别在于类型类vtable与对象分开传播,而存在类型将对象和vtable重新组合在一起试。
然而,与传统的子类型不同,你不会被迫将它们一对一配对,所以我们可以编写这样的东西,它将一个vtable打包成两个相同类型的值。
data TwoNumbers = forall a. Num a => TwoNumbers a a
f :: TwoNumbers -> TwoNumbers
f (TwoNumbers x y) = TwoNumbers (x+y) (x*y)
list1 = map f [TwoNumbers (42 :: Int) 7, TwoNumbers (3.14 :: Double) 9]
-- ==> [TwoNumbers (49 :: Int) 294, TwoNumbers (12.14 :: Double) 28.26]
甚至更高档的东西。一旦我们在包装器上进行模式匹配,我们就会回到类型类的土地上。虽然我们不知道x
和y
是哪种类型,但我们知道它们是相同的,并且我们有正确的字典可用于对它们执行数字运算。
以上所有内容与多个类型类似。编译器将在每个vtable的包装器类型中生成隐藏字段,并在模式匹配时将它们全部放入范围。
data SomeBoundedNumber = forall a. (Bounded a, Num a) => SBN a
g :: SomeBoundedNumber -> SomeBoundedNumber
g (SBN n) = SBN (maxBound - n)
list2 = map g [SBN (42 :: Int32), SBN (42 :: Int64)]
-- ==> [SBN (2147483605 :: Int32), SBN (9223372036854775765 :: Int64)]
因为我对Scala来说是一个初学者,我不确定我能帮你解决问题的最后部分,但我希望这至少可以解决一些困惑并给你一些帮助关于如何进行的想法。
答案 1 :(得分:1)
@ hammar的答案是完全正确的。这是doint它的scala方式。对于示例,我将Show
作为类型类,并将值i
和d
打包到列表中:
// The type class
trait Show[A] {
def show(a : A) : String
}
// Syntactic sugar for Show
implicit final class ShowOps[A](val self : A)(implicit A : Show[A]) {
def show = A.show(self)
}
implicit val intShow = new Show[Int] {
def show(i : Int) = "Show of int " + i.toString
}
implicit val stringShow = new Show[String] {
def show(s : String) = "Show of String " + s
}
val i : Int = 5
val s : String = "abc"
我们想要的是能够运行以下代码
val list = List(i, s)
for (e <- list) yield e.show
建立列表很简单,但列表不会记得&#34;记住&#34;每个元素的确切类型。相反,它会将每个元素向上转换为公共超类型T
。 String
和Int
Any
之间的超级超级类型越精确,列表类型为List[Any]
。
问题是:忘记什么,记住什么?我们想要忘记元素的确切类型但我们要记住它们都是Show
的实例。以下类正是这样做的
abstract class Ex[TC[_]] {
type t
val value : t
implicit val instance : TC[t]
}
implicit def ex[TC[_], A](a : A)(implicit A : TC[A]) = new Ex[TC] {
type t = A
val value = a
val instance = A
}
这是存在主义的编码:
val ex_i : Ex[Show] = ex[Show, Int](i)
val ex_s : Ex[Show] = ex[Show, String](s)
它使用相应的类型类实例打包一个值。
最后,我们可以为Ex[Show]
implicit val exShow = new Show[Ex[Show]] {
def show(e : Ex[Show]) : String = {
import e._
e.value.show
}
}
需要import e._
才能将实例纳入范围。感谢暗示的魔力:
val list = List[Ex[Show]](i , s)
for (e <- list) yield e.show
非常接近预期的代码。