澄清Haskell中的存在类型


10

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

请更正我到目前为止的以下理解。

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

我的怀疑

  • 在以上PDF的第20页中,对于以下代码,提到函数不可能要求特定的缓冲区。为什么会这样呢?当我起草函数时,即使我可能不知道要放入什么数据,我也完全知道我将使用哪种缓冲区。什么是错在有:: Worker MemoryBuffer Int如果他们真的想在抽象缓冲区他们能有一个求和型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表中找出来?如果是这样,那么它将存储什么样的信息?

Answers:


8

GADT通过提供隐式的forall,为使用现有类型的代码提供清晰,更好的语法

我认为人们普遍认为GADT语法更好。我并不是说这是因为GADT提供了隐式的forall,而是因为带有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'

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

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

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

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

允许您进行模式匹配以在“框”内窥视,并使类型类工具可用:

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

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

关于您对幻灯片第20页的困惑,作者说,对于一个存在性 Worker需求的函数来说,要求Worker具有特定Buffer实例是不可能的。您可以编写一个函数来Worker使用的特定类型创建Buffer,例如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

但是,如果您编写一个带有Workeras参数的函数,则该函数只能使用常规Buffer类型类工具(例如function 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


存在性是否像数据声明的等级1一样?存在性是像任何OOP语言一样在Haskell中处理虚拟函数的方式吗?
Pawan Kumar

1
我可能不应该将其称为AnyType2级类型。这只是令人困惑,我已将其删除。构造函数的AnyType作用类似于等级2的函数,构造函数的作用类似于等级SomeType1的函数(就像大多数存在的类型一样),但这并不是非常有用的表征。如果有的话,使这些类型变得有趣的是,即使它们“包含”了量化的类型,它们本身也是第0级(即,没有在类型变量上进行量化,因此是单态的)。
KA Buhr

1
类型类(特别是它们的方法函数)而不是存在性类型,可能是与虚拟函数最直接的Haskell等效项。从技术上讲,OOP语言的类和对象可以看作是存在的类型和值,但是实际上,在Haskell中通常有比存在的方法更好的方法来实现OOP的“虚拟函数”样式的多态性,例如和类型,类型类和/或参数多态性。
KA Buhr

4

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

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

如果我们改为定义:

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

这样Worker Int String就可以了,但是该类型不再存在-我们现在也必须始终传递缓冲区类型。

因为Haskell是一种像C这样的完全类型擦除语言,所以它如何在运行时知道调用哪个函数。是否就像我们将要维护的信息很少,并传入一个巨大的V表功能,然后在运行时从V表中找出来?如果是这样,那么它将存储什么样的信息?

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

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

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

您可以尝试搜索类型类的“基于字典的”实现,以了解发生了什么。您还可以要求GHC与一起打印内部优化的Core,-ddump-simpl并观察正在构建,存储和传递的字典。我必须警告您:Core级别较低,一开始可能很难阅读。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.