Haskell中的类型和类型变量

问题描述 投票:13回答:2

抓住Haskell类型系统的表面,运行:

Prelude> e = []
Prelude> ec = tail "a"
Prelude> en = tail [1]
Prelude> :t e
e :: [a]
Prelude> :t ec
ec :: [Char]
Prelude> :t en
en :: Num a => [a]
Prelude> en == e
True
Prelude> ec == e
True

不知何故,尽管en和ec有不同的类型,但它们都在== e上测试True。我说“某种程度上”不是因为我感到惊讶(我不是),而是因为我不知道允许这个的规则/机制的名称是什么。就好像表达式“[] == en”中的类型变量“a”被允许采用值“Num”进行评估。同样,当用“[] == ec”测试时,它被允许变成“Char”。

我不确定我的解释是否正确的原因是:

Prelude> (en == e) && (ec == e)
True

,因为直觉上这意味着在同一个表达式中,e“同时”假设Num和Char的值(至少我用来解释&&的语义)。除非Char的“假设”仅在(ec == e)的评估期间起作用,并且(en == e)是独立评估的,在单独的......减少中? (我在这里猜测一个术语)。

然后来了:

Prelude> en == es

<interactive>:80:1: error:
    • No instance for (Num Char) arising from a use of ‘en’
    • In the first argument of ‘(==)’, namely ‘en’
      In the expression: en == es
      In an equation for ‘it’: it = en == es
Prelude> es == en

<interactive>:81:7: error:
    • No instance for (Num Char) arising from a use of ‘en’
    • In the second argument of ‘(==)’, namely ‘en’
      In the expression: es == en
      In an equation for ‘it’: it = es == en

异常并不令人惊讶,但令人惊讶的是,在两个测试中,错误消息都抱怨“使用'en'” - 并且如果它是第一个或第二个操作数并不重要。

关于Haskell类型系统可能需要学习一个重要的教训。感谢您的时间!

haskell types type-systems
2个回答
12
投票

当我们说e :: [a]时,它意味着e是任何类型的元素列表。哪种类型?随便哪种!无论你目前碰巧需要哪种类型。

如果你来自非ML语言,首先通过查看函数(而不是值)可能会更容易理解。考虑一下:

f x = [x]

这个函数的类型是f :: a -> [a]。这大致意味着此功能适用于任何类型的a。你给它一个这种类型的值,它会返回一个包含该类型元素的列表。哪种类型?随便哪种!无论你碰巧需要什么。

当我调用此函数时,我有效地选择了我想要的类型。如果我把它称为f 'x',我选择a = Char,如果我称之为f True,我选择a = Bool。因此,重要的一点是调用函数的人选择类型参数。

但我不必一次性地选择它,而且永远不会选择它。相反,我每次调用函数时都会选择类型参数。考虑一下:

pair = (f 'x', f True)

在这里我两次调用f,我每次都选择不同的类型参数 - 第一次选择a = Char,第二次选择a = Bool

好了,现在进行下一步:当我选择类型参数时,我可以通过多种方式完成。在上面的例子中,我通过传递我想要的类型的值参数来选择它。但另一种方法是指定我想要的结果类型。考虑一下:

g x = []

a :: [Int]
a = g 0

b :: [Char]
b = g 42

这里,函数g忽略它的参数,因此它的类型和g的结果之间没有关系。但我仍然能够通过周围环境约束来选择结果的类型。

现在,精神上的飞跃:没有任何参数的函数(又称“值”)与带参数的函数没有什么不同。它只有零参数,就是这样。

如果一个值有类型参数(例如你的值e),我可以在每次“调用”该值时选择该类型参数,就像它是一个函数一样容易。所以在表达式e == ec && e == en中你只是“调用”值e两次,在每次调用时选择不同的类型参数 - 就像我在上面的pair示例中所做的那样。


关于Num的混淆是一个完全不同的问题。

你看,Num不是一种类型。这是一个类型类。类型类有点像Java或C#中的接口,除了您可以稍后声明它们,不一定与实现它们的类型一起。

所以签名en :: Num a => [a]意味着en是一个包含任何类型元素的列表,只要该类型实现(“有一个实例”)类型类Num

Haskell的类型推断方式是,编译器将首先确定它可以使用的最具体类型,然后尝试查找这些类型所需类型类的实现(“实例”)。

在你的情况下,编译器看到en :: [a]正在与ec :: [Char]进行比较,并且它表示:“哦,我知道:a必须是Char!”然后它会找到类实例并注意到a必须有Num的实例,并且由于aChar,因此Char必须有Num的实例。但它没有,所以编译器抱怨:“找不到(Num Char)”

至于“因使用en而产生” - 嗯,这是因为en是需要Num实例的原因。 en是在其类型签名中有Num的那个,因此它的存在是导致Num的要求的原因


4
投票

有时,将多态函数视为采用显式类型参数的函数是很方便的。让我们以多态身份函数为例。

id :: forall a . a -> a
id x = x

我们可以将此功能看作如下:

  • 首先,该函数将名为a的类型参数作为输入
  • 第二,该函数将先前选择的类型x的值a作为输入
  • 最后,函数返回x(类型为a

这是一个可能的电话:

id @Bool True

上面,@Bool语法为第一个参数(类型参数Bool)传递a,而True作为第二个参数传递(x类型的a = Bool)。

其他一些:

id @Int 42
id @String "hello"
id @(Int, Bool) (3, True)

我们甚至可以部分应用仅传递类型参数的id

id @Int       :: Int -> Int
id @String    :: String -> String
...

现在,请注意,在大多数情况下,Haskell允许我们省略类型参数。即我们可以写id "hello"和GHC将尝试推断缺失的类型参数。大致它的工作原理如下:id "hello"转换为id @t "hello"为一些未知的类型t,然后根据id的类型这个调用只能检查"hello" :: t,自从"hello" :: String,我们可以推断t = String

类型推断在Haskell中非常常见。程序员很少指定他们的类型参数,让GHC完成它的工作。

在你的情况下:

e :: forall a . [a]
e = []
ec :: [Char]
ec = tail "1"
en :: [Int]
en = tail [1]

变量e与多态值绑定。也就是说,它实际上是一个排序函数,它接受类型参数a(也可以省略),并返回类型为[a]的列表。

相反,ec不接受任何类型的论证。这是[Char]类型的简单列表。同样对于en

然后我们可以使用

ec == (e @Char)    -- both of type [Char]
en == (e @Int)     -- both of type [Int]

或者我们可以让类型推理引擎来确定隐式类型参数

ec == e     -- @Char inferred
en == e     -- @Int inferred

后者可能会产生误导,因为ec,e,en似乎必须具有相同的类型。事实上,他们没有,因为推断出不同的隐式类型参数。

© www.soinside.com 2019 - 2024. All rights reserved.