Haskell中的类型

9

我在Haskell方面比较新手,对于类型推断等机制的理解存在困难。

map :: (a -> b) -> [a] -> [b]
(.) :: (a -> b) -> (c -> a) -> c -> b

这到底是什么意思?

foldr :: (a -> b -> b) -> b -> [a] -> b
foldl :: (a -> b -> a) -> a -> [b] -> a
foldl1 :: (a -> a -> a) -> [a] -> a

这些有什么不同呢?
foldr map的推断类型是[a] -> [a -> a] -> [a] 但为什么会是这样呢?
谢谢!

1
你是否熟悉柯里化的概念? - Fletcher Moore
1
我相信这是将多参数函数转换为只有1个参数的概念。类似于f(x,y)=(f y)o(g x)? - Linda Cohen
在ghci(交互式haskell shell)中,您可以使用:t命令查找某个东西的类型:>> :t foldr map foldr map :: [b] -> [b -> b] -> [b] - jkramer
1
顺便提一下,在 Stack Overflow 上接受提供正确解决方案的答案被认为是良好的行为规范。到目前为止你已经问了六个问题,但没有接受任何答案。 - C. A. McCann
@camccann 谢谢你的建议。我之前不知道这个。现在我会去做的。 - Linda Cohen
显示剩余4条评论
4个回答

10

如果你以...为例

map :: (a -> b) -> [a] -> [b]

这意味着map函数需要两个参数:

  1. 从类型a到类型b的函数
  2. 类型为a的列表

它返回:

  1. b类型的列表

你已经可以看出其中的模式,但是当我们用'a'和'b'替换时,它将更加清晰。

  • a = 字符串(String)
  • b = 整数(Int)

因此,这个类型定义将是:

map :: (String -> Int) -> [String] -> [Int]

现在它是一个接受字符串并返回整数以及接受字符串列表并返回整数列表的函数。

假设我们的接受字符串并返回整数的函数为readread使用您提供的字符串将其转换为不同的内容。因为我们在此处将其放入整数上下文中,所以它会将字符串转换为整数。

map read ["1", "2", "112", 333"]

这将导致

[1, 2, 112, 333]
因为 map 接受你的函数 read 并将其应用于列表的每个元素。你不必告诉 read 它的参数,比如 read "1" 或者 read n,因为这些都由 map 代劳。
这就是所有内容了。一个函数的类型只表示它接受哪些类型的参数和返回什么类型的值。当然,还有柯里化,但你可能会在后面无意中或有意地学习到它!它基本上意味着一个函数不接受 多个 参数,而只接受一个。比如你取函数 +。如果你计算 1+2,它会返回一个函数,该函数接受另一个数字,该数字将添加到 1 中,并且因为这里确实有另一个数字 2,所以它将使用它。你也可以对其求值为 (1+),稍后再添加数字。当你没有中缀语法的加号时,它更清晰。它可以被写成 (+) 1 2,因此首先这个语句变成 (+) 1,它的类型(简化! 我不会在这里介绍Num类型类)为 Int -> Int。因此,(+) 1(或者说 (1+))只是另一个你可以将值应用于其中的函数,在这一点上,结果将能够计算为3。
以下是它在实践中的样子:(如果这里的 (Num a) => 让你困惑,那么请忽略它,这是你之后会更多了解的概念。如果你愿意,可以将此处的 a 类型替换为 Int,并完全忽略 (Num a) => 部分。)
 (+)  :: (Num a) => a -> a -> a
 (+2) :: (Num a) => a -> a
(1+)  :: (Num a) => a -> a
(1+2) :: (Num a) => a

Prelude> (+2) 5
7
Prelude> map (+3) [1,2,3]
[4,5,6]

针对你的第二个问题:你根本不需要定义推断类型。它们被称为“推断”,因为编译器/解释器会自动“推断”(计算)类型,而无需显式命名它们。

关于foldX函数的区别:它们都执行相同的操作:将列表减少为单个值。这些函数之间的差异仅在于内部定义。foldl从左侧折叠列表,而foldr则从右侧进行折叠。

因此,你可以像这样使用所有这些函数来汇总一个列表...

foldl1 (+) [1,2,3] == 6
foldr (+) 0 [1,2,3] == 6
foldl (+) 0 [1,2,3] == 6

除了foldl1之外,你需要向fold提供一个函数、一个起始值(累加器)和一个要折叠的列表。而fold1有它自己的累加器,你不需要提供你自己的。

在实践中,你应该使用foldr,因为与fold不同,它适用于大型列表,避免由于堆栈溢出而崩溃(哈哈)。这个规则的例外是Data.Map中的foldl'(注意“'”!)- 它是一个严格的折叠,也适用于大型列表。

如果你想给函数指定类型(如果你是写它们的人),可以考虑以下示例:

double :: Int -> Int
double n = 2 * n

或者用 Haskell 的方式

double :: Int -> Int
double = (*2)

两个例子的第一行 (double :: Int -> Int) 是你要找的。这是如何强制编译器或解释器识别函数的方法 - 你可以省略它们,编译器/解释器会找出它们(有时甚至比人们最初想象的还要好)


3
在Haskell中,函数使用一种称为柯里化的符号表示法。像这样的定义:
plus :: Int -> Int -> Int

意味着plus是一个函数,它接收两个Int并返回最右边的类型的值(再次是Int)。更详细地说,柯里化意味着你可以使用部分应用的函数编写代码,就像这样:

plus1 :: Int -> Int
plus1 = plus 1

在类型签名中使用小写标识符表示所谓的类型变量,可以将其视为任何可能类型的占位符。

定义:

map :: (a -> b) -> [a] -> [b]

因此,对于任何类型的a和b,map接受从a到b的函数和a的列表,并从中生成b的列表。让我们举个例子。
map show [1, 2, 3]

我们可以看到,给定的列表是一个由Int类型元素组成的列表,因此a=Int。此外,map函数应用于每个元素时需要调用show函数,它返回一个String类型。由于show函数必须适合定义中的a->b类型,我们可以得出b=String。
我们的表达式返回一个[b],因此结果是一个[String]。
为了使这种签名更加清晰,我们可以使用更详细的语法来书写。
map :: forall a b . (a -> b) -> [a] -> [b]

现在明确说明map应该适用于所有类型的ab


关于折叠,可以查看它们的定义。在了解上述知识的基础上,当清楚函数应该做什么时,类型签名应该是不言自明的。

例如:factorial n = foldl (*) 1 [1..n]

foldr map的签名变为:

foldr map :: [a] -> [a -> a] -> [a]

您也可以通过在Haskell解释器(GHCi)中键入:t foldr map来获取这些内容。


注:foldr和map是Haskell中的两个函数名,无法直接翻译。

1
在Haskell中,小写变量是类型变量。而Char只是表示Charab可以表示CharBoolInteger或其他一些类型。关键在于,所有的a都将是同一种类型。所有的b也将是同一种类型。
例如,map以它的第一个参数作为函数,这个函数接受一个类型为a的变量并返回另一个类型为b的变量。然后,map返回一个新函数,这个新函数接受一个类型为a的列表,并返回一个类型为b的列表。
假设你有一个函数increment,它会给一个整数加1。map increment [1,2,3]最终将返回一个列表[2,3,4]。
用类型变量替换前面的map的定义将如下所示: map :: increment -> [1,2,3] -> [2,3,4]

增量,顺便说一下,看起来像这样:

increment :: Integer -> Integer

之所以写得有些奇怪,是因为你可以部分应用函数。在这里,我将基于对 map 函数的部分应用,创建一个新函数:

incrementList = map increment

然后你可以使用

incrementList [1,2,3]

这将产生相同的结果


1
类型声明中的小写字母是“类型变量”。这意味着当有一个“a”时,它必须是相同的类型。
方括号(例如 (a -> b))表示子表达式接受函数作为其参数之一。
矩形括号表示某些东西的列表。
因此,map接受以下参数:
- (a -> b):一个接受一个参数的函数。 - [a]:第一个参数函数可以操作的列表。
并产生[b]作为结果的列表。
我们可以以相同的方式解码(.):
- (a -> b) 是一个接受单个参数的函数。 - (c -> a) 是一个生成与第一个函数的参数相同类型的函数。 - c是第二个函数使用的相同类型的参数。

然而,Haskell 可以做一个巧妙的事情,就是如果省略一个参数,它会返回一个具有剩余签名的函数。因此,(.) 通常用于链接函数。

假设我们有两个函数 ord :: Char -> Int,它接受一个字符并返回其整数表示形式,以及 add32 :: Int -> Int,它将 32 加到其参数上。

当我们编写 upcaseVal = add32 . ord 时,函数 upcaseVal 现在具有签名 upcaseVal :: Char -> Int,因为(.) 函数会让它保留 c -> b,我们在函数定义中替换这些值。

所以,foldr map 应该像这样:

  1. foldr的第一个参数是一个接受两个参数的函数,而map也是如此。
  2. 由于函数的输出必须与第二个参数的类型相同((a -> b -> b),我们可以看到在mapa = b

因此最终的类型将是:

(b -> b) -> [b] -> [b] -> b -> [b] -> b

但是我猜这并不需要让你太过烦恼,因为编译器大部分时间都会处理这类问题。

虽然我可能对 foldr map 类型感到困惑 - 但请不要完全相信。 - Jakub Hampl
foldr map 的类型是 [b] -> [b -> b] -> [b] - sepp2k

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