类型参数在"类型"中的奇怪行为与" newtype":这是一个错误还是一个功能?

时间:2018-06-01 09:46:54

标签: haskell types

我正在编写一个类型类来为Haskell数据类型添加类型反射。部分内容如下:

type VarName a = Text


class Reflective a where
   -- | A reflective type may have a fixed name, or it may be a union 
   -- where each variant has a distinct name.
   reflectiveName :: a -> VarName a
   -- | One default value of the type for each reflective name.
   reflectiveDefaults :: [a]

我的想法是,如果我写

data ExampleType = Foo Int | Bar String

然后在Reflective实例中reflectiveName将返回" Foo"或" Bar"适当时,reflectiveDefaults将返回[Foo 0, Bar ""]

所以现在我可以编写一个函数来给我所有变体的名称:

reflectiveNames :: (Reflective a) => [VarName a]
reflectiveNames = map reflectiveName reflectiveDefaults

我可以这样称呼它:

exampleNames = reflectiveNames :: [VarName ExampleType]

当我编译它时,我在reflectiveNames的类型声明中得到以下错误:

• Could not deduce (Reflective a0)
  from the context: Reflective a
    bound by the type signature for:
               reflectiveNames :: Reflective a => [VarName a]
  The type variable ‘a0’ is ambiguous

但是,如果我用新类型替换VarName:

newtype VarName a = VarName Text

然后它有效。

这是Haskell类型系统的一个功能,还是GHC中的一个错误?如果是前者,为什么要发明一个新的类型变量a0?

1 个答案:

答案 0 :(得分:2)

为什么type ...

失败了

如果您编写type然后 构造新类型,则构造别名。所以你定义了:

type VarName a = Text

因此,现在每次写VarName SomeType时,基本上都写了Text。所以VarName Char ~ VarName Int ~ Text(代字号~表示两种类型相同)。

类型别名很有用,但是因为它们通常最小化代码(通常别名的名称比其对应的短),它降低了签名的复杂性(一个不必记住大型类型的层次结构),最后如果某些类型尚未完全确定(例如时间可以建模为Int32Int64等),则可以使用它。我们想要定义一些占位符以轻松更改大量签名。

但重点是,每次编写VarName Char时,Haskell都会用Text替换它。所以现在如果我们看看你的功能,你写了:

reflectiveNames :: Reflective a => [Text]
reflectiveNames = map reflectiveName reflectiveDefaults

现在这个函数有一个问题:有一个类型变量a(在Reflective a中),但在签名中没有任何地方我们使用这个类型参数。问题是,如果你调用这个函数,Haskell不知道要填写什么a,这是一个真正的问题(这里),因为reflectiveNamereflectiveDefaults的语义可以对a ~ Char完全不同,然后对a ~ Int完全不同。编译器不能只是“选择a的类型,因为这意味着两个不同的Haskell编译器最终会产生生成不同输出的函数,从而产生不同的程序(通常,编程语言的一个基本需求方面是 unambiguity ,事实上没有两个语义上不同的程序映射在相同的源代码上。

...以及为什么它适用于newtype

现在,如果我们使用 newtype ,为什么不会发生这种情况?基本上newtypedata声明相同,除了一些小细节:例如幕后,Haskell将生成这样的构造函数,它只会存储包含在构造函数中的值,但它将值视为不同的类型。 newtype定义

newtype VarName a = VarName Text
因此(概念上)

几乎相当于:

data VarName a = VarName Text

虽然Haskell会(假设它是一个可以处理这种优化的编译器)将构造函数考虑在内,但我们可以在概念上假设它存在。

但主要区别在于我们定义了一个类型签名:我们定义了一个新类型,因此函数签名保持不变:

reflectiveNames :: Reflective a => [VarName a]
reflectiveNames = map reflectiveName reflectiveDefaults

我们不能只写Text而不是VarName a,因为Text 不是 a VarName a。这也意味着Haskell可以完美地导出a。如果我们要触发reflectiveNames :: [VarName Char],那么它知道aChar,因此instance使用Reflective a ~ Char }}。没有含糊之处。当然,我们可以定义别名:

type Foo = VarName Char   -- a ~ Char
type Bar b = VarName Int  -- a ~ Int

但仍然a分别解析为CharInt。由于这是一种新类型,我们将始终通过代码携带a类型,因此代码是明确的。