Skip to content

Latest commit

 

History

History
814 lines (568 loc) · 23.6 KB

1.3FormulatingAbstractionswithHigher-OrderProcedures.md

File metadata and controls

814 lines (568 loc) · 23.6 KB

1.3 Formulating Abstractions with Higher-Order Procedures

原书目录

  • 1.3.1 Procedures as Arguments]()
  • 1.3.2 Constructing Procedures Using Lambda]()
  • 1.3.3 Procedures as General Methods]()
  • 1.3.4 Procedures as Returned Values]()

1.3.1Procedures as Arguments

我们学过了如何写表达式,如何组合表达式,并加以抽象,隐藏实现细节,重用procedure。

比如,计算两个数的和,我们可以:

  • 直接用 1 + 2

  • 定义一个procedure

define sum(a, b):
	return a + b
  • 执行procedure
sum(1, 2)

上面求的是两个数的和,那如果我们多求几个数,比如1到10:

  • 可以加个循环调用sum(a, b)

    def sum_loop(a, b):
        ''' use loop to calc sum of numbers between a and b'''
        total = 0
        for i in range(a, b+1):
            total += i
        return total
    

    需要思考一下,如果上面换成是求a 到 b所有数的平方和,上面的函数就得改写了

  • 但是作者用的递归实现,确实很直观。

    def sum_ints(a, b):
        '''# calcualte sum of numbers in between a and b'''
        if a > b:
            return 0
        else:
            return a + sum_ints(a+1, b)
    

    同样,如果要求平方和,就得另写一个函数:

    def sum_of_squares(a, b):
        '''# calcualte sum of the square of numbers in between a and b'''
        if a > b:
            return 0
        else:
            return square(a) + sum_of_squares(a+1, b)
    

    观察一下这两个函数,找一下共性:

    • 退出条件相同 (if a > b)
    • body相似:变数FX + 函数自身调用

    进一步思考,如果我们把共性的地方提出来,将变数X作为参数,是否就可以写成一个新函数:

    def newfunc(fx, a, b):
    	if a > b:
            return 0
        else:
            return fx(a) + sum_of_squares(a+1, b)
    

    事实上确实是可以的,python lisp都支持这个功能,即函数作为实参。

    def calc_2(fx, a, b):
        '''abstract from itself and sum_of_squares functions'''
        if a > b:
            return 0
        else:
            return fx(a) + calc_2(f, a+1, b)
    

    至此,我们看到,定义一个procedure,可以给它传入不同类型的参数:

    • numbers,
    • other expressions
    • 函数

    这有什么好处?以求和为例,同一个函数只要变换不同的参数fx,就可以实现不同类型的求和

    calc_2(square, 1, 10)
    
    calc_1(sum_ints, 1, 10)
    

本节求多个数的和作者依然用的递归来讲述,是因为讲述起来比较直观呢还是啥?【】

还有一个问题,其实数学上,求1到n的和是有具体公式的,比如

$$ s_1.._n = n(n+1)/2 $$ 求1到n的平方和: $$ s_1.._n^2 = n(n+1)(2n+1)/6 $$ 为什么作者不直接套用公式呢?应该是为了讲述原理吧。当n很大的时候,这个公式会很复杂,可能没有固定公式吧(不确定),老老实实的递推吧

数学的发现中,从推导求1到n的平方和,引出了递归,明白了为什么: $$ (n+1)^4 -1 = 4S_3 + 6 S_2 + 4S_1 + S_0 $$ 其中

$$S_3$$ :1到3的平方和

$$S_2$$: 1到2的平方和 ....

可见,只要知道了 $$S_2, S_1, S_0$$ 便能算出$$S_3$$的值了,而$$S_2$$计算需要先算出$$S_1$$,同理$$S_1$$需要知道$$S_0$$。而$$S_0$$容易算出,所以一路再倒推回去便算出$$S_3$$了。

1.3.2 Constructing Procedures Using Lambda

终于讲到lambda了,各种现代语言都在用lambda,源头在这儿。

据说命名是一门大学问,起不了个好名字还不如不起,但是计算机不认识啊,如果函数没名字的话。错!计算机会认识的,前提是你用lambda。如果你愁给函数起名字的话,那就考虑用lambda吧。(我开玩笑的,lambda也不是随便到处用的)

lambda的由来

Lambda - Wikipedia本是希腊字符,借用来在各个领域有不同的表示,比如下面这个小写的字符在数理逻辑和计算机中用来表示匿名函数。就是前面说的,没有名字的函数。

image-20180621221649084

举个例子:

在没有lambda的时候我们这样定义一个求平方的函数并调用:

def square(a):
    return a * a
    
square(2)

用lambda是这样的:

square = lambda a: a * a
square(2)

看起来代码简洁了一点不是?可能还不是很明显,这样,我们把它们放到一个函数里面再看:

def calc_sum_of_square(a, b):
    def square(a):
        return a * a
    if a > b:
        return 0
    else:
        return square(a) + calc_sum_of_square( a+1, b)
def calc_sum_of_square_l(a, b):
    square = lambda a: a * a
    if a > b:
        return 0
    else:
        return square(a) + calc_sum_of_square( a+1, b)

显然,在第二个中,没有了def的函数定义清爽了不少,看到的只是一个和其他行类似的表达式,只是有点特殊,一般称之为lambda表达式。虽然名为表达式,实则仍是函数类型,如果你试试跑这行代码:

print(lambda a: a * a)

你会看到返回结果是<function <lambda> at 0x104020378>,所以将lambda表达式赋给一个变量比如square后,便可以像调用普通函数一样调用square函数。

lambda 不需要return关键词,因为一般都比较短,最后一行语句即是返回值。

lambda创建局部变量

前面提过嵌套方法,比如下面的方法,嵌套了helper函数,接受两个参数a,b。

def f2(x, y):
    def helper(a, b):
        return x * square(a) + y * b + a * b
    return helper(1 + x * y, 1 - y)

有了lambda,我们可以去掉helper方法及其参数:

def f3(x, y):
     z = lambda a,b:  x * square(a) + y * b + a * b
     return z (1 + x * y, 1 - y)

上面这个Python实现还是引入了一个中间变量 z, 不如lisp来的干脆, lambda之后

(define (f x y)
( <lambda 表达式>
	<传入lambda参数值1>
	<传入lambda参数值2>
)
)

如:

(define (f x y)
  ((lambda (a b)
     (+ (* x (square a)) 
        (* y b) 
        (* a b)))
   (+ 1 (* x y))
   (- 1 y)))

貌似Python不支持这样的语法?

不过还是理解不了lisp,一个简单的计算公式,要搞个嵌套函数或lambda,有必要么?用python完全可以这样写啊,还简洁明了:

def f1(x, y):
    a = 1 + x * y
    b = 1 - y
    return x * square(a) + y * b + a * b

或者lisp这样:(google搜索scheme赋值语句let写成,但是代码暂未跑通)

(define (f x y)
(let (a (+ (1 x * y))) (b (- (1 y)))
 x * a * a + y * b + a * b
))
(f 1 2)

奇怪,回头看书中lisp代码,这个let不就是lambda转换成的么。

有意思,这样看来,python和lisp最终的实现代码至少从样式上是类似的,都定义了a , b 两个中间变量,分别给a 和b赋值,然后求出最终解。

但作者却是从lambda的讲解一步步引出到这个赋值语句let (in lisp。python其实是省略了赋值关键词)。原来赋值是这么来的(不知道我理解对不对,先这样理解了)。

记得在哪里看过有人说,后面某章作者教怎么写class的getter,setter。作者这样讲述一些习以为常的feature,印象深刻。

写到这里,以为lisp中设置变量值便是用let了。继续看书,发现还是可以用define的,比如

(define a (* x y))

但是作者不太建议这么用,而是建议用let。只在定义procedure的时候建议用define。不明所以。初步理解是,用let的话,像书中所说,解释器不需要再额外做工作。至于define需要解释器怎么做还不太清楚(是要先转换成lambda表达式么?)

No new mechanism is required in the interpreter in order to provide local variables. A let expression is simply syntactic sugar for the underlying lambda application.

let的作用域好像没啥,就是函数内局部变量的作用范围,仅限于函数内。

后续研究:

  • python decorator on class, method

  • 闭包

Python Decorator

先看官方定义:

A decorator is any callable Python object that is used to modify a function, method or class definition.

装饰器自身可以是任意可调用的python 对象。

装饰器的作用对象可以是function, method (in class), class。

对作用对象做什么操作呢?

  • 修改之,并返回新的替代之

    就是装扮一下再出门

function是可调用的python 对象,所以我们可以试着从function 入手,比如定义一个python function decorator, 用它来修改一个函数,并返回一个新函数。

在做之前,我们先根据现有知识来操作函数:

  • 定义一个hello函数,say hello to a name
def hello(name):
    print("hello ", name)
  • 定义一个log1函数,接受一个function参数,我们准备后面将hello函数代入
def log1(func, name):
    print(func(name))
  • 调用函数log: 将hello函数传进去,我们可以得到结果"hello me"

      log1(hello, "me")
    

函数还可以怎么写呢?

回想1.1编程要素中学过,功能分解后的子功能,如果是功能特有的子功能,可以嵌套到定义功能的函数中。

我们假设log函数有个子功能Internal方法, 这个方法会在hello的基础上增添点文字:

def log2(func, name):
    '''embedded functions inside log'''
    def internal():
        return "Result is:" + func(name)
    return internal()

运行一下,为什么结果是这样的?上面第四行返回error:

TypeError: must be str, not NoneType

这是在调用hello,hello(name)返回None。嗯,确实是,因为我们前面hello()函数只是打印,print方法没有返回值。修改一下hello函数

def hello(name):
    return f"hello {name}"

现在调用log2方法就ok了。

等等,为什么要加这个internal()方法,前面的log1方法完成可以实现同样功能啊。

log2同样接受两个参数,既然我们是在hello函数基础上(不改其自身)增补功能获得一个增强版hello函数,那能不能像hello函数一样,只接受一个参数hello,返回一个新的hello函数?

答案是可以的。

将name参数隐藏起来呗,这样:

def log3(func):
    '''embedded functions inside log'''
    def internal(name):
        return "Result is:" + func(name)
    return internal

注意到区别么,这样改写以后,我们返回的是internal函数,而不是Internal()函数值。

新改写的log3函数就是一个简单的decorator,它主要做的事情是:

  • 接受一个function参数 (hello)
  • 定义一个内部的wrapper function (internal)
  • 在参数hello的基础上增补功能
  • 返回那个内部的wrapper function (internal)

怎么用?

我们不再是简单的调用hello(name), 因为它满足不了目前的需求(比如log2里面的功能:返回result is hello(name)值。我们现在用Log3:将hello覆写

hello = log3(hello, "name")
hello("me again")

上面的设计有什么可以改进的地方吗:

  • log函数除了接受function参数外,另一个参数name hardcode成了该function特有的参数name,不够灵活,

    我们改一下:

    def log(func, *args,**kargs):
        print("func name is {} ", func.__name__)
        return func( *args, **kargs)
    

现在灵活多了,同样是调用log(hello, "me") 同样结果。但hello可以take任意参数。

还可以更灵活一点,俗称python语法糖: 前面我们不是通过hello = log3(hello, "name")重置了hello函数么,其实这一行可以省略成注解样式:@log3

@log3
def hello5(name):
    return f"hello: {name}"

print(hello5("5-----me again again"))

视觉效果好多了,虽然理解上没那么直观了。

参考:A guide to Python's function decorators

####【】python decorator 与 decorator pattern

看到Python decorator, 很容易想到Decorator pattern - Wikipedia(装饰器模式),按照官方说法,二者是不同的:

The Decorator Pattern is a pattern described in the Design Patterns Book. It is a way of apparently modifying an object's behavior, by enclosing it inside a decorating object with a similar interface. This is not to be confused with Python Decorators, which is a language feature for dynamically modifying a function or class.[7]

初步理解,python decorator是一个python特定功能,decorator pattern是设计模式,实现这个设计模式要用到OO,要在class level实现。而python decorator只需在module level实现。

闭包

Closure (computer programming) - Wikipedia

1.3.3 Procedures as General Methods

看标题似乎前面学过的抽象还不够,还有共性可以研究。

前面学过的两种:

  • 函数,接受普通变量

  • 函数,接受函数作为变量

    还能咋地?想了一下没头绪。

例1:Finding roots of equations by the half-interval method

这是作者举得第一个例子,没有太看懂。。。

二分法求方程f(x)=0的解?

假设 f(a) < 0 < f(b),则f在a和b之间至少有一个零解。

如果f( (a+b) /2 ) > 0 , 则 f在a和(a+b)/2 之间至少有一个零解。 — 没明白

如果f( (a+b) /2 ) < 0 , 则 f在b和(a+b)/2 之间至少有一个零解。 — 没明白

一直这样二分下去终得零解

虽然数学原理没明白,不过不妨碍写code。

真写起来还是有些吃力的,仿照lisp代码写一下吧:

def fx(f, a, b):
    avg = (a + b)/2
    if abs(a - b) < 0.001:
        return avg
    else:
        if f(avg) > 0:
            return fx(f, a, avg)
        elif f(avg) < 0:
            return fx(f, avg, b)
        else:
            return avg

def half_interval(f, a, b):
    a_value = f(a)
    b_value = f(b)
    if a_value < 0 and b_value > 0:
        return fx(f, a, b)
    elif b_value < 0 and a_value > 0:
        return fx(f, b, a)
    else:
        print(f"error, {a} and {b} are not of opossite sign")

函数fx定义了具体的算法(应用了数学原理求解)

函数half_interval()则以fx为参数,加入了更多实际项目的一些考虑,比如判断输入参数区间内是否有解,如无解报错等。

怎么用?求解方程 $$ f(x) = x^3 - 2x -3 = 0 $$ 这个方程式的表达便可以用lambda表达式了,看着挺清爽的:

formula = lambda x: x * x * x - 2 * x - 3
value = half_interval(formula, 1, 2)
print(value)

目前用到的还是之前学过的,重温了一下。

数学是硬伤。

例2:Finding fixed points of functions

求函数固定点(感觉我翻译的不对,印象中没有这个数学词汇),即求x,使得f(x) = x

依然是数学,不过根据原理的描述,回顾例1,还是比较容易写出这个的实现代码:

import math
def fixed_point(f, x):
    if abs(f(x)-x) < 0.0001:
        return x
    else:
        return fixed_point(f, f(x))

print(fixed_point(math.cos, 1.0))

这两个例子看着都比较眼熟,都是给定一个估测值,然后不断改进,使得结果满足某个good-enough值。前面在1.1.7中牛顿法求方根也是如此。这二者什么区别?

先把牛顿法求方根的递归代码搬过来:

def sqrt(x):
    def average_guess(x, y):
        return (x + y) / 2.0

    def improve_guess( guess):
        return average_guess(x / guess, guess)

    def good_enough(y):
        v = abs(square(y) - x)
        return (v < 0.001)

    def square_iter(guess):        
        if good_enough(guess):
            print("----", guess, good_enough)
            return guess
        else:
            guess = improve_guess(guess)
            print("++++", guess)
            return square_iter(guess)
    return square_iter(1.0)

这代码有点长哦。

事实上,sqrt()可以利用前面的fixed_point()方法重写:

def sqrt(x):
    return fixed_point(lambda y: x/y, 1.0)

从那么一长串代码到这么短的调用fixed_point()即实现同样的功能:求方根,怎么来的?

fixed_point:

f(x) = x

方根: $$ y^2 = x $$ 类似于 $$ y = x / y $$ 和fixed_point类比一下: $$ f(y) = x / y $$ 具体怎么推理到 $$ y = 1/2 * (y + x/y) $$ 以后再补了,【average damping】

average damping: 渐进值平均值

为什么前面我们用到的算法叫fixed point, 固定点?

  • 首先给定一个初始估测值
  • 根据一定的规则(good enough value), 不断调整这个估测值
  • -最终结果是趋向于一个固定值,即题解

是否所有的应用了fixed point方法的都能最终得到一个固定值呢?我不确定,但正如书中求方根的例子所讲,如果选不好估测值(guess)的话是很有可能的。比如

方根: $$ y^2 = x $$ 类似于 $$ y = x / y $$ 我们这么来估测:

  1. 初始估测值 y1
  2. 下一个估测值 y2 = x/y1
  3. 再下一个估测值 y3 = x/y2 = x/(x/y1), 结果 y3=y1, 又回到初始估测值,死循环了。

这说明我们的初始估测值可能没有选好。书中给出的原因是,估测值之间的变化太快了,可以缩小范围。

如何做?

先思考一下,y = x/y, 最终的 y 值的可能区间是什么? $$ y = x / y $$

$$ y + y = x/y + y $$

$$ 2y = x/y + y $$

$$ y = (x/y + y) /2 $$

从以上推导可以看出,y 的最终值趋向于 (x/y + y) / 2, 即 y 和 x/y 的均值, y 值总是介于 y 和 x/y 之间。

这样,我们便可以这样来估测:

  1. 初始估测值 y1

  2. 下一个估测值 y2 = (x/y1 + y1)/2

  3. 再下一个估测值 y3 = (x/y2 +y2)/2 = (x + 1/4*(x/y1+y1)^2) / (x/y1+y1)

    不再死循环了

现在的fixed point方法变成了 y = (x + x/y) / 2, 而不是 y = x/y了。

sqrt()方法可以重新改写一下:

def average(x, y):
    return (x + y) / 2

def sqrt(x):
    return fixed_point(lambda y: average(y, x/y), x)

上述过程用到的技巧就是average damping(渐进平均值),对题解的估测值不断求平均,慢慢逼近最终解。

参考:

algorithm - Why does average damping magically speed up the convergence of fixed-point calculators? - Stack Overflow

感觉这一节并不是更进一步的抽象,只是通过具体实例进一步解释了procedure作为procesure的参数的应用。

1.3.4 Procedures as Returned Values

这一节应该讲的是procedure作为返回值。 首先回顾一下前面学到的averge damping:

对于函数f(x), 认为 f(x)在x点的值 = (x + f(x)) / 2

假设 $$ f(x) = x^2 $$ 当x =10时, $$ f(10) = (10 + f(10)) /2 = 55 $$ 似乎有点不好理解。明明f(10) = 100, 为什么是55?

其实这里的55只是一个初始估测值,就是求解10*10的时候,按照fixed point算法,相当于是求解另一个函数f(y) = (y+f(y))/2的固定点。用代码表示如下:

def average_damping(f):
    return lambda x: (x + f(x)) / 2

返回值是一个由lambda产生的函数,该函数接受一个参数x, 返回一个平均值。

上一节求方根,我们用fixed point方法实现的代码如下:

def average(x, y):
    return (x + y) / 2

def sqrt(x):
    return fixed_point(lambda y: average(y, x/y), x)

注意到fixed_point函数内的lambda参数,其返回值是一个平均值,很像我们前面的average_damping函数返回的lambda函数,于是可以改写一下:

def sqrt(x):
    return fixed_point(
    	average_damping(
    		lambda y: x/y ), 1.0)

和1.1.7的求方根代码比较一下:

def sqrt(x):
    def average_guess(x, y):
        return (x + y) / 2.0

    def improve_guess( guess):
        return average_guess(x / guess, guess)

    def good_enough(y):
        v = abs(square(y) - x)
        return (v < 0.001)

    def square_iter(guess):        
        if good_enough(guess):
            print("----", guess, good_enough)
            return guess
        else:
            guess = improve_guess(guess)
            print("++++", guess)
            return square_iter(guess)
    return square_iter(1.0)

显然前者更抽象,而且很清晰的体现了应用的算法思想:

  • fixed point
  • average damping
  • lambda function

更重要一点,前者的复用(reuse)更方便,作者以求立方根为例,只需简单的更改,便可以实现:

def cube_root(x):
    return fixed_point(
    	average_damping(
    		lambda y: x/(y*y) ), 1.0)

一点启发:

以前理解的复用(reuse)很窄,局限于像定义一个procedure, 然后可以在其他procedures中调用这个procedure,便是复用了,比如上面的average_damping方法即可用于sqrt()方法,也可以用于cube_root()方法。

其实还有更高层次的复用,即设计的这个procedure本身的逻辑是否可以复用于其他类似待解决的问题,比如我实现了sqrt()函数,那求立方根函数是否可以借用sqrt()函数的逻辑快速实现呢?

实现更高层次的复用,需要对程序更进一步抽象,但也不是越抽象越好,it depends

  • 没那个进一步抽象的能力的话就别想了,
    • 当然可以继续培养
  • 实际应用的场景
    • 可能没必要,
    • 可能没时间,
    • 。。。

但无论哪种情况,不要忘记,习惯思考:是否还可以进一步抽象成更通用的模型?

这一点还蛮重要的,我们常常容易过早认知闭合,无论是急于交工还是急于炫耀。留一点时间深入思考,这是我非常欠缺的。

作者后面讲的重写牛顿法求方根,还没有很理解具体推理。但主旨已经get到,就先放过了。放一下那两段用来比较的漂亮的lisp代码,一目了然:

(define (sqrt x)
  (fixed-point-of-transform 
   (lambda (y) (/ x y))
   average-damp
   1.0))
(define (sqrt x)
  (fixed-point-of-transform 
   (lambda (y) (- (square y) x))
   newton-transform
   1.0))

lisp除了括号有点烦,代码还是挺悦目的。

本节的最后,也是本章的最后,作者呼应了开篇的编程要素,对于其中的procedure,给出了一个first-class elements说法,有点像头等舱客人,有那么一些“特权”,比如

They may be named by variables.
They may be passed as arguments to procedures.
They may be returned as the results of procedures.
They may be included in data structures.65

1-3 都是本章讲过的内容,这些procedure可以作为变量、作为函数参数、作为函数结果返回。而第4条procedure作为数据来用,将在下一章学习,期待。这第4条也是Lisp区别于其他高级语言的一个特性。

想起一个采访,好像是python的创始者在一次演讲中,lisp语言的创始人在听其演讲,很朴素的问了一个问题“ python中的function可以当做数据用吗?”,答案是否。忘记出处了。对函数做数据用这一点印象深刻,期待接下来的学习。

学习第二章之前重温一下《lisp的本质》,之前是看了这个对lisp有了兴趣并决定看sicp的时候也学习lisp,不过第一章学下来因为lisp能实现的功能python也可以,本着实用主义的思想,书中的代码都用python实现了,lisp没碰多少。下一章也许需要多写lisp代码了吧。

defmacro - The Nature of Lisp

Lisp的本质

本章后面很多练习没有做,看得太慢,练习没时间做,以后慢慢补。