【问题标题】:Clarification on Existential Types in Haskell澄清 Haskell 中的存在类型
【发布时间】:2020-05-21 15:44:26
【问题描述】:

我正在尝试理解 Haskell 中的存在类型并遇到了一个 PDF http://www.ii.uni.wroc.pl/~dabi/courses/ZPF15/rlasocha/prezentacja.pdf

请纠正我到现在为止的以下理解。

  • Existential Types 似乎对它们包含的类型不感兴趣,但与它们匹配的模式表明存在某种类型,除非我们使用 Typeable 或 Data,否则我们不知道它是什么类型。
  • 当我们想要隐藏类型(例如:用于异构列表)或在编译时我们并不真正知道类型是什么时,我们会使用它们。
  • GADT's 通过提供隐式 forall's 为使用存在类型的代码提供清晰和更好的语法

我的疑惑

  • 在上述 PDF 的第 20 页中,在下面的代码中提到,函数不可能要求特定的缓冲区。为什么会这样?当我起草一个函数时,我确切地知道我要使用什么样的缓冲区,尽管我可能不知道我要放入什么数据。 拥有:: Worker MemoryBuffer Int 有什么问题如果他们真的想对 Buffer 进行抽象,他们可以拥有 Sum 类型 data Buffer = MemoryBuffer | NetBuffer | RandomBuffer 并拥有像 :: Worker Buffer Int 这样的类型
data Worker x = forall b. Buffer b => Worker {buffer :: b, input :: x}
data MemoryBuffer = MemoryBuffer

memoryWorker = Worker MemoryBuffer (1 :: Int)
memoryWorker :: Worker Int
  • 由于 Haskell 是像 C 一样的全类型擦除语言,那么它如何在运行时知道要调用哪个函数。是不是像我们要保留很少的信息并传入一个巨大的函数 V 表,然后在运行时它会从 V 表中计算出来?如果是这样,那么它将存储什么样的信息?

【问题讨论】:

    标签: haskell gadt existential-type


    【解决方案1】:

    GADT 通过提供隐式 forall 为使用存在类型的代码提供清晰和更好的语法

    我认为普遍认为 GADT 语法更好。我不会说这是因为 GADT 提供了隐式 foralls,而是因为使用 ExistentialQuantification 扩展名启用的原始语法可能会造成混淆/误导。当然,该语法看起来像:

    data SomeType = forall a. SomeType a
    

    或有约束:

    data SomeShowableType = forall a. Show a => SomeShowableType a
    

    而且我认为共识是这里使用关键字forall 使得该类型很容易与完全不同的类型混淆:

    data AnyType = AnyType (forall a. a)    -- need RankNTypes extension
    

    更好的语法可能使用了单独的 exists 关键字,所以你可以这样写:

    data SomeType = SomeType (exists a. a)   -- not valid GHC syntax
    

    GADT 语法,无论是与隐式还是显式forall 一起使用,在这些类型中都更加统一,并且似乎更容易理解。即使有一个显式的forall,下面的定义也传达了这样一个想法:你可以取任何类型的值a,并将它放在一个单态SomeType'中:

    data SomeType' where
        SomeType' :: forall a. (a -> SomeType')   -- parentheses optional
    

    并且很容易看出和理解该类型与:

    data AnyType' where
        AnyType' :: (forall a. a) -> AnyType'
    

    Existential Types 似乎对它们包含的类型不感兴趣,但与它们匹配的模式表明存在某种类型,除非我们使用 Typeable 或 Data,否则我们不知道它是什么类型。

    当我们想要隐藏类型(例如:用于异构列表)或在编译时我们真的不知道类型是什么时,我们会使用它们。

    我想这些并不太遥远,尽管您不必使用 TypeableData 来使用存在类型。我认为说存在类型在未指定类型周围提供了一个类型良好的“盒子”会更准确。从某种意义上说,该框确实“隐藏”了类型,这使您可以制作此类框的异构列表,而忽略它们包含的类型。事实证明,像上面的 SomeType' 这样的无约束存在是非常无用的,而是一个受约束的类型:

    data SomeShowableType' where
        SomeShowableType' :: forall a. (Show a) => a -> SomeShowableType'
    

    允许您进行模式匹配以窥视“盒子”内部并提供类型类工具:

    showIt :: SomeShowableType' -> String
    showIt (SomeShowableType' x) = show x
    

    请注意,这适用于任何类型类,而不仅仅是 TypeableData

    关于您对幻灯片第 20 页的困惑,作者是说不可能接受存在的函数 Worker 要求 Worker 具有特定的 @ 987654343@ 实例。您可以编写一个函数来使用特定类型的Buffer 创建Worker,例如MemoryBuffer

    class Buffer b where
      output :: String -> b -> IO ()
    data Worker x = forall b. Buffer b => Worker {buffer :: b, input :: x}
    data MemoryBuffer = MemoryBuffer
    instance Buffer MemoryBuffer
    
    memoryWorker = Worker MemoryBuffer (1 :: Int)
    memoryWorker :: Worker Int
    

    但是如果你编写一个以Worker 作为参数的函数,它只能使用一般的Buffer 类型类设施(例如,函数output):

    doWork :: Worker Int -> IO ()
    doWork (Worker b x) = output (show x) b
    

    它不能试图要求b 是一个特定类型的缓冲区,即使是通过模式匹配:

    doWorkBroken :: Worker Int -> IO ()
    doWorkBroken (Worker b x) = case b of
      MemoryBuffer -> error "try this"       -- type error
      _            -> error "try that"
    

    最后,关于存在类型的运行时信息可以通过所涉及的类型类的隐式“字典”参数获得。上面的Worker 类型,除了具有缓冲区和输入字段外,还有一个不可见的隐式字段,指向Buffer 字典(有点像v-table,虽然它不是很大,因为它只包含一个指针到相应的output 函数)。

    在内部,类型类Buffer表示为具有函数字段的数据类型,实例是这种类型的“字典”:

    data Buffer' b = Buffer' { output' :: String -> b -> IO () }
    
    dBuffer_MemoryBuffer :: Buffer' MemoryBuffer
    dBuffer_MemoryBuffer = Buffer' { output' = undefined }
    

    存在类型有一个隐藏字段用于此字典:

    data Worker' x = forall b. Worker' { dBuffer :: Buffer' b, buffer' :: b, input' :: x }
    

    doWork 这样对存在的Worker' 值进行操作的函数实现为:

    doWork' :: Worker' Int -> IO ()
    doWork' (Worker' dBuf b x) = output' dBuf (show x) b
    

    对于只有一个函数的类型类,字典实际上被优化为一个新类型,所以在这个例子中,存在的Worker 类型包括一个隐藏字段,它由一个指向output 函数的函数指针组成缓冲区,这是doWork 唯一需要的运行时信息。

    【讨论】:

    • Existentials 是否像 Rank 1 一样用于数据声明?存在主义是在 Haskell 中处理虚函数的方式,就像在任何 OOP 语言中一样?
    • 我可能不应该将AnyType 称为 rank-2 类型;这只是令人困惑,我已将其删除。构造函数 AnyType 的作用类似于 rank-2 函数,而构造函数 SomeType 作用为 rank-1 函数(就像大多数 非存在类型),但这不是一个很有帮助的表征。如果有的话,这些类型的有趣之处在于它们本身的等级为 0(即,没有在类型变量上量化,因此是单态的),即使它们“包含”量化类型。
    • 类型类(特别是它们的方法函数)而不是存在类型,可能是最直接的 Haskell 等价于虚函数。从技术意义上讲,OOP 语言的类和对象可以看作是存在类型和值,但实际上,在 Haskell 中实现 OOP“虚函数”风格的多态性通常比存在类型更好,例如 sum 类型,类型类和/或参数多态性。
    【解决方案2】:

    在上述 PDF 的第 20 页中,以下代码提到函数不可能要求特定的缓冲区。为什么会这样?

    因为Worker,按照定义,只接受一个参数,即“输入”字段的类型(类型变量x)。例如。 Worker Int 是一种类型。相反,类型变量b 不是Worker 的参数,而是一种“局部变量”,可以这么说。它不能像Worker Int String 那样传递——这会触发类型错误。

    如果我们改为定义:

    data Worker x b = Worker {buffer :: b, input :: x}
    

    然后Worker Int String 可以工作,但类型不再存在——我们现在也总是必须传递缓冲区类型。

    由于 Haskell 是一种类似于 C 的完整类型擦除语言,那么它如何在运行时知道要调用哪个函数。是不是像我们要保留很少的信息并传入一个巨大的函数 V 表,然后在运行时它会从 V 表中计算出来?如果是这样,那么它将存储什么样的信息?

    这大致正确。简而言之,每次应用构造函数Worker,GHC 都会从Worker 的参数中推断出b 类型,然后搜索实例Buffer b。如果找到,GHC 会在对象中包含一个指向该实例的附加指针。在最简单的形式中,这与在存在虚函数时添加到 OOP 中每个对象的“指向 vtable 的指针”没有太大区别。

    不过,在一般情况下,它可能要复杂得多。如果这样可以加快代码速度,编译器可能会使用不同的表示并添加更多指针而不是单个指针(例如,直接将指针添加到所有实例方法)。此外,有时编译器需要使用多个实例来满足约束。例如,如果我们需要为Eq [Int] 存储实例......那么就没有一个,而是两个:一个用于Int,一个用于列表,并且需要将两者结合起来(在运行时,除非进行优化)。

    很难准确猜测 GHC 在每种情况下的作用:这取决于大量可能触发或不触发的优化。

    您可以尝试在谷歌上搜索类型类的“基于字典”的实现,以了解更多关于发生了什么的信息。您还可以使用-ddump-simpl 要求 GHC 打印内部优化的 Core,并观察字典的构建、存储和传递。我必须警告你:Core 相当低级,一开始可能很难阅读。

    【讨论】:

      猜你喜欢
      • 2017-02-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-01-06
      • 2013-09-06
      • 1970-01-01
      • 2021-12-21
      • 1970-01-01
      相关资源
      最近更新 更多