Haskell中涉及列表的函数类型签名

3

我刚开始学习Haskell,正在跟随书籍《Learn You a Haskell》。我遇到了以下示例:

tell :: (Show a) => [a] -> String  
tell [] = "The list is empty"  

我明白这里的 (Show a) 是一个类约束,而参数的类型,在这种情况下是 a 必须能够被“showable”显示。
考虑到这里的 a 是一个列表而不是列表中的元素,为什么我无法声明函数如下:
tell :: (Show a) =>a->String

编辑1:-从下面的答案中,我似乎理解到需要指定模式匹配的a的具体类型。考虑到这一点,下面的实现应该是正确的:

pm :: (Show a) =>a->String
pm 'g'="wow"

它给我以下错误:

 Could not deduce (a ~ Char)
from the context (Show a)
  bound by the type signature for pm :: Show a => a -> String
  at facto.hs:31:7-26
  `a' is a rigid type variable bound by
      the type signature for pm :: Show a => a -> String at facto.hs:31:7
In the pattern: 'g'
In an equation for `pm': pm 'g' = "wow"

失败了,已加载的模块: 无。

从错误信息中我理解到无法推断 a 的具体类型,但是如何使用 Show 声明呢。

我知道可以通过这种方式解决以上问题:

pmn :: Char->String
pmn 'g'="wow"

但我只是想更好地理解Show类型类


当前的标题是可以的。 - Robert Harvey
5个回答

3
在这两个签名中,a 不是一个列表 - 它可以是任何类型,并且你不能选择它是哪种类型(除了它必须是 Show 的实例)。
在...(后面的内容需要提供)
tell₁ :: Show a => [a] -> String
tell₁ [] = "The list is empty"
... -- (remember to match the non-empty list case too!)

您正在匹配 a 的列表,而不是 a 类型本身的值。

如果您写成

tell₂ :: Show a => a -> String
tell₂ [] = "The list is empty"
...

您可能认为类型a是列表(某些东西的列表)的类型。但它可以是任何类型,例如Bool
(但我可能不理解您的问题-您还没有说出问题在哪里。当像这样提问时,通常应指定您做了什么,期望什么以及发生了什么。这里没有真正指定其中任何一个,因此人们只能猜测您可能的意思。)

很好的解释了正在匹配的内容,但是(我自己也很好奇),为什么后者会失败呢?列表不是Show吗?即使我们将列表与a进行匹配而不是[a],列表不应该能够匹配Show a吗? - trutheality
3
List确实实现了Show类型类,但当你说:“Show a => a -> String”时,它意味着函数将接受任何实现Show的类型,并且最重要的是你只能在“a”上调用show类函数,不能调用a的其他内容。你的函数永远不会知道a的具体类型。而你正在尝试在“a”上调用列表模式匹配。 - Ankur
@Ankur 谢谢,这很有道理,需要列表类型的模式匹配。 - trutheality
@Alice:正确的实现应该是:pm c ="wow"。您可以在参数c上调用任何Show类型类函数。您之前尝试进行模式匹配是不可行的,因为您不知道参数的确切类型,只知道它实现了Show类型类。但是当您将Char指定为类型时,模式匹配就可以工作了。 - Ankur
谢谢@Ankur,如果您能将您的评论发布为答案,我可以接受它。 - Rasmus

3
问题不在于Show。实际上,如果我们尝试:
tell2 :: a -> String
tell2 [] = "The list is empty"

我们遇到了一个类型检查错误。让我们看看它的具体信息:
test.hs:5:7:
    Couldn't match expected type `a' with actual type `[t0]'
      `a' is a rigid type variable bound by
          the type signature for tell2 :: a -> String at test.hs:4:10
    In the pattern: []
    In an equation for `tell2': tell2 [] = "The list is empty"

现在我们要问自己,所谓的“类型”构造真正意味着什么?当你写下tell2 :: a -> String时,你想表达的是对于任何恰好是类型 a 的值,tell2 将会给我们一个 String。然而,[a](或者[c] 或者 [foo] —— 名称并不重要)并非恰好等同于 a。这似乎是一个武断的区分,据我所知,确实是这样。让我们看看当我们写下以下内容时会发生什么:
tell2 [] = "The list is empty"

> :t tell2
> tell2 :: [t] -> [Char]

如你所知,编写 ta 是没有区别的,而 [Char] 只是 String 的类型同义词,因此我们编写的类型和 GHC 推断的类型是相同的
嗯,不完全是这样。当您手动在源代码中指定函数类型时,您类型签名中的类型变量就会变得严格。这到底是什么意思呢?
来自https://research.microsoft.com/en-us/um/people/simonpj/papers/gadt/

"我们使用更简洁的术语 'rigid type' 来描述由程序员提供的类型注释直接完全指定的类型,而非 '用户指定的类型(user-specified type)'。

所以,任何由程序员类型签名指定的类型都是刚性类型。所有其他类型都是“摇晃的”[1]

因此,仅仅通过编写它,类型签名就已经变得不同了。在这个新的类型文法中,我们有 a /= [b]。对于刚性类型签名,GHC 将推断出它可以推断出的最少信息。它必须从模式绑定中推断出 a ~ [b],但是它无法从您提供的类型签名中进行推断。
让我们看一下 GHC 为原始函数给出的错误提示:
test.hs:2:6:
    Could not deduce (a ~ [t0])
    from the context (Show a)
      bound by the type signature for tell :: Show a => a -> String
      at test.hs:1:9-29
      `a' is a rigid type variable bound by

我们再次看到了刚性类型变量(rigid type variable)等概念,但在这种情况下,GHC还声称无法推断出某些东西。(顺便说一下 - 在类型语法中,a ~ b === a == b)。实际上,类型检查器正在寻找类型中的约束条件以使函数有效;它没有找到,并且友好地告诉您需要什么来使其有效:
{-# LANGUAGE GADTs #-}
tell :: (a ~ [t0], Show a) => a -> String
tell [] = "The list is empty"

如果我们直接插入 GHC 给出的建议,它将能够通过类型检查,因为此时 GHC 不需要进行任何推断;我们已经准确地告诉了它 a 是什么。

3
列表确实实现了Show类型类,但是当你说:Show a => a -> String时,它意味着函数将接受任何实现Show的类型,并且更重要的是你只能在一个“nothing else”的情况下调用show类函数,你的函数永远不会知道a的具体类型。而你正在尝试对a进行列表模式匹配。 针对问题的新编辑更新: 正确的实现应该是:pm c ="wow"。你可以在参数c上调用任何Show类型类函数。你不能像之前那样进行模式匹配,因为你不知道参数的确切类型,你只知道它实现了Show类型类。但是当你将Char作为类型指定时,模式匹配就起作用了。

2
一旦你在'g'上进行模式匹配,例如:
pm 'g' = "wow"

你的函数不再具有(Show a) => a -> String类型;相反,它具有'a'的具体类型,即Char,因此它变成了Char -> String

这与你明确给出的类型签名直接冲突,该签名说明你的函数适用于任何类型'a'(只要该类型是Show的实例)。

在这种情况下,你不能进行模式匹配,因为你正在对Int、Char等进行模式匹配。但是你可以使用Prelude中的show函数:

pm x = case show x of
             "'g'" -> "My favourite Char"
             "1"   -> "My favourite Int" 
             _     -> show x

正如你可能猜到的那样,show函数有点神奇。实际上,对于每个属于Show类型类实例的类型,都实现了一整套show函数。


感谢你的完美解释,但Ankur的解决方案对于问题的第一部分也更加完整。不过你在这里的解释非常准确。+1 - Rasmus

1
tell :: (Show a) =>a->String

这段话意思是说tell接受任何类型a的值,该值可以显示。您可以在任何可显示的内容上进行调用。这意味着在tell的实现中,您必须能够对任何可显示的内容进行操作。
您可能认为这将是该类型签名的一个可以接受的实现:
tell [] = "The list is empty"

因为列表可以展示,而且是第一个参数的有效值。但是我在检查参数是否为空列表;只有列表类型的值才能与列表模式(如空列表模式)匹配,因此如果我调用了 tell 1tell Truetell (1, 'c') 等,则没有意义。

tell 内部,类型 a 可以是任何是 Show 实例的类型。因此,我可以对该值执行的唯一操作是对所有是 Show 实例的类型都有效的操作。这基本上意味着您只能将其传递给具有通用 Show a => a 参数的其他类似函数。1

你的困惑源于这个误解:“考虑到这里的 a 是列表而不是列表的元素”关于类型签名 tell :: (Show a) => [a] -> String。这里的 a 实际上是列表的元素,而不是列表本身。

那个类型签名的意思是“tell接受一个参数,该参数是一些可显示类型的列表,并返回一个字符串”。这个版本的知道它接收到了一个列表,因此它可以对其参数执行列表操作。列表中的内部元素是某个未知类型的成员。

1 大多数这些函数也无法对值进行任何操作,除了将其传递给另一个 Show 函数,但迟早该值将被忽略或传递给 Show 类型类中的实际函数之一;每种类型都有专门的实现,因此每个专门的版本都知道它正在操作的类型,这是最终完成任何操作的唯一方式。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接