本帖最后由 mxymxy 于 2013-12-28 16:54 编辑
过程抽象(上)
这里兑现之前的承诺,把过程抽象简单讲了,大神勿喷……
“程序设计一个很重要的地方就是控制复杂度,而过程是一种黑盒封装的形式,某种程度上……” “说人话!” 好好好,简要地说,就是说它是程序变得简单了。这个谁都知道的。我们定义过程之后,就可以在使用时不用关心它的实现,而仅仅关注于它的结果。Ruby的过程抽象也是一样,但是它和一般的过程其实是有区别的,因为原则上任何能进行相同计算的东西都可以用同一个过程处理。假设我们写了一个用乘法来计算数字乘方得函数,而字符串如果也能计算乘法,那么我们就可以直接用这个函数来计算字符串的乘方,而不需要重新定义其他过程。 我们通过使用过程(不论是存在的还是假想的),就将问题一步步地分解成子问题,不断简化它们,直到问题得以解决。比如说,我们要计算一个函数的平方根,于是我们就是求y^2=x的y。我们先有一个猜测值,我们评价这个猜测值,不够好的话我们就不断改进这个猜测。于是我们大致能写出下面的程序: - def square-root(x)
- guess=1.0
- score=evaluate(...)
- until good-enuf?( ...)
- guess=improve(...)
- end
- return guess
- end
复制代码然后我们再分别实现evaluate(可以是计算x^2和guess的差值,也可以是计算improve对guess的改进程度),good-enuf?(是精确到绝对小数位或者是相对位数),和improve(可以用二分法或者是牛顿法),我们就写出了这个程序。当然我们也许不会吧这三个过程单独提出来,而是会写到一起去,但是我们至少在心里清楚,它们是独立的过程块,满足如下依存关系: square-root
↓
evaluate,good-enuf?,improve
↓
......
再比如说,我们要计算一个线段围成的复杂几何图形的面积,我们可以先把它分成多边形,然后计算这些多边形的面积;计算多边形的面积时我们又需要用到向量的叉积;…… 程序,不论多大多小,其结构都可以像这样被断成一个个较小的、易于控制的块,这些块又进一步分段成更小的块;而且,当我们要重复利用某一个块的时候,我们不需要重写代码;当某一个块出错的时候,我们也只需要修改一处即可。这就是过程抽象的直接价值。
二、λ对象和它的环境 假设我们要对数组里的所有元素求和,我们可能会写出这样的函数: - def arr_sum(ary)
- i = 0
- sum=0
- while i < ary.length
- sum+=ary[i]
- end
- return sum
- end
复制代码现在我们又需要计算平方和,立方和……我们需要写很多类似的代码,他们的差别仅仅在于对sum的计算,于是我们考虑,应该写出一个通用函数,用我们来告知其我们要计算的是平方还是立方。于是有了这样的代码: - # 注意这段代码是执行不了的,因为sqr被定义为object的私有方法的缘故
- def sqr(x)
- return x*x
- end
- def arr_do(ary, p)
- i=0
- sum=0
- while i<ary.length
- sum+=p.call(ary[i])
- i+=1
- end
- return sum
- end
- p arr_do([1,2,3],:sqr.to_proc) #思路可行,语法无效
复制代码如果仅仅是为了临时调用,名字也可以省了,就成了这样: - def arr_do(ary, p)
- i=0
- sum=0
- while i<ary.length
- sum+=p.(ary[i])
- i+=1
- end
- return sum
- end
- p arr_do([1,2,3],->x{x*x}) # =>14
复制代码这样,arr_do就是一个接受过程参数的一般性的方法。 有时候,我们需要计算数组中每个元素除数字y的值,y运行时刻确定。于是: - def y_over_x(y)
- return ->x{y/x}
- end
- def arr_do(ary, p)
- i=0
- sum=0
- while i<ary.length
- sum+=p.(ary[i])
- i+=1
- end
- return sum
- end
- p arr_do([1,2,3],y_over_x(6)) #=>11
- x_d_54= y_over_x(54)
- p arr_do([1,2,3], x_d_54) #=>99
复制代码于是我们发现,一个过程可以: 1. 用变量命名 2. 提供给过程作为参数 3. 成为返回值 4. 包括在数据结构里 因此过程是一个对象,和数字、字符什么的没有任何区别的对象。事实上它不过是一段二进制存储的代码而已,和数据存储于同一区域。那么我们就得到了方法的对象,也就是所谓的里的λ对象。我们将看到,通过这一对象,我们将走入Ruby编程的第三范式——(不完全的)函数式模型。 最好用的定义(也有其他写法,但不方便): ->(参数列表;局部变量列表){语句列表} 圆括号是可选的,但是最好加上;{}之前必须紧凑书写,多余的空格不要加,不然即使符合松本先生的语法定义,在RMVA里也会报错。 例子: - fac = ->(x,y,factor=2;ret){
- ret = [x * factor, y * factor]
- return ret
- }
- p fac.(1, 2) #=>[2,4]
- #注意fac.是fac.call的简写
- p fac.(1, 2, 3) #=>[3,6]
复制代码如果你想把一个lambda对象传递给需要代码块的函数,请用&修饰它,不然就会被当做普通参数传递而报错: - p ([1,3,2,6,5].sort &->(a,b){b-a})
- #=>[6,5,3,2,1]
复制代码然后,我们还缺少什么呢?外部变量。比如说y_over_x,它的返回值理所当然地需要能调用y,不然整个架构就会变得毫无意义,这就是所谓的“闭包”。可是,注意到我们在调用它返回的λ时,y_over_x的生命周期已经结束了,我们如何才能正确取得y的值呢? 松本先生告诉我们:“闭包并不持有它需要变量的值,而是确实动态地持有变量本身。闭包延长了局部变量的生命周期。”(这和C#,C++都是不完全一样的,请务必不要混淆) 也就是下面的例子: - access_pair = ->(intval){
- value = intval
- getter = ->(){ value }
- setter = ->(newval){ value = newval }
- return getter, setter
- }
- gX, sX = access_pair.(10)
- gY, sY = access_pair.(5)
- p gX.() #=>10
- p gY.() #=>5
- sY.(8)
- p gY.() #=>8
- p gX.() #=>10
复制代码看到了吗?gX和sX将共享同一个access_pair中的value,而gY和sY将共享另一个access_pair中的value,这是λ闭包包括了创建者的(变量)环境。 而当我们不希望λ干扰创建者的(变量)环境时,我们就要这么写: 这里value是真正的λ的局部变量了,它只属于λ的环境,不属于创建者的环境。 于是,我们将形如->(V){E}式子中的V包含的变量(不论是参数还是局部变量),称为是“被绑定到λ上的”,不包含在V中的变量称为是“自由出现的”。脱离了->(V)的约束的{E},里面的V也同样叫做是自由出现的。自由出现的变量一般是定义它的地方的参数或局部变量。于是被绑定的变量和自由出现的变量共同构成了λ的(变量)环境。
二、λ对象的性质 在Ruby中,我们不能判断两个λ是不是相等的,只能判断他们中的一个是不是另一个dup出来的。否则,即使完全一样,也不是相等的: - ->(x){x}==->(x){x} #=>false
复制代码所以在Ruby中比较λ是不适当的行为。 但是这并不妨碍我们在逻辑上认为这两个λ是等价的。以下我们使用“=”来表示式子两边在逻辑上是等价的,尽管编程中无法区别。 为了研究λ的性质,我们再引入这样一个假想的函数repl,我们要求repl(expr,V,W)的结果是把式子expr中所有V的自由出现变换成W的自由出现,即: - repl(x,x,y)=y
- repl(return [x,z,->(x){x}];,[x,z],[y,w])=return[y,w,->(x){x}];
复制代码然后我们可以得出以下三条规则:
1.alpha变换:->(V){E}=->(W){repl(E,V,W)} 这是很好理解的,名字不过是个标记,对参数和局部变量统一换名当然不会对代码块的工作产生任何影响。通过这一规则,我们认为->(x){x}和->(y){y}就是等价函数。
2.beta消解或beta规约:令F=->(V){E},则F.(W)=repl(E,V,W) 这条规则描述了λ是如何起到“函数”的作用的,就是通过实际参量替换形式参量,然后代入计算求值。同时这和alpha变换也是一脉相承的,因为不管你用什么形式上的名字,一规约全换掉了,也就没有差别。这一规则在逻辑推导中常常被用来化简表达式。
3.eta等价:对两个表达式E和E’,如果对所有的X有(->(V){E}).(X)=(->(V){E’}).(X),那么E=E’,反之亦然 这条规则表明,所有输入相同参数得到相同返回值的函数(λ)是逻辑上相同的函数(λ)。
好了剩下的且看下节分解。
|