柯里化及相关延伸思考

#怎么理解

这篇文章的1.6.6 Currying 这一节。

#前提:编程语言得支持这个玩法

需要编程语言把函数当作 first-class,一等成员,即允许函数被:用参数传递给函数、被函数返回、被赋值给变量。这种函数内定义的函数,可以捕获它们被定义时(注意不是被调用时,记住这点就能弄明白)环境的变量定义,而且还能在被返回时保留这些定义(闭包)。于是我们可以用工厂模式一样的高级函数,输入一些配置参数,返回的是一个配置好的函数,然后我们再用这个参数去实际地用。

#柯里化

柯里化指,把接受多个参数的函数,变成一个连续的单个参数的函数。 这样会弄出来一堆零碎的中间函数,不过中间函数都隐藏在最高阶(柯里化后只接第一个参数,返回接受其他参数的函数)的函数的内部了,外部只能看到这第一个函数的定义,所以从外部看起来不是很凌乱。

有些语言(比如 Haskell)的函数,只支持单个参数,于是只能用柯里化这种方式来积累参数,实现多个参数的函数定义。

#例子:幂乘(power)

我直接用人家文章里的 python 代码吧。

1
2
3
4
def curried_pow(x):
    def h(y):
        return pow(x, y)
    return h

这是一个幂乘高阶(工厂)函数,你可以用这个函数生成计算特定幂乘数的函数(预先把第一个参数,即幂乘数,塞入生成后的函数的闭包):

1
2
square = curried_pow(2)
cube = curried_pow(3)

这样第一个参数就保留在这两个生成的函数的闭包里了,然后再拿着返回的函数去应用到实际的(业务逻辑)计算:

1
2
3
4
# curried_pow(2)(4) = 4^2 = 16
sixteen = square(4)
# curried_pow(3)(2) = 2^3 = 8
eight = cube(2)

#扩展思考

#参数的位置亦代表了构造步骤先后

你会发现,我们想用柯里化的时候,得把配置类(先固定好)的参数放在前面,计算变量类(后使用时填入)的参数放在后面,这样就能顺畅地先配置再执行,就像上面的例子。

那要是反过来呢,函数的意思会变:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def curried_pow(x):
    def h(y):
        # x,y颠倒顺序
        return pow(y, x)
    return h

# 对2做n次幂乘
pow_on_2_with = curried_pow(2)
# 对3做n次幂乘
pow_on_3_with = curried_pow(3)

# curried_pow(2)(4) = 2^4 = 16
sixteen = pow_on_2_with(4)
# curried_pow(3)(2) = 3^2 = 9
nine = pow_on_3_with(2)

#外部环境(闭包)在内部看来是不可变的

之所以有上面的配置先后顺序的考虑,是因为本小节标题所示的特性。变量有不同范围的生命周期,内部环境能看到外部环境里的定义,想修改定义所绑定的值可以重新覆盖定义,但这个新定义的范围仅在本地环境,没法向上影响到外部环境。

1
2
3
4
i = 0
def func():
    i = 1
print(i) # i还是0

我倒觉得把环境看成热塑塑料(或者更熟悉的502胶水)比较好,还在当前环境时,随便改定义捏形状,一旦退出环境或者深入到下一层环境(联想盗梦空间),环境固定死改不了了。比如函数一旦被构造出来,函数内的环境也就固定下来了。在调用函数的外部环境看来,函数内部环境直接一个黑盒,读都不能读;函数自己也不能修改硬编码的定义(比如上面i=1,不能下次调用让i定义为其他的值)。

#通过 Self Reference 来绕开这个限制

这节思路来自这道练习题。举个更简单的例子,我想构造一个 count_down()函数,每调用它一次,它会打印内部环境中保存的数字,并且返回一个保存数字减了一的函数:

1
2
3
4
5
6
7
count_down = count_down_on(3)
count_down = count_down()
3
count_down = count_down()
2
count_down = count_down()
1

结合上一节的限制,这有点难,因为看起来 count_down() 函数的环境得自己处理每次调用把变量值减一的操作。其实还是有办法的,两种办法:

  1. 把函数使用变量的方式改为从外部(环境)获取定义,有点依赖反转的意思(但其实就是提前构造了下一轮的新的函数环境):
1
2
3
4
5
6
def count_down_on(n):
    def count_down():
        print(n)
        return count_down_on(n - 1)

    return count_down
  1. 利用 Python 提供的 nonlocal 修饰符,允许在本地环境修改外部环境中的绑定(看官方文档里这个操作的起源和其他语言中的应用能多学到一些知识)。
1
2
3
4
5
6
7
8
def count_down_on(n):
    def count_down():
        nonlocal n
        print(n)
        n = n - 1 # 不加 nonlocal 声明,这句赋值会报编译错误
        return

    return count_down
updatedupdated2023-08-082023-08-08