当我们使用变量记录运算过程中用到的数据时,就引入了边际效应的风险,举一个简单例子:
bData as [1,2,3,4,5,6,7]; ## do something for i in range(bData.len()-1,-1,-1): if bData[i] > 3: bData.pop(i); end; end; ## do something print(bData);
这段代码用于把bData列表中数据筛选一遍,把不符合条件的数据(i > 3)删掉,然后打印结果数据。
看上去很简单,不容易出错,但如果程序写复杂,代码量增加了,比如上面代码中用“do something”注释的地方,插入大量代码,bData变量可能不经意被修改,有时修改不在本处,可能用参数传递到其它函数被修改了,结果print值并非预期,这种情况就是边际效应(side-effects)。
实现同样过滤功能,我们可改用如下方式编写无副作用的代码:
bData as lambda: vCond,bInput, filter(vCond,bInput); end; print(bData(lambda: i, i <= 3 end,[1,2,3,4,5,6,7]));
这里,bData同样是变量,但它描述一种运算,即:利用FP的惰性求值的特性,保障特定数据免遭意外被改。
Bindings捆绑
Binding不只是FP语言的一个特色,也是脚本语言的通用特色。在脚本语言中,一个变量总是以“实体”的形式存在的,C/C++不是这样,比如:
int i = 5; i = 5.5f;
这里变量i始终是int类型,尽管第二条语句将float值赋给它,系统把值转化为int再赋过去。相应的,脚本语言处理此类语句更向改换链接(变量名只是一个链接标识),比如在Python中:
i = 5 i = 5.5
第一条语句执行后,i是整形变量,而第二条语句执行后i变成浮点变量了。我们可以把变量i看作一种标识,该标识先与值为5的整数实体捆绑,然后当第二语句执行后,改成与值为5.5的浮点数实体捆绑。
这就是强类型与弱类型语言的差别。我们看一下CSE语言如何处理的:
i as TInt = 5; i = 5.5f;
CSE解释器也像C/C++那样,执行第二条语句时把浮点数转成int类型再赋过去,这种设计迎合了“用脚本仿真C/C++”的需求,与常见脚本语言处理风格存在很大差异。
接下来,我们要用CSE的Interface风格的类定义模拟Binding特性,因为CSE的class类也缺省去仿真C++的class了,Interface风格类提供一种自定义操作的机制,CSE已用它模拟Python脚本接口,请参阅CSE在线帮助《PyLib参考手册》。
我们先定义一个Binding类,如下:
class Binding: end as AInterface; class Binding: declare #dict as TEntryArray*; func Binding(me): ppDict as TEntryArray** = &me asTEntryArray**; *ppDict = new TEntryArray(); end; func `~Binding`(me): ppDict as TEntryArray** = &me asTEntryArray**; delete *ppDict; *ppDict = NULL; end; func `#=`: ## dir() declare(me); return me.#dict->keys(); end; func `#1:`: ## get attribute declare(me,attr); if "#dict" == attr: return *(&me as TEntryArray**); end else: pDict as *(&me asTEntryArray**); ret as pDict->get(attr.toId()); if CseNull == ret: throw(EAttrError,"attribute(%s) inexistent" % [attr]); end else return ret; end; end; func `#1;`: ## set attribute declare(me,attr,value); if "#dict" == attr: throw(EAttrError,"attribute(#dict) is readonly"); end else: pDict as *(&me asTEntryArray**); pDict->set(attr.toId(),value); return value; end; end; end;
这个Binding类重新定义存取类成员的方式,都定向到对#dict成员读写,#dict是字典类型,众多“key-value”成对保存在该变量中。然后,我们运行如下代码:
ASet as Binding(); ASet.i = 5; ASet.i = 5.5f;
前一条对ASet.i赋值,i变量是int整形,后一条对ASet.i赋值,i变量将变为浮点值,即:通过Binding类,我们让CSE也缺省提供弱类型赋值。
删除ASet.i变量:
ASet.i = CseNull;
查看ASet下都定义了哪些实体:
dir(ASet);
再验证一下函数实体是否也支持:
ASet.test = lambda: i1,i2, i1 + i2; end; ASet.test(3,5);
ASet.test(3,5)调用结果肯定是8。
Closures闭包
Closure是一种将函数与它依赖的运行环境一起打包的技术,它最初是在20世纪60年代作为Scheme组成部分开发的,目前JavaScript、Python、Ruby、PHP(V5.3以后版本)都支持闭包。
闭包的价值通常建立在lambda匿名函数之上,有了闭包,不仅函数自身作为一种数据可以被传递,被记录,运行该函数的上下文环境也被保存,这让经过一次或多次传递后的函数仍能正常调用。
比如,我们有如下代码:
iPeriod as TInt = 7; func nextPeriodFunc() as TCseObj: return lambda: i, iPeriod + i end; end;
一周是7天,全局变量iPeriod值为7,已知本周一是2号,求下周一是多少号?
nextWeek as nextPeriodFunc(); nextWeek(2);
这种编程风格容易产生边际效应,全局变量不小心被改将导致nextWeek运算出错,改用闭包形式:
func nextPeriodFunc(iDay) as TCseObj: iPeriod as TInt = iDay; return Closure(lambda: i, iPeriod + i end); end; nextWeek as nextPeriodFunc(7); nextWeek(2);
把iPeriod变量改作局部变量,然后使用闭包,把“lambda: i, iPeriod+ i end”函数定义与它运行的环境(含iPeriod)打包。最后调用nextWeek(2),您将得到结果值:9。
CSE已提供将函数环境打包(#packSpace)及闭包调用(#closureCall)的功能,我们用如下代码封装一个Closure类就可以了:
需要提醒一下,闭包特性延伸了函数内局部变量的生存周期,如上面举例iPeriod局部变量并不随nextPeriodFunc(iDay)函数调用结束而释放,如果这个nextPeriodFunc还定义另一个占用大量内存的变量,结果是什么?它占用的内存将一直占着,直到闭包实体被释放(如上面例子中nextWeek变量被删除)。class Closure: declare space as TCseObj; declare fun as TCseObj; func Closure(me,AFunc,Space =#packSpace()): me.fun = AFunc; me.space = Space; end; func `operator()`: declare(me,#_); return #closureCall(me.fun,#_,me.space); end; end;
CSE与函数式编程
CSE语言具备完整支持函数式编程的能力,但不意味着它现在就是一门函数式编程语言,本文介绍了CSE在FP编程通用特性上的实现情况,但CSE官方并未正式宣称CSE支持FP编程(其实正式宣称也未尝不可,业界大部分支持FP的语言并非纯粹只用函数式表达风格)。
CSE用户手册未正式推荐FP特性,主要因为当前状况下,还不存在必须用FP才能解决的应用,FP的表述风格终归不如命令式那么好学、易用、也更人性。或许,CSE今后向多核并行方向发展时,FP编程才正式推荐给大家使用。
本系列文章(用CSE模拟LISP语言)用到的代码已整理到functional.cse文件,请点击此处下载。
(完)
相关文章:
作者自述CSE语言设计思想(三)----用CSE模拟LISP语言(上)
作者自述CSE语言设计思想(四)----用CSE模拟LISP语言(中)