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,否则我们不知道它是什么类型。
当我们想要隐藏类型(例如:用于异构列表)或在编译时我们真的不知道类型是什么时,我们会使用它们。
我想这些并不太遥远,尽管您不必使用 Typeable 或 Data 来使用存在类型。我认为说存在类型在未指定类型周围提供了一个类型良好的“盒子”会更准确。从某种意义上说,该框确实“隐藏”了类型,这使您可以制作此类框的异构列表,而忽略它们包含的类型。事实证明,像上面的 SomeType' 这样的无约束存在是非常无用的,而是一个受约束的类型:
data SomeShowableType' where
SomeShowableType' :: forall a. (Show a) => a -> SomeShowableType'
允许您进行模式匹配以窥视“盒子”内部并提供类型类工具:
showIt :: SomeShowableType' -> String
showIt (SomeShowableType' x) = show x
请注意,这适用于任何类型类,而不仅仅是 Typeable 或 Data。
关于您对幻灯片第 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 唯一需要的运行时信息。