作者自述CSE语言设计思想(三)----用CSE模拟LISP语言(上)

在接下来几篇博客中,我将通过用CSE脚本模拟LISP语言,来介绍CSE的函数式编程(Functional programming,FP)特性。

 

什么是函数式编程

从一个问题开始,到底什么是函数式编程?有人说像Lisp、Haskell、ErLang那样的编程就是函数式编程,当然,这是保守的回答,没有错,但也没说明问题实质。

Wikipedia对函数式编程的定义是:

Functional programming is a programming paradigm thattreats computation as the evaluation of mathematical functions and avoids stateand mutable data. It emphasizes the application of functions, in contrast tothe imperative programming style, which emphasizes changes in state.

函数式编程是一种编程范式,在这种编程方式中,我们更多的使用函数运算。函数式运算的特点是没有状态和可变量,一个函数有输入值和输出值,函数运算不会产生任何副作用(side effect)。与之相对的是Imperative Programming,就是我们通常说的命令式编程。

我举一个例子:

double oneThird =0.3333;
printf("I should get one third: %f\n",oneThrid * total);

三分之一是分成比例,该比例乘以总数是你该得份量,当我们用“oneThrid * total”表示时,引入了两个潜在风险(或称BUG),其一,oneThird是变量,当你前后做两次“oneThrid* total”计算时,如果oneThird变量被修改了,其它线程可能意外修改它,但你不知道,以为它没发生变化,这导致后一次计算值与前一次不一样,这就是边际效应。另一个风险是oneThird的来源,无论你怎么预先定义这个“分成比例”都是不准确的,上面我们预置0.3333,如果分1万个苹果,3人各拿0.3333*10000 = 3333个,结果多出1个,如果觉得0.3333不够精确,小数点后再添一个3,但分10万个苹果,同样也会多1个。

大家看到问题出在哪了吗?预置常量不足以表达1/3的数据本原,要改用动态计算的方式,即“1除以3”,改成如下语句:

printf("I shouldget one third: %f\n",(1.0/3) * total);

这时,total取任意值,计算机都尽它的能力(视浮点精度而定)让结果准确,不像上面分1万个苹果差1个,分10万个差1个,这种变化就是从命令式编程走向函数式编程。

有人对函数式编程做了总结,参见David Mertz写的文章:CharmingPython: Functional programming in Python,FP至少具备如下7个特征:

  • 函数是“首类”(first-class),你可以像对待数据一样对待函数,比如将函数作为一种数据传递给另一个函数。
  • 递归被用作主要控制结构,在某些FP语言中,甚至没有其他的可成构造“循环”的手段。
  • FP通常重点关注列表处理(比如LISP语言),常用递归子列表作为循环替代品。
  • FP是可避免副作用(或称边际效应)的纯功能性语言。这不同于命令式语言中,常用变量记录或传递中间状态。
  • FP不欢迎或禁止使用“语句”,所有东西都是可计算的表达式,换句话说,都是函数加参数,即便定义变量或函数也是如此。
  • FP关注计算什么甚过如何进行计算。
  • FP编程习惯使用高阶函数,换句话说,某函数对另一些函数做操作,该函数又被别的函数操作。

 

CSE与函数式编程

与其理解把CSE看成一门脚本语言,还不将它看成一个语言族,其表达能力已覆盖过程语言、面向对象语言、脚本语言。CSE具有强大的表述能力,得益其精巧的内核,不妨把CSE内核看作一门“编程语言的语言”,CSE-C、CSE-C++、CSE-C#、CSE-CSE都是这个语言的实例化表现形式。

如果我们内层角度去看CSE,它是函数式语言,具有柔性语义,衍化能力强,所以能较好仿真各种语言。如果从外层角度去看,CSE具有强烈的命令式特质,因为它已仿真的几门语言都是命令式的。

在本系列文章中,我们尝试构造一门全新的LISP语言,姑且称之为“CSE-LISP”,语法特征与Common List接近。用到一些尚未公开的内核API,不少内容连CSE用户手册也未涉及,可以说是独家揭密。如果想试着运行本文举例的代码,请安装VcAgileV2.2.2.3以上版本。

 

操作运算与函数调用的等价性

CSE语言中,数学运算、逻辑运算等一元或二元操作表达式与函数调用是等价的,比如面脚本:

3+ 4
3 + 4 * 5
expr1 || expr2

可以写以如下形式:

`+`(3,4)
`+`(3,`*`(4,5))
`||`(expr1,expr2)

上面加、乘等操作符用一对反斜引号括起来,表示将操作符看成常规标识符(变量名、函数名都是标识符),后面举例还会经常用到反斜引号。

因为存在这种等效性,模拟一门LISP语言时,我们可以大量重用CSE现有定义,不必从零开始构。如下操作符将直接挪到CSE-LISP中使用:

 

操作符

说明

一元操作符

!

 

~

 

++

 

--

 

+

取正

-

取负

二元操作符

=

 

+=

 

-=

 

*=

 

/=

 

%=

 

&=

 

|=

 

^=

 

<<=

 

>>=

 

is

 

as

 

inside

 

&&

 

||

 

&

 

|

 

^

 

==

 

!=

 

>

 

>=

 

<

 

<=

 

<<

 

>>

 

+

-

*

/

 

%

 

@

 

标记操作符

(

tier立即数表达式,用“)”结尾

$(

函数式表达式,用“)”结尾

@(

逗号表达式,用“)”结尾

[

buff立即数表达式,用“]”结尾

{

dict立即数表达式,用“}”结尾

变元操作符

global

 

local

 

dummy

 

return

 

yield

 

throw

 

try

 

lambda

 

declare

 

 

其中“$(”操作是CSE专用于描述函数式风格的调用表达式,前面举列的3个表达式改成标准的函数式风格,如下:

$(+,3,4)
$(+,3,$(*,4,5)
$(||,expr1,expr2)

这3行与Common List中如下语句等效:

(+3 4)
(+ 3 (* 4 5)
(or expr1 expr2)

是不是很像?除了:

1.        CL中用一对括号表示一次运算,在CSE要用“$(”与“)”,原因嘛,括号“()”已被CSE其它语义占用。委屈点,我们就用“$()”。

2.        CL用空格分隔算式列表中各个项目,CSE要用逗号。

3.        我们暂用“||”表示逻辑或运算,后面我还会介绍怎么把它也改成“or”。

上面表格中的所有操作符都能用在“$()”表达式中,用法请从已有CSE知识基础上推导,我不展开介绍。

 

我们的起点

拿C/C++语言来对照,表达式级别的操作(如加、减、乘、除等),借助CSE现有能力就已支持“函数式”化改造,语句级别的指令,如if、else、for、while等控制语句,我们还缺,而且我们不计划用现有东西,因为现有CSE语法为仿真C/C++而实现,带有明显的命令式特征。

在语句之上是函数定义,我们使用CSE的lambda即可,lambda指令已是函数式风格。class类?算了吧,那是命令式语言的观点。

OK,后面我们重点补充控制语句的FP实现,至于CSE-LISP的元数据,借用CSE的内核数据类型即可,域名空间、变量读值与赋值也借用CSE已有框架。

 

结构化设计的三种控制结构

结构化程序设计的三种基本控制结构是顺序、选择、循环。CSE的逗号表达式(这个叫法借鉴C/C++语言,只是这么称呼而已)已经支持顺序执行了,写成lisp风格,例如:

$(=,i,0);
@($(++,i),$(++,i),i);

第一句,给变量i赋值0,猜猜第二句的运算结果是什么?是2。列表中各表达式依次计算,最后一个计算结果用于返回,i进行两次“++”操作,结果自然是2。创建变量并赋值,既可以用“=”,也可以用“as”指令,不过,用“=”要受变量的类型前缀限制,详情请参考CSE功能手册。

选择结构在多数语言中用if/else表达,其中if判断式中用到与、或逻辑判断,与或判断具有短路特点。我们为CSE-LIST语言也设计and与or,if、else就改用一个switch指令,不能再用if/else了。先定义and与or指令:

$(as,and,`&&`);
$(as,or,`||`);

测试一下是否具有短路效应:

$(and,0,$(print,"noprint"));
$(and,1,$(print,"should print"));
$(or,1,$(print,"no print"));
$(or,0,$(print,"should print"));

借助and与or的短路效应来构造switch对true与false进行分支的机制,如下:

$(as,switch,
  $(lambda,
    $(declare,iCond,#tExpr,#fExpr),
    $(and,iCond,@(#tExpr @ -1, returntrue)),
    $(or,iCond,@(#fExpr @ -1, returnfalse))
  )
)

lambda用来定义一个匿名函数,将该匿名函数赋给switch,“@ -1”让指定的表达式在父级域名空间做计算。我们测试一下:

$(switch,0,$(print,"truebranch"),$(print,"false brance"))
$(switch,1,$(print,"true branch"),$(print,"false brance"))

switch调用带3个参数,分别为条件表达式、true分支语句、false分支语句。上面true与false分支都用一条print语句,如果是多条语句,可用CSE的逗号表达式(如前面举例)表示。

顺序与选择轻松搞掂,我们接下来啃循环结构,要复杂一些。如果借用CSE已有的while关键字,情况会很简单,比如:

$(as,i,0);
$(while,$(<,i,10),$(print,i),$(++,i))

问题是,我们要用FP的风格搞掂循环结构,不借助预先定义的、基于先验知识的while定义。基于列表递归迭代是FP构造循环的主流方式,我们现在就用FP风格定义loop循环:

loopas lambda:
  declare(#cond,#_);
  iCond as #cond @ -1;
  $(and,iCond,
    @(#_.insert(0,`@()`);
      #expras #notator(#_);
      #expr@ -1;
      nextLoop as #notator(`$()`@0,loop@0,#cond);
      nextLoop.join(#_);
     return nextLoop;
  )),
  $(or,iCond,returnPASS@0)
end;

为了方便大家理解,上面部分语句没有使用纯正的函数式风格。这正是CSE强大的地方,纯函数式描述与命式式描述可以混用,后面我还会介绍更易读、易写的CSE-LISP代码编写要求。

上面函数定义是不是有点费解,我稍微解释一下,declare语句用于从调用栈恢复函数调用参数,如果declare中指定参数名以“#”开头,表示你要原样恢复表达式(而不是计算后的值),“#_”表示将剩余的参数都恢复成一个列表。#notator函数用于把各参数组装成函数式调用,比如“#notator(print@0,3)”与“#notator([print@0,3])”,均等效于“$(print,3)”。

这个loop函数在判断条件表达式为true时,顺序执行循环体中的语句,最后还将执行过的loop语句组装回来作为结果值返还给调用者,如果判断条件为false时,则返回PASS。这个loop循环可按如下方式使用:

$(=,i,0);
$($(loop,i < 10,
  $(print,"here",i);
  $(++,i);
));

loop函数要用“$( $(”两层方式来调用,里层调用后如果返回“$(”指令,意味着系统还得计算它,否则外层“$(”不知道如何调用,让循环一直进行,只需让loop函数返一段“让它再次运行”的代码,退出循环则返回“PASS@0”,CSE中的PASS函数的含义是什么都不做。

 

(未完,待续)

 

相关文章:

作者自述CSE语言设计思想(四)----用CSE模拟LISP语言(中)

作者自述CSE语言设计思想(五)----用CSE模拟LISP语言(下)



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值