遇到一个费心的例子,想了很久,备忘之.
<real world haskell>第四章:
myFoldl f z xs = foldr step id xs z
where step x g a = g(f a x)
这是用foldr来实现foldl.作者说看懂这个例子需要准备纸,笔,头痛药,时间.果然如此,可恨作者自己没有解释.看读者们的评论也大多表示了不满,给出的自己的困惑乃至解释都不太令人满意,但好歹一步一步接近了真理.
这个例子涉及到了partial function(haskell称为currying)特性.书中前面部分没有提到相关内容,导致让人一头雾水.抱怨下这本书,本来haskell的学习曲线就陡,书中的内容组织感觉把难度又提高了,好在在线版本附加了很多网友的comment,不可不看,都是重要心得及解释.
例子之前有提过haskell中返回值亦是函数.但没解释参数.实际上haskell的函数只有一个输入参数,对于多参数的情况,都会把其分解为curry functions.例如:
func a b c = a + b + c
可看作:
((func a) b) c = a + b + c
func a返回值为一个函数, 同样((func a) b)亦视为一个函数.
解释器中查看func的类型为: Num a => a -> a -> a -> a,可看为: Num a => a -> ( a -> ( a -> a)).由此看到func a的类型为输入参数a,返回一个类型为( a -> ( a -> a))的函数.
举个普通的例子:
max 4 5
=> 5
我们再看:
max4 = max 4
max4 5
=> 5
由max4 = max 4 即可知max 4返回一个函数.max4可视为一个curry function.
我们前面的myFoldl例子可以暂时改写为:
myFoldl f z xs = (foldr step id xs) z
where step x g a = g(f a x)
foldr的普通实现书中已给出为:
foldr f zero (x:xs) = f x (foldr f zero xs)
foldr _ zero [] = zero
我们尝试展开myFoldl (-) 0 [1,2,3]:
= (foldr step id [1,2,3]) 0
没有疑问,然后:
= (step1 1 (step2 2 (step3 3 id))) 0
首先step1 1 ... 到step2 2 ...这个递归调用foldr没有多大困扰.关键是在step3递归结束时有:step3 3 id.
而step函数在myFoldl中被声明为3个参数,但这里展开为只有2个参数.所以step 3 id显然是被currid了.即前面解释的curry function.此时返回step 2 ...看,它也只有2个参数即2和(step3 3 id),也是被curried,再看step1,前面解释过:func a b c = ((func a) b) c.在这里,step1是能够被编译器识别成功的,因为此时最后边的参数0被考虑进来了.实际上这不过是把前面的等式反着看:((func a) b) c = func a b c, 去掉括号会让我们清楚一些:
= step1 1 (step2 2 (step3 3 id)) 0
(step2 (step3 3 id))肯定是一个函数,当然我们并不知道他是不是符合满足类型的函数.谁管呢,惰性计算,到时再说.
(step1 1 (step2 2 (step3 3 id)))这是一个函数.仔细想想,它的类型为什么?它只需再匹配一个参数即可满足step的定义了.显然代码末尾的0满足了这一要求.step1的类型为a->a.
匹配成功,根据step的定义step x g a = g(f a x)得:
= (step2 2 (step3 3 id)) ((-) 0 1)
依次类推:
= (step3 3 id) ((-) ((-) 0 1) 2)
= id ((-) ((-) ((-) 0 1) 2) 3)
由递归顺序可以看到,(-)被myFoldl从列表[1,2,3]的左边开始应用,所得结果再应用到列表的下一位.同时这个结果并没有直接被计算,而是保存在chunk中.这正是foldl的行为.
通过curry,使原来foldr的计算顺序从右->左变为左->右.
函数流的走向本来是固定的,curry的应用使函数流的走向改变了.
我们将函数中的id替换为(-):
myFoldl f z xs = foldr step (-) xs z
where step x g a = g(f a x)
然后执行:myFoldl (-) 0 [1,2,3] 20,其结果为-26.由此可见id函数至少是最后被应用的.
从展开公式看,id实际上只被应用了最后一次且是唯一一次.中间的递归层次中,step调用的g,都是临时产生的curry函数.这个触动很大.函数能够动态生成,然后像值一样飞来飞去.
**********************************************************************
** 思路逆推 **
**********************************************************************
作者如何想到foldr实现foldl的思路的?foldr将step应用到列表中的每一个元素,从后往前.
foldr f zero (x:xs) = f x (foldr f zero xs)
= f x1 (f x2 (f x3 (...(f xn (foldr f zero []))...)))
若一切正常,则foldr从匹配 foldr _ zero [] = zero开始回推计算.我们要打破这个过程,改变计算的顺序.每一个递归过程都调用了f,f是个函数.考虑对f产生一些操作.f本身已经是我们的初始输入数据之一了,它不可改变.那么按照haskell的惯用思路,显然要把f放在一个函数中,增加一个中间层也是软件工程中的一个思路.然后foldr应用该函数,该函数再变换f和相应的参数(函数).
来分析展开式,可以看成:
f x1 (f x2 (f x3 (...(f xn (foldr f zero []))...)))
= f x1 (...)
f x1 (...)中x1的赋值是已经被计算的了,(...)仍待计算,整个f x1 (...)尚未被计算.我们现在要做的是x1与初始值必须先计算,(...)后计算.因此.我们需要产生一个变换,它将执行f(zero x1),结果再与(...)运算.此时要注意到: 1 (...)被看成一个函数. 2 (...)内部是一个嵌套结构,都可以转换为形如f xn (...)的形式.由此给我们带来的希望是,对f x1 (...)的变换将是连锁传递下去的.
我们需要什么样的变换?为方便,f x1 (...)写为f x g.缺少初始值zero, 添加之:(f x g) * a. a代表的是前一次计算的结果.开始时既是初始值.*号在此及以下只是表述某一种操作的意思,不是haskell的语法.最外层的t变换其初始值是从外部函数而非在foldr的嵌套展开中.
于是变换t形如:
t (f x g a) = f(x g) * a
我们考虑a作为初始值时传入了最外层t中.而g是一个嵌套的过程,显然g中自然也包含了t变换,g中的t变换接受的初始值为什么呢?稍加思考即之是f(a x).即是说我们要把当前的f(a x)结果作为参数传入g中.
于是变换t形如:
t (f x g a) = g * f(a x)
或许有人在想跟前个变换式子相比g为什么随意和a互换了?因为我们并不知道变换究竟是什么.那么肯定可以有一个变换达到这个结果的.就假定变换是这个即可.
g即(...),是一个函数.f(a x)表示当前计算的结果,很自然的,我们可以把f(a, x)当作g的一个参数.
t(f x g a) = g f(a x)
正如我们第一次把a当作整个(f x1 (...))的参数一样.这又是一个熟悉的嵌套模式.显然为了获得(...)内的值,我们必须先计算f(a x)的值,既是说f(zero x1)首先被计算.如此即保证了myFoldl的行为是从x1开始计算的.
t变换是作为myFoldl中的一个函数存在的,因此f对它来说是全局可见的.将f作为参数是多余的:
t(x g a) = g f(a x)
我们将t变换代替原来的f,代入:
t x1 (t x2 (t x3 (...(t xn (foldr t zero []))...))) a
最外层的t变换,其初始值为0.令a=0即可.
t x1 (t x2 (t x3 (...(t xn (foldr t zero []))...))) 0
根据前面t变换的推导,得
= (t x2 (t x3 (...(t xn (foldr t zero []))...))) (f 0 x1)
= (t x3 (...(t xn (foldr t zero []))...)) f((f 0 x1) x2)
= (foldr t zero []) f(f(...(f 0 x1) x2)...) xn)
同时必然会遇到递归终止条件的问题.foldr t 0 [] = 0这不是我们需要,不符合语法.我们需要foldr t zero []为一个函数.该函数输入后边的值,不做改变,仍输出该值.显然我们需要的就是id这个函数了.由此可知foldr的初始值为id.
= id f(f(...(f 0 x1) x2)...) xn)
= f(f(...(f 0 x1) x2)...) xn)
最后的式子,正是foldl的规则.我们实现了利用foldr实现foldl的要求.
于是我们可得,foldr所用的算子为:
step x g a = g (f a x)
foldr的初始值参数为:id函数.
myFoldl f z xs = (foldr step id xs) z
where step x g a = g(f a x)
这个例子确实不错,把握到函数编程的一些要点.短短2行代码,包含了这么多的隐含思路.这或许可以看成是一种心智包袱.c++的心智包袱有时像政治课本一样讨厌,或者像黑夜理乱麻一样让人望而生畏.haskell的更具单纯的数学美,一旦理解,赏心悦目.
------------------------------------------------------------------
一旦熟悉curry概念,更自然的推理感觉会是这样.
myfoldl f zero xs = foldr t g xs = t x1 (t x2 (t x3 (myfoldr t g []))) = t x1 (t x2 (t x3 g)) 如果t只有2个参数,那么: t x3 g将可直接计算. 于是t x2 (...)也被计算. 到最外围的t x1 (...)也被计算. t我们希望改变计算顺序的计划就泡汤了.因此必须让t x3 g被curry.所以t的参数个数要大于2个. 一旦参数个数大于2个: t x3 g被curry. t x2 (...)被curry. t x1 (...)是最外围了.此时它必须被计算.不然全部不可计算显然不符合语法也不符合我们的打算. 显然t x1 (...) zero是显而易见的.我们还漏了zero这个初始化参数呢.在myfoldl的计算中,要求先计算f(zero x1).那么可考虑做如下变换: t x1 ... zero = (...) (f zero x1) 即是说,t是这么个函数: t x g a = g (f a x). 因为g即(...)被curry了,它还缺少一个参数.所以我们把f a x即f zero x1作为参数传递给g即(...). 于是有:t x2 (...) (f zero x1) (...) f(f(zero x1) x2)
t x3 g f(f(zero x1) x2) g f(f(f(zero x1) x2) x3) 令g = id即可: f(f(f(zero x1) x2) x3) - (- (- 0 1) 2) 3) = 6.