总体来说,我对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
需要两个参数。
6条答案
按热度按时间mzmfm0qo1#
了解高阶函数
作为函数式语言,Haskell支持高阶函数(HOF)。在数学中,HOF被称为functionals,但你不需要任何数学来理解它们。在通常的命令式编程中,比如在Java中,函数可以接受值,比如整数和字符串,对它们做一些事情,然后返回一些其他类型的值。
但是,如果函数本身与值没有什么不同,并且可以接受一个函数作为参数,或者从另一个函数返回它,那会怎么样?
f a b c = a + b - c
是一个无聊的函数,它将a
与b
相加,然后减去c
。但是,如果我们可以将它 * 泛化 *,这个函数可能会更有趣。如果我们有时想求和,有时想乘,或者用c
除,而不是减法呢?请记住,
(+)
只是一个返回一个数字的2个数字的函数,它没有什么特别之处,所以任何返回一个数字的2个数字的函数都可以代替它。写g a b c = a * b - c
,h a b c = a + b / c
等等对我们来说并不合适,我们需要一个通用的解决方案,毕竟我们是程序员!下面是在Haskell中如何完成的:你也可以返回函数。下面我们创建一个函数,它接受一个函数和一个参数,然后返回另一个函数,这个函数接受一个参数,然后返回一个结果。
一个
(\m -> m
fn)
结构是一个anonymous function,它有一个参数m
,它把f
应用到m
和n
上。基本上,当我们调用g (+) 2
时,我们创建了一个只有一个参数的函数,它只把它接收到的值加2。所以let f = g (+) 2 in f 10
等于12,let f = g (*) 5 in f 5
等于25。(See以Scheme为例,也可以是my explanation of HOFs。)
了解咖喱
Currying是一种将多个参数的函数转换为一个参数的函数的技术,该函数返回一个参数的函数,该函数返回一个参数的函数......直到它返回一个值。这比听起来容易,例如,我们有一个两个参数的函数,如
(+)
。现在假设你只给予它一个参数,它会返回一个函数。你可以在以后用这个函数把第一个参数添加到其他函数中。例如:
你猜怎么着,Haskell默认情况下curries所有函数,从技术上讲,Haskell中没有多个参数的函数,只有一个参数的函数,其中一些可能会返回一个参数的新函数。
从类型上看很明显。在解释器中编写
:t (++)
,其中(++)
是一个将两个字符串连接在一起的函数,它将返回(++) :: [a] -> [a] -> [a]
。类型不是[a],[a] -> [a]
,而是[a] -> [a] -> [a]
,这意味着(++)
接受一个列表,并返回一个[a] -> [a]
类型的函数。这个新函数还可以接受另一个列表,并且它最终将返回类型为[a]
的新列表。这就是为什么Haskell中的函数应用语法没有括号和逗号,把Haskell的
f a b c
和Python或Java的f(a, b, c)
比较一下,这不是什么奇怪的美学决定,在Haskell中,函数应用是从左到右的,所以f a b c
实际上是(((f a) b) c)
,这是完全有意义的,只要你知道f
在默认情况下是curry的。然而,在类型中,关联是从右到左的,所以
[a] -> [a] -> [a]
等价于[a] -> ([a] -> [a])
。它们在Haskell中是一样的,Haskell完全一样地对待它们。这是有意义的,因为当你只应用一个参数时,你得到的是[a] -> [a]
类型的函数。另一方面,检查
map
的类型:(a -> b) -> [a] -> [b]
,它接收一个函数作为它的第一个参数,这就是它有括号的原因。要真正理解currying的概念,请尝试在解释器中找到以下表达式的类型:
部分应用程序和部分
现在你已经了解了HOF和currying,Haskell会给你一些语法来缩短代码:当你调用一个带有一个或多个参数的函数来取回一个仍然接受参数的函数时,它被称为partial application。
你已经知道,你可以部分地应用一个函数而不是创建匿名函数,所以你可以写
(replicate 3)
而不是写(\x -> replicate 3 x)
。但是如果你想用一个除法运算符(/)
而不是replicate
呢?对于中缀函数,Haskell允许你使用任何一个参数来部分地应用它。这称为sections:
(2/)
等价于(\x -> 2 / x)
,(/2)
等价于(\x -> x / 2)
。使用反勾号,您可以获取任何二进制函数的一部分:(2
elem)
与(\xs -> 2
elemxs)
相等。但是请记住,在Haskell中,任何函数都是默认的,因此总是接受一个参数,所以section实际上可以与任何函数一起使用:假设
(+^)
是一个奇怪的函数,它对4个参数求和,那么let (+^) a b c d = a + b + c in (2+^) 3 4 5
返回14。成分
其他编写简洁灵活的代码的方便工具有composition和application operator。复合运算符
(.)
将函数链接在一起。应用运算符($)
只将左侧的函数应用于右侧的参数,因此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
和f . g . h $ x + y + z
是等价的,但是(.)
和($)
是不同的东西,所以请阅读Haskell: difference between . (dot) and $ (dollar sign)和Learn You a Haskell中的部分内容来了解它们的区别。q3qa4bjr2#
你可以这样想,函数存储参数,然后返回一个新函数,这个新函数只需要其他参数。新函数已经知道第一个参数,因为它和函数存储在一起。这是由编译器内部处理的。如果你想知道它是如何工作的,你可能会对这个页面感兴趣,尽管如果你是Haskell的新手,它可能会有点复杂。
如果一个函数调用是完全饱和的(所以所有的参数都是同时传递的),大多数编译器都会使用普通的调用方案,就像C语言一样。
ct2axkht3#
这有帮助吗?
写为lambdas.max '是一个lambda的值,它本身返回一个lambda,给定一些m,它返回值。
因此,max' 4为
zvms9eto4#
如果Haskell没有内置的对curry的支持,那么考虑一下如何将咖喱实现为高阶函数可能会有所帮助。下面是一个针对两个参数的函数的Haskell实现。
现在你可以给
curry
传递一个两个参数的函数和第一个参数,它将返回一个参数的函数(这是一个闭包的例子)。在ghci中:
幸运的是,我们不必在Haskell中这样做(如果你想做的话,你可以在Lisp中这样做),因为支持是内置在语言中的。
guykilcj5#
如果你来自类似C的语言,它们的语法可能会帮助你理解它。例如,在PHP中,add函数可以这样实现:
bvjveswy6#
Haskell基于Lambda演算。在内部发生的事情是所有的东西都被转换成一个函数。所以你的编译器计算
(+)
如下也就是说,
(+) :: a -> a -> a
本质上与(+) :: a -> (a -> a)
相同。希望这对您有所帮助。