SPL 看 Lambda 语法

Lambda 语法的主要目标是快捷方便地定义和使用临时函数,降低代码量,提高开发效率。SPL 专注于结构化数据计算领域,经常面对复杂的计算逻辑和冗长的计算代码,为了更适应具体的应用环境,SPL 对 Lambda 语法可以进行适当的改造。

循环变量

常规的 Lambda 语法没有内置的循环变量,而循环变量几乎哪里都要用到,程序员每次都要定义。

例 1,过滤出整数集合 A=[2,1,2] 中的偶数,常规的 Lambda 语法大概要写成:A.select(n -> n % 2 == 0)

上面的伪代码中,select 是用于过滤的高阶函数,其参数可以是一个 Lambda 表达式,即 Lambda 语法写的匿名函数。Lambda 表达式从结构上分为 2 部分,-> 是分隔符,-> 符号之前是函数参数的定义,之后是函数体。参数定义中的 n 就是循环变量,这表示在函数体中,可以直接用 n 引用 A 的当前成员。

SPL 在工程实现上追求代码尽量简短易懂,直接内置了循环成员变量名 ~,同时可以省去函数参数的定义部分,例 1 可以写作:A.select(~ % 2==0)

所有的 Lambda 表达式都用相同的循环变量名,有利于代码风格统一:

例 2,每个成员乘 3: A.(~ * 3)

例 3,分组: A.group(~)

步骤多些的计算,通常要多次使用循环变量,内置循环变量不仅降低了代码量,还因为去掉了 Lambda 表达式中的参数定义部分,使代码结构更加清晰易读:

例 4,前 100 个奇数的平方和:to(100).(~ * 2 - 1).sum(~ * ~)

循环计数

常规的 Lambda 语法没有内置的循环计数,而循环计数在 Lambda 表达式里很常见,这就要求程序员自行管理循环计数,代码比较繁琐。

例 5,过滤出整数集合 B=[1,4,4,4,5] 中偶数位置的成员,常规的 Lambda 语法大概要写成:

```

index=0

A.select((index=index+1, index %2 ==0))

```

上面的伪代码中,要在 Lambda 表达式之外定义循环计数,再在 Lambda 表达式内由程序员维护和使用循环计数,这就犯了变量作用域扩大化的毛病,还不如将 Lambda 表达式改为基本的 for 循环,虽然代码长一些,但变量作用域更小,代码容错更强。

SPL 同时为 Lambda 语法内置了循环计数 #,不仅作用域更小,而且代码更短。例 5 的代码可以写作:A.select(# % 2==0)

循环计数相关的计算通常有一定难度,内置循环变量可以简化此类计算:

例 6,对两个长度相同的序列 A=[3,8,2] 和 B=[2,0,4],计算出对位相加的序列,即 [3+2,8+0,2+4]。SPL 代码:A1.(~+B1(#))

例 7,对于序列 A=[3,4,5,6,7,8],计算出奇数号成员之和与偶数号成员之和的差,即 (3+5+7)-(4+6+8)。SPL 代码:A.sum(if(#%2==1,1,-1)*~)

例 8,序列 A 存储 12 个月的销售额,计算每个月的增长额是多少,也就是每个月份的销售额减上月销售额的差:A.(if(#>1,~-A(#-1),0))

相邻引用

相邻引用是指在表达式中引用与当前循环变量相邻的成员或集合。常规的 Lambda 语法没有相邻引用的语法,需要程序员自己硬写,代码比较繁琐,比如上面例 8 中用 A(#-1) 表示与当月间隔 -1 个月(上个月)的销售额。

SPL 扩展了 Lambda 语法,提供了相邻引用的语法,代码可以大幅简化。SPL 用 ~[t] 表示相邻引用,比如 ~[-1] 表示上个位置的成员或值,~[0] 等价于 ~。

例 9,用相邻引用语法改写例 8:A.(if(#>1,~-~[-1],0))

使用相邻引用后,相关的复杂计算都可以得到简化:

例 10,判断数列 A 是否递增:A.pselect(~!=int(~) || ~<=~[-1] )==null

例 11,序列 A 存储了 12 个月的销售额,计算三个月的移动平均值,1 月和 12 月只算两个月:A.(avg(~[-1],~,~[1]))

相邻引用也可以写成区间集合的形式,即 ~[begin:end],begin 省略时表示从头开始取,end 省略时表示一直取到结尾。比如 [-1:1] 表示上一个到下一个之间共 3 个成员的集合。本例可以简化成:A1.(~[-1:1].avg())

结构化数据

常规的 Lambda 语法没有针对结构化数据做特殊的简化,引用单数据表的字段时通常要附带结构化数据对象名或循环变量名(两者是一回事),代码比较繁琐。例 12,根据订单记录的集合计算总金额,常规的 Lambda 语法大概要写成:

Orders.sum(Orders.Price * Orders.Quantity) 或 Orders.sum(~.Price * ~.Quantity),

SPL 的 Lambda 语法对结构化数据这种绝对优势的数据类型做了特殊的简化。引用单表的字段时,SPL 允许省略表名,例 12 可直接写作:Orders.sum(Price * Quantity)

这种省略表名的语法源自 SQL,如 select sum(Price * Quantity) from Orders

直接引用单表的字段名,可以使 SPL 代码更加简短易读,这在较复杂的计算中尤为明显。例 13,根据销售表 sales,将 2014 年前 10% 的销售员再给予 5% 的业绩奖励。


A

B

1

=connect("db").query("select * from sales where year(OrderDate)=2014")

/ 连接数据源,读取销售表

2

=A1.groups(SellerId;sum(Amount):Amount)

/ 按销售员分组汇总当年销售总额

3

=A2.sort@z(Amount).to(A2.len()*0.1)

/ 按销售额降序排列,取前百分之十

4

=A3.run(Amount*=1.05)

/ 使用函数 A.run(),对前百分之十循环,每人给予销售额 5% 的奖励

对结构化数据也可以使用相邻引用的语法。例 14,某支股票的最大连续上涨天数。


A

B

1

=T("share_index.csv").sort(TDATE)

/ 读文件并排序

2

=A1.group@i(CLOSING<CLOSING[-1])

/ 上涨的记录分到同一组

3

=A2.max(~.count())

/ 取最大值

除了对函数式编程的 Lambda 语法进行扩展,SPL 也对对象式编程进行了适当的简化,其目的都是降低代码量提高开发效率,这里就不展开了。