理解高阶函数
Haskell 作为一种函数式语言,支持高阶函数 (HOF)。在数学中,HOF 被称为泛函 http://en.wikipedia.org/wiki/Functional_%28mathematics%29,但你不需要任何数学知识就能理解它们。在通常的命令式编程中,例如在 Java 中,函数可以接受值(例如整数和字符串),对它们执行某些操作,然后返回某种其他类型的值。
但是,如果函数本身与值没有什么不同,并且您可以接受函数作为参数或从另一个函数返回它,该怎么办?f a b c = a + b - c
是一个无聊的函数,它求和a
and b
然后减去c
。但如果我们可以的话,这个函数可能会更有趣概括如果我们有时想要求和怎么办a
and b
,但有时会相乘?或者除以c
而不是减去?
记住,(+)
只是一个返回数字的 2 个数字的函数,没有什么特别的,因此任何返回数字的 2 个数字的函数都可以代替它。写作g a b c = a * b - c
, h 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
A (\m -> m `f` n)
构造是一个匿名函数 https://wiki.haskell.org/Anonymous_function共 1 个参数m
适用f
对此m
and n
。基本上,当我们打电话时g (+) 2
我们创建一个只有一个参数的函数,它只会将 2 添加到它收到的任何值上。所以let f = g (+) 2 in f 10
等于 12 并且let f = g (*) 5 in f 5
等于 25。
(See also my explanation of HOFs https://stackoverflow.com/a/27657332/596361 using Scheme as an example.)
了解柯里化
Currying http://en.wikipedia.org/wiki/Currying是一种将多个参数的函数转换为一个具有 1 个参数的函数的技术,该函数返回一个具有 1 个参数的函数,该函数返回一个具有 1 个参数的函数...直到它返回一个值。这比听起来更容易,例如我们有一个有 2 个参数的函数,比如(+)
.
Now imagine that you could give only 1 argument to it, and it would return a function? You could use this function later to add this 1st argument, now encased in this new function, to something else. E.g.:
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]
, but [a] -> [a] -> [a]
, 意思是(++)
接受一个列表并返回一个类型的函数[a] -> [a]
。这个新函数可以接受另一个列表,它最终将返回一个新类型的列表[a]
.
这就是为什么 Haskell 中的函数应用语法没有括号和逗号,比较 Haskell 的f a b c
使用Python或Javaf(a, b, c)
。这不是什么奇怪的审美决定,在 Haskell 函数应用程序中从左到右,所以f a b c
实际上是(((f a) b) c)
,这完全有道理,一旦你知道了f
默认情况下是柯里化的。
然而,在类型中,关联是从右到左的,所以[a] -> [a] -> [a]
相当于[a] -> ([a] -> [a])
。它们在 Haskell 中是同一件事,Haskell 对待它们的方式完全相同。这是有道理的,因为当您仅应用一个参数时,您会返回一个类型的函数[a] -> [a]
.
另一方面,检查类型map http://hackage.haskell.org/package/base-4.7.0.1/docs/Prelude.html#v: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 个或多个参数的函数以返回仍接受参数的函数时,它被称为部分应用 https://wiki.haskell.org/Partial_application.
您已经了解,您可以只部分应用函数,而不是创建匿名函数,因此不必编写(\x -> replicate 3 x)
你可以写(replicate 3)
。但如果你想有一个分歧怎么办(/)
运算符而不是replicate
?对于中缀函数,Haskell 允许您使用任一参数部分应用它。
这就是所谓的sections https://wiki.haskell.org/Section_of_an_infix_operator: (2/)
相当于(\x -> 2 / x)
and (/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。
作文
其他编写简洁灵活代码的方便工具是作品 https://wiki.haskell.org/Function_composition and 应用运营商 https://wiki.haskell.org/%24。组合运算符(.)
将功能链接在一起。应用运营商($)
只是将左侧的函数应用于右侧的参数,所以f $ x
相当于f x
。然而($)
具有所有运算符中最低的优先级,因此我们可以使用它来去掉括号:f (g x y)
相当于f $ g x y
.
当我们需要将多个函数应用于同一个参数时,它也很有帮助:map ($2) [(2+), (10-), (20/)]
会产生[4,8,10]
. (f . g . h) (x + y + z)
, f (g (h (x + y + z)))
, f $ g $ h $ x + y + z
and f . g . h $ x + y + z
是等价的,但是(.)
and ($)
是不同的东西,所以请阅读哈斯克尔: 之间的区别。 (点)和 $(美元符号) https://stackoverflow.com/q/940382/596361 and 学习 Haskell 的部分内容 http://learnyouahaskell.com/higher-order-functions#function-application了解其中的差异。