【问题标题】:Impredicative types vs. plain old subtyping指示性类型与普通旧子类型
【发布时间】:2012-03-16 06:13:08
【问题描述】:

我的一个朋友上周提出了一个看似无关紧要的 Scala 语言问题,但我没有很好的答案:是否有一种简单的方法可以声明属于某个常见类型类的事物的集合。当然,Scala 中没有一流的“类型类”概念,所以我们必须从特征和上下文边界(即隐式)的角度来考虑这一点。

具体来说,给定一些 trait T[_] 代表一个类型类,以及类型 ABC,在范围内有相应的隐式 T[A]T[B]T[C],我们要声明类似于List[T[a] forAll { type a }],我们可以将ABC 的实例抛入其中而不受惩罚。这在 Scala 中当然不存在。 question last year 对此进行了更深入的讨论。

自然的后续问题是“Haskell 是如何做到的?”好吧,GHC 特别有一个类型系统扩展,称为impredicative polymorphism,在"Boxy Types" 论文中进行了描述。简而言之,给定一个类型类T,可以合法地构造一个列表[forall a. T a => a]。给定这种形式的声明,编译器会执行一些字典传递魔法,让我们在运行时保留与列表中每个值的类型相对应的类型类实例。

问题是,“字典传递魔法”听起来很像“vtables”。在像 Scala 这样的面向对象语言中,子类型化是一种比“Boxy Types”方法更简单、更自然的机制。如果我们的ABC 都扩展了特征T,那么我们可以简单地声明List[T] 并感到高兴。同样,正如 Miles 在下面的评论中指出的那样,如果它们都扩展了特征 T1T2T3,那么我可以使用 List[T1 with T2 with T3] 作为不具格言的 Haskell [forall a. (T1 a, T2 a, T3 a) => a] 的等价物。

然而,与类型类相比,子类型的主要、众所周知的缺点是紧密耦合:我的ABC 类型必须具有它们的T 行为。假设这是一个主要的交易破坏者,我不能使用子类型。所以 Scala 的中间立场是 pimps^H^H^H^H^Himplicit 转换:在隐式范围内给定一些 A => TB => TC => T,我可以再次非常高兴地用我的 @ 填充 List[T] 987654357@、BC 值...

...直到我们想要List[T1 with T2 with T3]。此时,即使我们有隐式转换A => T1A => T2A => T3,我们也不能将A 放入列表中。我们可以重构我们的隐式转换以提供A => T1 with T2 with T3,但我以前从未见过有人这样做,这似乎是另一种形式的紧密耦合。

好吧,我想我的问题最后是,我想,是之前在这里提出的几个问题的组合:"why avoid subtyping?""advantages of subtyping over typeclasses" ...和相同的?不知何故,隐含的转换是两人的秘密爱子?有人可以在 Scala 中阐明一个良好、干净的模式来表达多个边界(如上面的最后一个示例)吗?

【问题讨论】:

  • 啊,抱歉,初读时错过了!
  • 有一点我不清楚:如果List[T] 是列表[forall a. T a => a] 的适当(上下文)翻译,那么为什么List[T1 with T2 with T3] 不是列表@987654369 的适当翻译@?
  • @MilesSabin 谢谢,这在我原来的帖子中并不清楚。我已经编辑澄清,如果我可以使用隐式转换,我实际上并不想从字面上扩展T1T2T3。也感谢你有机会在 StackOverflow 上关于类型系统的问题中使用“秘密爱子”这个短语 :)
  • 我认为您对含意多态性和存在限定性感到困惑。类型[forall a. Show a => a] 表示所有类型的列表,例如Show a[()] 不满足这种类型,因为() 是更具体的类型()。我不确定是否有任何值满足这种类型,但这不是你想要的;为了得到你想要的,你需要一个存在限定,它需要在它周围包装一个新的数据构造函数(尽管它没有必须成为一个 GADT)。
  • 我相信您所问的并不是 Scala 的全部内容。尽管 Scala 使用函数式构造,但它的核心仍然是 OO,因此您应该使用子类型化。你的最后一个例子令人困惑。您需要 T1 AND T2 AND T3 类型的对象。 'A' 可以转换为 T1、T2 或 T3,但你能保证它可以转换为所有三个吗?我在这里找不到反例,但不难发现这样的结构可能会导致不健全的事情。因此,实际上需要对所有三个进行隐式转换以确保稳健性。只是我的两分钱。

标签: scala haskell functional-programming subtype impredicativetypes


【解决方案1】:

您将禁言类型与存在类型混淆了。指示性类型允许您将 多态 值放入数据结构中,而不是任意具体的值。换句话说[forall a. Num a => a] 意味着您有一个列表,其中每个元素都可以作为任何数字类型使用,因此您不能放置例如IntDouble[forall a. Num a => a] 类型的列表中,但您可以在其中放入类似 0 :: Num a => a 的内容。暗示类型不是你想要的。

你想要的是存在类型,即[exists a. Num a => a](不是真正的 Haskell 语法),它表示每个元素都是某种未知的数字类型。然而,要在 Haskell 中编写它,我们需要引入一个包装器数据类型:

data SomeNumber = forall a. Num a => SomeNumber a

注意从existsforall 的变化。那是因为我们正在描述 构造函数。我们可以将任何数字类型放入中,但是类型系统会“忘记”它是哪个类型。一旦我们将其取出(通过模式匹配),我们所知道的就是它是某种数字类型。幕后发生的事情是,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]

甚至更高级的东西。一旦我们在包装器上进行模式匹配,我们就回到了类型类的领域。尽管我们不知道xy 是哪种类型,但我们知道它们是相同的,并且我们有正确的字典可用于对它们执行数字运算。

以上所有内容都适用于多个类型类。编译器将简单地为每个 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 的初学者,我不确定我能否帮助您解决问题的最后一部分,但我希望这至少可以消除一些困惑并给您一些关于如何进行的想法。

【讨论】:

    【解决方案2】:

    @hammar 的回答完全正确。这是 scala 的方式。例如,我将 Show 作为类型类,并将值 id 打包到一个列表中:

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

    构建列表很容易,但列表不会“记住”每个元素的确切类型。相反,它将每个元素向上转换为一个公共超类型TStringInt之间更精确的超超类型是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._ 才能将实例纳入范围。感谢implicits的魔力:

    val list = List[Ex[Show]](i , s)
    for (e <- list) yield e.show
    

    这与预期的代码非常接近。

    【讨论】:

      猜你喜欢
      • 2016-11-30
      • 1970-01-01
      • 2015-03-30
      • 2013-02-24
      • 1970-01-01
      • 1970-01-01
      • 2016-08-17
      • 2018-08-14
      • 1970-01-01
      相关资源
      最近更新 更多