与某些人可能认为 Haskell 中的一切都不是函数相反。严重地。数字、字符串、布尔值等不是函数。甚至没有零函数。
零函数
空函数是不带参数并执行一些“副作用”计算的函数。例如,考虑这个无效的 JavaScript 函数:
main();
function main() {
alert("Hello World!");
alert("My name is Aadit M Shah.");
}
不带参数的函数只有在有副作用的情况下才能返回不同的结果。因此,它们类似于 Haskell 中的 IO 操作,不带参数并执行一些副作用计算:
main = do
putStrLn "Hello World!"
putStrLn "My name is Aadit M Shah."
一元函数
相比之下,Haskell 中的函数永远不能为空。事实上,Haskell 中的函数总是一元的。 Haskell 中的函数总是只有一个参数。 Haskell 中的多参数函数可以使用柯里化或使用具有多个字段的数据结构来模拟。
add' :: Int -> Int -> Int -- an example of using currying
add' x y = x + y
add'' :: (Int, Int) -> Int -- an example of using multi-field data structures
add'' (x, y) = x + y
协变和逆变
Haskell 中的函数是一种数据类型,就像您在 Haskell 中定义的任何其他数据类型一样。但是,函数很特殊,因为它们是contravariant in the argument type and covariant in the return type。
当您定义一个新的代数数据类型时,其类型构造函数的所有字段都是协变的(即数据源)而不是逆变的(即数据汇)。协变字段产生数据,而逆变字段消耗数据。
例如,假设我创建了一个新的数据类型:
data Foo = Bar { field1 :: Char, field2 :: Int }
| Baz { field3 :: Bool }
这里的字段field1、field2 和field3 是协变的。它们分别产生Char、Int 和Bool 类型的数据。考虑:
let x = Baz True -- I create a new value of type Foo
in field3 x -- I can access the value of field3 because it is covariant
现在,考虑函数的定义:
data Function a b = Function { domain :: a -- the argument type
, codomain :: b -- the return type
}
当然,函数实际上并没有如下定义,但我们假设它是这样定义的。一个函数有两个字段domain 和codomain。当我们创建 Function 类型的值时,我们不知道这两个字段中的任何一个。
- 我们不知道
domain 的值,因为它是逆变的。因此,它需要由用户提供。
- 我们不知道
codomain 的值,因为尽管它是协变的,但它可能取决于domain,我们不知道domain 的值。
例如,\x -> x + x 是一个函数,其中domain 的值为x,codomain 的值为x + x。这里domain 是逆变的(即数据接收器),因为数据通过domain 进入函数。同样,codomain 是协变的(即数据源),因为数据通过 codomain 来自函数。
Haskell 中代数数据结构的字段(如我们之前定义的Foo)都是协变的,因为数据通过它们的字段从这些数据结构中出来。数据永远不会像 domain 函数字段那样进入这些结构。因此,它们永远不会是逆变的。
多参数函数
正如我之前解释的,虽然 Haskell 中的所有函数都是一元的,但我们可以使用柯里化或具有多个数据结构的字段来模拟多参数函数。
为了理解这一点,我将使用一个新的符号。减号 ([-]) 表示逆变类型。加号 ([+]) 表示协变类型。因此,从一种类型到另一种类型的函数表示为:
[-] -> [+]
现在,函数的域和共域可以分别替换为其他类型。例如在柯里化中,函数的 codomain 是另一个函数:
[-] -> ([-] -> [+]) -- an example of currying
请注意,当协变类型被替换为另一种类型时,新类型的方差将被保留。这是有道理的,因为这相当于一个有两个参数和一个返回类型的函数。
另一方面,如果我们用另一个函数替换域:
([+] -> [-]) -> [+]
请注意,当我们用另一种类型替换逆变类型时,新类型的方差会翻转。这是有道理的,因为虽然([+] -> [-]) 作为一个整体是逆变的,但它的输入类型变成了整个函数的输出,它的输出类型变成了整个函数的输入。例如:
function f(g) { // g is contravariant for f (an input value for f)
return g(x) + 10; // x is covariant for f (an output value for f)
// x is contravariant for g (an input value for g)
// g(x) is contravariant for f (an input value for f)
// g(x) is covariant for g (an output value for g)
// g(x) + 10 is covariant for f (an output value for f)
}
Currying 模拟多参数函数,因为当一个函数返回另一个函数时,我们会得到多个输入和一个输出,因为为返回类型保留了方差:
[-] -> [-] -> [+] -- a binary function
[-] -> [-] -> [-] -> [+] -- a ternary function
具有多个字段作为函数域的数据结构也可以模拟多参数函数,因为函数的参数类型会翻转方差:
([+], [+]) -- the fields of a tuple are covariant
([-], [-]) -> [+] -- a binary function, variance is flipped for arguments
非函数
现在,如果您看一下数字、字符串和布尔值等值,这些值不是函数。但是,它们仍然是协变的。
例如,5 自身产生一个值5。同样,Just 5 产生一个值Just 5,fromJust (Just 5) 产生一个值5。这些表达式都没有消耗一个值,因此它们都不是逆变的。但是,在Just 5 中,函数Just 使用值5,而在fromJust (Just 5) 中,函数fromJust 使用值Just 5。
因此,Haskell 中的所有内容都是协变的,除了函数的参数(它们是逆变的)。这很重要,因为 Haskell 中的每个表达式都必须计算出一个值(即产生一个值,而不是消耗一个值)。同时,我们希望函数消耗一个值并产生一个新值(从而促进数据的转换,beta reduction)。
最终的结果是我们永远不能有一个逆变表达式。例如,表达式Just 是协变的,而表达式Just 5 也是协变的。但是,在表达式Just 5 中,函数Just 使用值5。因此,逆变仅限于函数参数并受函数范围的限制。
因为 Haskell 中的每个表达式都是协变的,所以人们经常将像 5 这样的非函数值视为“空函数”。虽然这种直觉是有见地的,但它是错误的。值 5 不是空函数。这是一个不能被 beta 归约的表达式。同样,值fromJust (Just 5) 也不是空函数。它是一个表达式,可以 beta 简化为 5,它不是一个函数。
但是,表达式 fromJust (Just (\x -> x + x)) 是一个函数,因为它可以通过 beta 简化为 \x -> x + x,这是一个函数。
Pointful 和 Pointfree 函数
现在,考虑函数\x -> x + x。这是一个有意义的函数,因为我们通过给它命名 x 来明确声明函数的参数。
每个函数也可以用 pointfree 样式编写(即不显式声明函数的参数)。例如,函数\x -> x + x 可以用无点样式编写为join (+),如following answer 中所述。
请注意,join (+) 是一个函数,因为它在 beta 中简化为函数 \x -> x + x。它看起来不像一个函数,因为它没有点(即显式声明的参数)。但是,它仍然是一个函数。
Pointfree 函数与柯里化无关。 Pointfree 函数是关于编写没有点的函数(例如 join (+) 而不是 \x -> x + x)。柯里化是指一个函数返回另一个函数,从而允许部分应用(例如,\x -> \y -> x + y,可以用无点样式编写为(+))。
名称绑定
在绑定f = map 中,我们只是给map 提供了替代名称f。请注意,f 不会“返回”map。它只是map 的替代名称。例如,在绑定x = 5 中,我们不会说x 返回5,因为它没有。 x 这个名字既不是函数也不是值。它只是一个标识5 值的名称。同样,在f = map 中,名称f 只是标识map 的值。据说f 这个名字表示一个函数,因为map 表示一个函数。
绑定f = map 是无点的,因为我们没有明确声明f 的任何参数。如果我们愿意,我们可以写f g xs = map g xs。这将是一个有意义的定义,但由于eta conversion,我们可以更简洁地以无点形式将其写成f = map。 eta 转换的概念是\x -> f x 等价于f 本身,并且可以将pointful \x -> f x 转换为pointfree f,反之亦然。请注意,f g xs = map g xs 只是 f = \g xs -> map g xs 的语法糖。
另一方面,f = id . map 是一个函数,不是因为它是无点的,而是因为 id . map beta 简化为函数 \x -> id (map x)。顺便说一句,任何由id 组成的函数都等价于它自己(即id . f = f . id = f)。因此,id . map 等同于 map 本身。 f = map 和 f = id . map 没有区别。
请记住,f 不是“返回”id . map 的函数。为了方便起见,它只是给表达式id . map 取了一个名字。
附:有关无点函数的介绍,请阅读:
What does (f .) . g mean in Haskell?