饭后温柔

汉堡与老干妈同嚼 有可乐味
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

haskell笔记一:用foldr实现foldl

Posted on 2012-02-19 17:25  饭后温柔  阅读(2415)  评论(1编辑  收藏  举报

遇到一个费心的例子,想了很久,备忘之.

<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.