首页 文章

“类型”与“新类型”中类型参数的奇怪行为:这是一个错误还是一个功能?

提问于
浏览
4

我正在编写一个类型类来为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 回答

  • 2

    为什么这种类型的失败...

    如果您编写 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编译器最终会产生生成不同输出的函数,从而形成一个不同的程序(通常是编程语言的基本所需方面之一)是不明确的,事实上没有两个语义上不同的程序映射在相同的源代码上) .

    ...以及为什么它适用于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 不是 VarName a . 这也意味着Haskell可以完美地得出 a 是什么 . 如果我们例如触发 reflectiveNames :: [VarName Char] ,那么它知道 aChar ,因此它将 instanceinstance 用于 a ~ Char . 没有含糊之处 . 当然我们可以定义别名,如:

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

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

相关问题