在《Real World Hashell》一书里,有一段通过foldr表示foldl的代码
1 | myFoldl :: (a -> b -> a) -> a -> [b] -> a |
刚看到这的时候,感觉特别难懂,尤其是之前没有函数式编程的经历,对柯里化了解不深的情况下,更加一头雾水
如果是顺序读这本书的话,我们应该已经知道了foldr
的签名
1 | Prelude> :type foldr |
困惑
这里最让人感到奇怪的有两点
foldr
的签名是接受三个参数,如下- 一个函数,这个函数接受两个参数,类型a和类型b,返回一个类型b的值
- 一个类型b的初始值
- 一个可折叠类型a,这里我们简单当作一个列表
但是,实际上我们传递了四个参数
上面说了,
foldr
的第一个参数是个函数,接受两个参数,但传递过去的step函数,有三个参数
上述两点,也可以总结成一点:参数个数不匹配。
Currying
要理解这个问题,我们需要一些其他知识,也就是“部分函数应用和柯里化”(Currying)
这部分内容在原书后续有讲到
这两部分是相辅相成的,理解了部分函数应用和柯里化,会更容易弄明白foldr表示foldl;而弄清楚foldr表示foldl,则会对部分函数应用和柯里化有更深刻的理解
在说部分函数应用和柯里化之前,我们先看看一般命令式语言中的现象
比如有如下Java代码,两数相加
1 | private int add(int a, int b) { |
这个函数需要两个参数a和b,然后返回两数之和
我们可以通过add(1, 2)
的方式调用这个函数,但是,add(1)
则是不合法的,因为没有此种方法签名
与Java不同的是,在Haskell中,这是允许的
1 | Prelude> add a b = a + b |
在Haskell中,本质上,所有的函数都只接受一个参数
上面的add
函数,看上去接受两个类型为a的参数,返回一个类型为a的值,实际上是先接受一个类型为a的参数,返回一个函数,这个函数再接受一个类型为a的参数,返回类型为a的值
就像我们有一个机床,可以将一堆铁疙瘩制作成各种机器,然后我们可以用制造出的机器,将原料加工成成品
现在我们要做衣服,做衣服需要布料,需要使用缝纫机,我们还没缝纫机,不过我们有机床,有铁疙瘩,可以造一个出来,在Haskell里可以表示为
1 | type Iron = String |
如果用类似Java语言解释这个签名的话,就是machineTool
这个函数需要两个参数,铁和布,将这两个参数同时放进去,会出来一个衣服
在Haskell里,并不需要同时传两个参数,我们可以只传一个
1 | sewMachine = machineTool "a piece of iron" |
这是合法的,表示给机床一块铁,做出了一台缝纫机,缝纫机是个函数,可以继续接受参数,它现在接受一个Cloth类型的参数,返回Clothes类型的结果
1 | *Main> :type sewMachine |
这个缝纫机可以接受一匹布,产生一件衣服 sewMachine "a piece of cloth"
解析
回到最初的右折叠表示左折叠问题
1 | myFoldl f z xs = foldr step id xs z |
再看一下foldr的签名
1 | Prelude> :type foldr |
foldr第一个参数是函数,其返回值类型为b,和foldr第二个参数,也就是foldr的初始值类型一样,也是b
在foldr step id xs z
这里,第二个参数,也就是初始值,是id
1 | Prelude> :type id |
我们看到id是一个函数,接受一个类型为a的参数,返回的类型同样也是a(实际上id
是identity
的缩写,本身的意思,id
作用是返回参数本身)
既然初始值类型是函数,那foldr的第一个参数,返回的也是一个函数
我们看到step的定义,它可以接受三个参数,但是在foldr的应用过程中,只会应用两个(列表的从右往左下一个值和初始值或者上一次函数的计算结果),结合函数的部分应用和柯里化,我们知道step函数在应用两个参数后,返回的是另一个函数,这个函数接受一个参数(step函数参数的第三个)
也就是foldr的第一个参数,类型为(a -> b -> b)
,在实际执行过程中,a
类型是列表内容的类型,第二个参数b
类型,第一次是初始值,也就是id
,实际类型是一个函数,同时,结果仍然是b
,是一个函数,作为下一次计算(a -> b -> b)
的第二参数
由于foldr的最终返回结果是一个函数,所以可以应用一个参数,这个参数在foldr step id xs z
这里,也就是z
,或者说可以把foldr step id xs z
看作是(foldr step id xs) z
,foldr全部折叠完,其最后结果是一个函数,然后用z作为函数的参数对这个最终函数进行调用
展开
参数个数的问题弄清楚后,我们可以把表达式进行展开,将实际参数带入到表达式的变量中
假设我们现在使用myFoldl
对一个列表求和,列表是[1,2]
,求和的初始值是0
按照左折叠的规则,我们预期执行顺序应该是((0 + 1) + 2)
,从初始值和列表第一个元素开始相加,其和与列表的第二个元素继续相加,如此从左到右一次计算整个列表,并返回最终结果
1 | myFoldl f z xs = foldr step id xs z |
经过上述将实际参数带入到变量,推导后,myFoldl的行为确实与预期一样,我们就这样通过foldr表示了foldl