17

一般来说,我对 Haskell 和 FP 很陌生。我已经阅读了许多描述什么是柯里化的著作,但我还没有找到关于它实际上是如何工作的解释。

这是一个函数:(+) :: a -> (a -> a) 如果我这样做(+) 4 7,该函数接受4并返回一个接受7并返回的函数11。但是会发生什么4?第一个函数有什么作用4?有什么(a -> a)7

当我想到一个更复杂的函数时,事情变得更加混乱:

max' :: Int -> (Int -> Int)
max' m n | m > n = m
         | otherwise = n

(Int -> Int)将其参数与什么进行比较?它只需要一个参数,但它需要两个来做m > n

4

5 回答 5

18

理解高阶函数

Haskell 作为一种函数式语言,支持高阶函数(HOF)。在数学中,HOF 被称为泛,但您不需要任何数学来理解它们。在通常的命令式编程中,就像在 Java 中一样,函数可以接受值,比如整数和字符串,对它们做一些事情,然后返回一个其他类型的值。

但是,如果函数本身与值没有什么不同,并且您可以接受一个函数作为参数或从另一个函数返回它呢?f a b c = a + b - c是一个无聊的函数,它先求和a再减。但是这个函数可能会更有趣,如果我们可以概括它,如果我们有时想求和,但有时会相乘怎么办?还是除以而不是减?bcabc

请记住,(+)它只是一个返回一个数字的 2 个数字的函数,它没有什么特别之处,因此任何返回一个数字的 2 个数字的函数都可以代替它。写作g a b c = a * b - ch a b c = a + b / c类的东西对我们来说并不合适,我们需要一个通用的解决方案,毕竟我们是程序员!这里是如何在 Haskell 中完成的:

let f g h a b c = a `g` b `h` c in f (*) (/) 2 3 4 -- returns 1.5

你也可以返回函数。下面我们创建一个函数,它接受一个函数和一个参数并返回另一个函数,该函数接受一个参数并返回一个结果。

let g f n = (\m -> m `f` n); f = g (+) 2 in f 10 -- returns 12

(\m -> m `f` n)构造是一个有1 个参数的匿名函数m,适用fmn。基本上,当我们调用时,g (+) 2我们创建一个有一个参数的函数,它只会在它收到的任何内容上加 2。所以let f = g (+) 2 in f 10等于 12 和let f = g (*) 5 in f 5等于 25。

(另请参阅以 Scheme 为例对 HOF 的解释。)

理解柯里化

柯里化是一种将多个参数的函数转换为 1 个参数的函数的技术,该函数返回一个具有 1 个参数的函数,该函数返回一个具有 1 个参数的函数......直到它返回一个值。这比听起来容易,例如我们有一个有 2 个参数的函数,比如(+).

现在想象一下,你可以只给它一个参数,它会返回一个函数?您可以稍后使用此函数将现在包含在此新函数中的第一个参数添加到其他内容中例如:

f n = (\m -> n - m)
g = f 10
g 8 -- would return 2
g 4 -- would return 6

猜猜看,Haskell 默认对所有函数进行柯里化。从技术上讲,Haskell 中没有多参数的函数,只有一个参数的函数,其中一些可能会返回一个参数的新函数。

从种类上就可以看出来。:t (++)在解释器中编写,(++)将 2 个字符串连接在一起的函数在哪里,它将返回(++) :: [a] -> [a] -> [a]. 类型不是[a],[a] -> [a],而是[a] -> [a] -> [a],意思是(++)接受一个列表并返回一个类型的函数[a] -> [a]。这个新函数可以接受另一个列表,它最终会返回一个新的 type 列表[a]

这就是为什么 Haskell 中的函数应用程序语法没有括号和逗号,比较 Haskellf a b c和 Python 或 Java 的f(a, b, c). 这不是什么奇怪的审美决定,在 Haskell 函数应用程序中是从左到右的,f a b c实际上也是如此(((f a) b) c),这完全有道理,一旦你知道f默认情况下是咖喱。

然而,在类型中,关联是从右到左的,因此[a] -> [a] -> [a]等价于[a] -> ([a] -> [a]). 它们在 Haskell 中是一样的,Haskell 对待它们完全相同。这是有道理的,因为当你只应用一个参数时,你会得到一个 type 的函数[a] -> [a]

另一方面,检查map:的类型(a -> b) -> [a] -> [b],它接收一个函数作为它的第一个参数,这就是它有括号的原因。

要真正深入了解柯里化的概念,请尝试在解释器中查找以下表达式的类型:

(+)
(+) 2
(+) 2 3
map
map (\x -> head x)
map (\x -> head x) ["conscience", "do", "cost"]
map head
map head ["conscience", "do", "cost"]

部分应用和部分

现在您了解了 HOF 和柯里化,Haskell 为您提供了一些语法来缩短代码。当你调用一个有 1 个或多个参数的函数来取回一个仍然接受参数的函数时,它被称为部分应用程序

您已经明白,您可以部分应用一个函数,而不是创建匿名函数,因此(\x -> replicate 3 x)您可以只编写(replicate 3). 但是如果你想要一个除法(/)运算符而不是replicate? 对于中缀函数,Haskell 允许您使用任一参数部分应用它。

这称为sections :(2/)等价于(\x -> 2 / x)并且(/2)等价于(\x -> x / 2)。使用反引号,您可以获取任何二进制函数的一部分:(2`elem`)相当于(\xs -> 2 `elem` xs).

但是请记住,任何函数在 Haskell 中默认情况下都是柯里化的,因此总是接受一个参数,因此节实际上可以与任何函数一起使用:假设(+^)是一个奇怪的函数,它对 4 个参数求和,然后let (+^) a b c d = a + b + c in (2+^) 3 4 5返回 14。

作曲

编写简洁灵活的代码的其他方便工具是组合应用程序运算符。组合运算符(.)将功能链接在一起。应用运算符($)只是将左侧的函数应用于右侧的参数,因此f $ x等效于f x. 然而($)在所有运算符中具有最低的优先级,所以我们可以用它来去掉括号:f (g x y)相当于f $ g x y.

当我们需要对同一个参数应用多个函数时,它也很有帮助:map ($2) [(2+), (10-), (20/)]将 yield [4,8,10](f . g . h) (x + y + z), f (g (h (x + y + z))), f $ g $ h $ x + y + zandf . g . h $ x + y + z是等价的,但是(.)and($)是不同的东西,所以请阅读Haskell: 之间的区别。(点)和 $(美元符号)以及Learn You a Haskell中的部分内容以了解差异。

于 2015-02-19T20:50:18.070 回答
14

你可以把它想象成函数存储参数并返回一个只需要其他参数的新函数。新函数已经知道第一个参数,因为它与函数一起存储。这由编译器在内部处理。如果你想知道它是如何工作的,你可能会对这个页面感兴趣,尽管如果你是 Haskell 的新手,它可能会有点复杂。

如果函数调用完全饱和(因此所有参数同时传递),大多数编译器使用普通调用方案,就像在 C 中一样。

于 2011-07-11T15:36:22.903 回答
13

这有帮助吗?

max' = \m -> \n -> if (m > n)
                       then m
                       else n

写成 lambdas。max' 是一个 lambda 的值,它本身返回一个给定一些 m 的 lambda,它返回该值。

因此 max' 4 是

max' 4 = \n -> if (4 > n)
                   then 4
                   else n
于 2011-07-11T15:28:13.153 回答
3

如果您来自类 C 语言,它们的语法可能会帮助您理解它。例如在 PHP 中 add 函数可以这样实现:

function add($a) {
    return function($b) use($a) {
         return $a + $b;
    };
}
于 2011-07-12T07:54:41.590 回答
3

Something that may help is to think about how you could implement curry as a higher order function if Haskell didn't have built in support for it. Here is a Haskell implementation that works for a function on two arguments.

curry :: (a -> b -> c) -> a -> (b -> c)
curry f a = \b -> f a b

Now you can pass curry a function on two arguments and the first argument and it will return a function on one argument (this is an example of a closure.)

In ghci:

Prelude> let curry f a = \b -> f a b
Prelude> let g = curry (+) 5
Prelude> g 10
15
Prelude> g 15
20
Prelude> 

Fortunately we don't have to do this in Haskell (you do in Lisp if you want currying) because support is built into the language.

于 2011-07-11T17:19:55.157 回答