Haskell中的类型和类型变量

时间:2019-02-27 02:15:55

标签: haskell types type-systems

在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”进行测试时,它也可以变成“字符”。

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

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类型系统的重要课程。谢谢您的时间!

2 个答案:

答案 0 :(得分: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

好吧,现在下一步:选择类型参数时,可以用几种方法来完成。在上面的示例中,我通过传递所需类型的value参数来选择它。但是另一种方法是指定我想要的结果的类型。考虑一下:

g x = []

a :: [Int]
a = g 0

b :: [Char]
b = g 42

此处,函数g忽略其参数,因此其类型与g的结果之间没有关系。但是我仍然可以通过受周围环境的约束来选择结果的类型。

现在,我们的思想有了飞跃:没有任何参数(又称“值”)的功能与带有参数的功能没什么不同。它只有零个参数,仅此而已。

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


关于Num的困惑是完全不同的事情。

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

因此,签名en :: Num a => [a]表示en是具有 any 类型元素的列表,只要该类型实现(“具有”的实例)该类型类Num

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

在您的情况下,编译器会看到en :: [a]ec :: [Char]进行了比较,并指出:“哦,我知道:a必须是Char!”然后查找类实例,并注意到a必须具有Num的实例,并且由于aChar,因此得出Char必须具有Num的实例。但这没有,因此编译器抱怨:“找不到(Num Char)”

至于“从使用en开始”-那是因为en是需要Num实例的原因。 en是类型签名中带有Num的那个,因此它的存在是引起对Num的要求的原因

答案 1 :(得分:4)

有时候,将多态函数视为带有显式 type参数的函数很方便。让我们以多态身份函数为例。

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允许我们省略type参数。即我们可以编写id "hello",GHC会尝试推断出缺少的类型参数。大致来说,它的工作方式如下:将id "hello"转换为id @t "hello"的某个未知类型t,然后根据id的类型,此调用只能键入检查{{1 }},并且自"hello" :: t起,我们就可以推断出"hello" :: String

类型推断在Haskell中非常普遍。程序员很少指定他们的类型参数,而让GHC来做。

在您的情况下:

t = String

变量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 必须具有相同的类型。实际上,它们还没有,因为要推断出不同的隐式类型参数。