【程序设计】5.7 [一把抓] Lambda 语法 *

 

5.7 Lambda 语法*

回顾一下 A.pos(x) 和 A.select(x) 这两个函数,我们说前者不是循环函数,参数 x 中不能用 ~、#这些符号,而后者是循环函数,参数中可以用 ~、# 这些符号了。其实,这两个函数的计算过程中都会对 A 遍历,也就是一个个处理 A 的成员,这两者的参数到底有什么不同呢?

在引入循环函数之前,所有的函数 f(x) 都有一个特点,它的参数 x 如果是个计算表达式,那么会在计算这个函数 f 之前就被计算好。比如 max(3+5,2+1),计算机在计算 max 函数时,拿到的参数已经是 8 和 3 了,即使这个参数中可能含有变量也是这样,max(x+5,y+3) 这样的计算表达式在 x 和 y 没有被赋值是无法计算的,而 x 和 y 被赋值后也会把 x+5 和 y+3 先计算出来再传入给 max 函数去计算。

这没毛病,我们从数学课上学到的函数知识就是这样的,A.pos(x) 的参数也是这样的。

但是,A.select(x) 不一样,这个 x 在计算 select 函数之前很可能是算不出来的,它实际上是在计算函数的过程中才能被计算。比如 A.select(~%2==0),我们没办法在计算这个函数之前就把 ~%2==0 算出来,这里有个未知的 ~,必须要在循环 A 的过程才能确定,然后才能计算出这个表达式。

针对序列的函数,其参数是个待计算的表达式,要在循环序列的过程中才能计算,有这种特点的函数,我们才称之为循环函数。所以,A.pos(x)、A.pseg(x) 不是循环函数,而 A.(x)、A.run(x)、A.select(x)、A.max(x) 则是循环函数。

对于非循环函数,即使是把参数写成表达式的样子,但传入函数时也会先被计算成具体的数值。

对于循环函数,传入函数的参数就是表达式,是个含有某些如 ~、# 等未知变量的表达式。这个表达式事实上可以看成以这些未知变量为参数的函数。也就是说,循环函数的参数,本质上是另一个函数,而不是个简单数值。

这是循环函数与非循环函数的本质差别,它以函数为参数。

最早期的程序语言不支持函数作为参数,人们当时还缺乏经验,只会简单延用数学上的函数经验(其实数学家们早就开始搞泛函分析、研究函数的函数了,但毕竟相对深奥)。后来为了更方便的代码复用,人们引入了函数指针的概念,把函数作为参数传递给另一个函数,使前者可以被后者调用。但是,还是要先用一段代码明确地定义出这个被调用的函数,代码书复杂度和理解难度都不低。

比如我们举例中的 A.select(~%2==0),要传统语法就要分两步,先定义一个函数 f(x) 为 x%2==0,再用 A.select(f) 来计算。select 函数在循环计算时,再把 A 的当前成员作为参数传入函数 f,计算后获得返回值,决定是不是要把这个成员加入到 select 的返回值中。

其实,很多要用作参数的函数,其计算逻辑并不复杂,简单用表达式就可以描述清楚了,比如这个 x%2==0 就很简单,为这么一件事还要再先定义一个 f 函数,实在太麻烦了。

于是,人们进一步发明了直接用表达式来定义一个函数的编程语法,不需要事先定义这个 f 函数,而直接把它写到 select 函数的参数中,比如可以这样:

A.select( f(x):{x%2==0} )

这种在参数中临时定义函数的写法,业界有个术语叫做lambda 语法

有了 lambda 语法,代码书写就简单多了,现在许多程序语言也都开始支持这种语法。

还有问题。

这种被传入的函数必须是个有参数的函数,用于引用宿主函数(那个要把它传入进去的函数,这个例子中的 select)计算过程中产生的一些变量(这里就是 A 的成员),否则要是没参数就直接在调用宿主函数之前算完好了。

所以,上面那个 f(x):{x%2==0} 的语法中,必须要有个指明参数的办法,要用 f(x) 中的 x 告诉计算机,x%2==0 中的 x 是这个作为参数的函数的参数(有点绕),要等着宿主函数往里传值。如果只写 x%2==0,你不知道这个 x 是什么东西,是不是别的什么地方的变量?

还是有点麻烦,而且理解起来有点晕,但这是大部分支持 lambda 语法的程序语言的现状。

还没完。

select 这种宿主函数中还可能用到 A 成员的序号(即 #), 这个被传的函数 f 只有一个参数是不够的,因为 select 函数要接受各种各样的作为参数的函数,这些函数的参数还必须是统一的,否则 select 函数是没法调用它们的。

所以这种函数起码要有两个参数,这个语法就得写成:

A.select( f(x,y):{x%2==0} )

我们这个例子没有用到为序号(也就是 #)准备的 y 参数,但却不能不在定义式中写上。

然而,我们知道,select 还可能用到 ~[-1] 之类的东西,那还要传入多少东西?这些都需要事先定义成 f 的参数,这太麻烦了,干脆把整个 A 传入算了,再把当前序号传进来,反正其实都能算出来了。最后写出来可能是这样:

A.select( f(x,y):{x(y)%2==0} )

参数 x 表示宿主函数要计算的序列,y 表示宿主函数正在计算的序列成员序号,剩下都由自己在这个定义里写出来。

但是,这样又变麻烦了。

其实,大部分支持 lambda 语法的程序语言并没有走到这一步,只支持到 f(x):{x%2==0} 这个程度,也就是说,没办法在表达式中引用序号以及做相邻引用了。

那么,我们怎样让代码写起来又简单,又能有很强的描述能力呢?

就是 SPL 现在的办法。

SPL 把这个事情简化了,固定用了 ~ 和 #以及 [] 这些符号来表示这个作为参数的函数的参数,于是,写成 ~%2==0 就可以了,不需要再先写个 f(x)来约定参数名称。

这样,书写上和理解上都简单了,而且,因为这些丰富的符号,描述能力也变强了。

【程序设计】 前言及目录

【程序设计】5.6 [一把抓] 排序相关

【程序设计】6.1 [重复用] 自定义函数