Faramita语言设计草案0.1
广告语(不可当真):真正的可移动代码(mobile code),真正的分布式计算
名词缩写解释:
RPC: Remote Procedure Call 远程调用
DSL: Domain Specific Language 领域专用语言
AOP: Aspect Oriented Programming 面向方面编程
1. 为什么叫Faramita这个名字?
梵语波罗密特(Faramita)的意思是“到达彼岸”或“做事成功”。传说当时外国商人乘船远涉重洋,饱经大海风浪之险,每当他们达到“黄木之湾”的扶胥港,遥见山上的南海神庙时,不禁发出“Faramita”的欢呼!
(引自陈柏坚《广州是中国经久不衰的外贸港市》)
Faramita是彼岸的意思。开头的三个字母“Far”正好表示“远”的意思。Faramita这个名字非常适合表达这门编程语言的设计初衷——这门语言设计出来的目的就是要迁移到远程服务器端运行。
我在网上搜索了一下Faramita这个词,发现还没有一门叫做Faramita的编程语言,于是就选用了这个名字。
Faramita这门语言还不存在,还属于设想和草创阶段。为了避免引起“不自量力”的大惊小怪,这里先要做一个免责声明。这年头,DSL思想已经深入人心,随便一个人设计一门语言,真的不是什么大逆不道、不自量力的事情。
当然,设计语言是一回事,实现又是另外一回事。大部分头脑一时发热的语言设计者,都不具备实现一门编程语言的能力。比如,我就不具备这门能力。我做过语法解析的工作,我知道那工作有多难。更别说还要把一门语言运行起来。
好了,废话少说,言归正传。虽然Faramita这门语言距离真正实现还遥遥无期,但我还是想把这门语言的设计理念先写出来。
2. 数据库服务模式的启发
Faramita语言的典型应用场景是这样的。客户端把一段代码发到服务器端,服务器端接收到客户端发来的代码,解释执行,然后把结果返回给客户端。
我所知道的,采用这种使用模式的语言有两种,一种是关系数据库查询语言SQL,一种XML数据库查询语言XQuery。我不熟悉XQuery,就拿SQL做例子。
数据库客户端拼装一段SQL代码,发到数据库服务器,数据库服务器解释执行SQL,把结果返回给客户端。
我用JDBC编写程序的时候,曾经遇到过很多次这样的问题——我们需要根据前一条SQL的结果,来决定下一条SQL是什么。
在一般的编程模式中,客户程序先发一条SQL,拿到结果,然后再发下一条SQL。
有些数据库支持动态临时存储过程,这时候,为了减少查询次数,提高运行效率,客户端可以动态拼装一段存储过程,里面包含SQL以及并不复杂的判断逻辑。客户端把拼装的存储过程发给数据库,数据库就可以执行存储过程,直接返回最终结果。
从理论上来说,存储过程应该在数据库中定义,然后在客户端调用。但是,这种临时拼装存储过程的做法有它的灵活性和应用场景。
Faramita语言的工作方式就有些类似于这种模式。
3. 关于Web Service的设想
目前的Web Service实现方案有两种。一种是SOAP协议,一种REST风格。
我们先来看一个SOAP的典型例子。
The SOAP request:
POST /InStock HTTP/1.1
Host: www.example.org
Content-Type: application/soap+xml; charset=utf-8
Content-Length: nnn
<?xml version="1.0"?>
<soap:Envelope
xmlns:soap="http://www.w3.org/2001/12/soap-envelope"
soap:encodingStyle="http://www.w3.org/2001/12/soap-encoding">
<soap:Body xmlns:m="http://www.example.org/stock">
<m:GetStockPrice>
<m:StockName>IBM</m:StockName>
</m:GetStockPrice>
</soap:Body>
</soap:Envelope>
The SOAP response:
HTTP/1.1 200 OK
Content-Type: application/soap+xml; charset=utf-8
Content-Length: nnn
<?xml version="1.0"?>
<soap:Envelope
xmlns:soap="http://www.w3.org/2001/12/soap-envelope"
soap:encodingStyle="http://www.w3.org/2001/12/soap-encoding">
<soap:Body xmlns:m="http://www.example.org/stock">
<m:GetStockPriceResponse>
<m:Price>34.5</m:Price>
</m:GetStockPriceResponse>
</soap:Body>
</soap:Envelope>
我们可以看到,SOAP不过就是一种XML格式的RPC,和XML-RPC没有什么本质区别。
这么简单的一个RPC调用,我们为什么不用一段简单的脚本呢?
比如,
The Faramita request:
POST /InStock HTTP/1.1
Host: www.example.org
Content-Type: application/soap+xml; charset=utf-8
Content-Length: nnn
GetStockPrice(StockName=IBM)
The Faramita response:
HTTP/1.1 200 OK
Content-Type: application/soap+xml; charset=utf-8
Content-Length: nnn
Price=4.5
如果把上述的RPC写成REST风格,应该是这样的。
http://www.example.org/InStock/GetStockPrice/IBM
可以看到,REST风格却是简单了许多,所有参数都在URI中表现出来。但是,REST风格的限制同样也很明显,它无法表达复杂的数据类型。当然,据说,REST风格的目的是资源定位,而不是RPC。REST风格有其自己的资源查询语言,比如XPath。
Faramita语言针对的目标主要是SOAP,而不是REST。目前的Web Service本质上其实都是RPC函数库,提供的RPC接口都是比较高层的封装。客户端只能采用简单的RPC方式来调用Web Service的服务。
我有这么一个想法,Web Service为什么不能象数据库服务器那样工作呢?Web Service可以只提供一些基本函数,客户端可以自由组装这些基本函数,就象组装SQL一样。更进一步,Web Service甚至可以象支持动态存储过程的数据库那样工作,客户端可以自由组装Web Service基本功能函数,还可以加入复杂的控制逻辑,然后一股脑发到Web Service服务器执行,就象数据库客户端把拼装的动态存储过程发到数据库服务器一样。
4. 普通解释语言的局限
Faramita语言的使用模式和应用场景正是类似于SQL。只不过,Faramita语言是一种各种语法元素完整的通用编程语言,而不是一种特制的数据查询语言。
接受一段代码,然后解释执行,任何一门解释语言都可以做到这一点。比如,ErLang、Python、Ruby、Javascript等。这些语言都有这样的动态代码解释函数eval,具体调用方式如下:
eval(context, code)
其中 context就是包含变量表的运行环境,code就是解释执行的代码。具体的函数名不一定叫做eval,但很多解释语言都提供类似的函数。
那么,我为什么还要另外设计一门语言呢?主要有几个原因。
一是代码越界问题。这些解释语言都不是为了“发到远程服务器执行”这个目的而设计的,所以,代码可以越界访问超出本函数范围的外部变量或者全局变量。这会给代码的打包发送带来很大麻烦。为了保证代码执行,还需要把整个运行环境都打包发送过去。当然,这个问题可以由程序员自己来解决。程序员可以限制code的访问范围,从而可以缩减Context。但是,这种依靠人脑来进行约束的做法,不能总是保证万无一失。
二是函数的命名参数问题。我们通过前面的SOAP例子可以看到,在远程调用中,为了匹配参数,函数名的参数都是带着名字的。在现有的解释语言中,有不少语言都可以用间接的方式实现命名参数,比如ErLang、Python、Ruby等。但这些语言并没有在语法中直接支持命名参数,在参数包装方面总是会麻烦一些。
三是不支持Curry。几乎所有的动态解释语言都支持Closure(闭包)这个特性。比如Javascript、ErLang、Python、Ruby等。但是,这些语言却都不支持Curry。我只知道一门支持Curry的解释语言——Haskell,以及从Haskell派生出来的Jaskell。
5. 解释一下Curry
Curry是什么意思呢?Curry有点Partial Evaluation的意思。一个函数有多个参数,我们如果给够了参数,那么函数就会执行。如果我们给的参数少于函数需要的参数,那么我们得到的只是一个部分赋值的函数。下面举个例子。
如果我们只给出第一个参数,比如 f1(2),这个时候返回的结果是另一个有两个参数的函数。
f(x, y, z) {
return x + 2*y + 3*z;
}
如果我们再给出一个参数,比如 f1(2),这个时候返回的结果是另一个有一个参数的函数。
f12(z) {
return 1 + 2*2 + 3*z; // 即 5 + 3*z
}
如果我们再给出一个参数,整个函数就会执行完毕,返回的结果是一个数字。
整个过程串起来就是:f(1)(2)(3) = 1 + 2*2 + 3*3
Curry是函数式语言的特性。关于Curry和函数式语言的具体描述,不是本文描述的重点。如果有读者对此抱有疑惑的话,可以在网上找到很多简单易懂的例子。
我一直觉得,Curry比Closure(闭包)这个特性更加重要,更加实用。
对于Python、Ruby、Javascript这种面向对象语言来说,Closure(闭包)在大部分情况下,就是鸡肋一般的特性。当然,对于ErLang这种函数式语言来说,闭包是必需的。不过,如果ErLang能够支持Curry的话,就不用写那么宽的匿名闭包函数定义了。
关于什么是Closure(闭包),也请读者到网上自行寻找很多简单易懂的例子。
6. Faramita语言的设计概要
从上面的SOAP例子可以看出,Web Service远程调用本质上还是一种函数调用。因此,Faramita语言是一门函数式编程语言。函数就是一等公民的对象,函数对象可以作为参数和返回值传来传去。至于是否支持类定义这样的面向对象语言特性,倒不是必需的。
为了更好的映射甚至替代SOAP,Faramita语言从语法级别就支持命名参数。为了统一起见,Faramita强制要求所有函数调用都采用命名参数的方式。命名参数写起来虽然长一些,但是,用参数名字代替参数位置,能够让程序的可读性提高,尤其是在参数比较多的时候。
Faramita语言从语法级别上就支持Curry,但是,Faramita语言不支持Closure(闭包)。在Faramita语言中,名字非常重要,连函数调用都要强制采用命名参数呢,Faramita自然不允许定义匿名函数。为了解析、链接、打包的方便,Faramita语言甚至不允许定义嵌套函数。
为了防止函数代码访问越界,Faramita语言不允许函数访问任何外部变量和全局变量,Faramita语言也不允许函数代码对传进来的参数进行任何改变,也就是说,Faramita语言中,函数的参数都是只读的。这种设计借鉴了ErLang语言的一些特点,能够更好地支持多CPU并行,或者多核并行。
ErLang语言的设定非常严格,任何变量都是只读变量,只能赋值一次。以至于,ErLang连循环这样的控制语句都不支持。在Faramita语言中,函数参数是只读的,但是,函数体内部的局部变量却是可以改变的,而局部变量的改变不会扩散到函数的外部。因此,虽然Faramita不支持语句级别的并行,但是却能够支持函数级别的并行。
注意,Faramita并不限制函数内部的IO访问。而IO访问可能会引起全局状态的读写冲突。这时候,函数级别的并行特性就丧失了。
Faramita并不是一门为并行计算设计的语言,而是为远程解释执行设计的语言,因此,运行效率并不是Faramita考虑的因素。参数只读而导致函数级别的并行特性,只是Faramita为了防止函数代码越界访问的一种副产品而已。
同样,关于并行计算,还是请读者自行去网上搜索相关资料。
SOAP用XML表达程序语句虽然臃肿不堪,但是,不得不承认,XML的解析还是要比脚本程序的解析简单了一些。
Faramita语言一方面尽量讨好程序员,让程序写起来尽量简单,一方面讨好解析器,让程序解析起来尽量简单。
Faramita语言一方面借鉴了Python、Ruby、Javascript、ErLang的一些简单语法特性,尽量简化条件语句和循环语句等流程控制语句,一方面借鉴了XML首尾呼应的特性,引入了很多begin、end的语法,写起来稍显繁琐。
下面讲解Faramita的具体语法特性。
7. Faramita数据类型
Faramita是一门函数式编程语言,不支持类的定义,语法元素比较简单,但是,包含的概念却不简单。
Faramita是一门弱类型动态解释语言。变量没有类型,但是变量值却有类型。Faramita的类型包括如下。
number:表示数字类型,比如整型、浮点型。Faramita并不是一门科学计算语言,对于数字计算并没有特殊的能力,因此,Faramita并不区分数字类型,都统一为number。至于数字运算,完全按照约定俗成。
string: 字符串类型。内部包含byte[]和encoding charset。Faramita中的类型名称、变量名称、参数名称全都用字符串表达。
list:比如,[1,2,x,y]。 意义同python、ruby、ErLang的list定义一样。
function:函数类型。包含Context参数定义和函数代码。其意义和ErLang的function定义一样,只不过,Faramita的function支持Curry、Partial Evaluation。
diction:字典类型。这是Faramita语言中最特殊的类型。diction有些象HashMap这种数据类型,里面可以放很多成对的name – value。只不过,diction还提供了更丰富的信息,diction允许空名定义,即只定义名字,而不定义对应的值。diction是Faramita中最常用、最重要的类型。定义方式比如(a=12,b,c,d=4)。
8. Faramita函数定义
Faramita语言中,函数定义分为两个段——context和code。
比如,f(x,y,z){return x + 2*y + 3*z}这样的函数,用Faramita语言表现,就是下面的样子。
begin_function f
begin_context
x, y, z
end_context
begin_code
return x + 2*y + 3*z
end_code
end_funciton f
可以看到,一个简单的函数,用Faramita语言表现,显得繁琐了许多。从函数定义的方面来说,Faramita并不是一个对程序员非常友好的语言,而是这种函数定义的方式,一是为了讨好解析器,降低解析难度,代码打包容易,二是为了实现Curry。上面的函数定义中的Context,实际上就是函数对象的成员变量列表,数据类型是diction。
Faramita中,函数调用的语言就友好了许多,当然,命名参数的强制要求还是麻烦了一些。函数调用必须这么写。
比如,f(x = 1, y = 2, z = 3)
上述函数调用的结果为 1 + 2 * 2 + 3 * 3 = 14。
9. Faramita函数对Curry的灵活支持
那么,f(x=1)的调用结果如何呢?Faramita支持Curry,而且支持得非常灵活。
这里需要引起注意的是,f(x=1)这个调用。(x=1)这实际上就是diction对象的创建语句。
f(x=1)的真实含义是,把(x=1)这个diction对象赋给f这个函数对象。f(x=1)完全可以写成下面的形式。
dict_x = (x=1)
f(dict_x)
当f函数得到一个diction类型的参数列表的时候,f函数会拿传进来的参数d和自己的context进行一一对比。首先,参数名字一定要匹配上,如果参数名字不符,那么就会报错。其次,对参数个数进行匹配,如果传进来的参数个数少于context要求的参数个数,函数不会真正执行,而是会把传进来参数先记录在context中,产生一个新的函数对象。这时候,curry就发生了。比如,f(x=1)的结果只是另一个函数对象f1,相当于如下的函数定义。
begin_function f1
begin_context
x = 1, y, z
end_context
begin_code
return x + 2*y + 3*z
end_code
end_funciton f1
同样,f(x=1)(y=2)的执行结果也是一个新的函数对象,相当于如下的函数定义。
begin_function f12
begin_context
x = 1, y = 2, z
end_context
begin_code
return x + 2*y + 3*z
end_code
end_funciton f12
只有当参数完全圆满之后,函数的code代码段才会执行,产生结果。
比如,f(x=1)(y=2)(z=3)的执行结果就是1+2*2+3*3=14。
我们可以看到,Faramita对于curry的支持非常直观。另外,由于context里面的参数都有名字,Faramita的Curry并不强求参数传入的顺序,写成f(z=3)(x=1)(y=2)可以得到同样的结果。
另外,Faramita函数支持缺省初始参数值,比如,上面的f12函数定义中,x = 1, y = 2就可以看做x 和 y 的缺省参数值。调用f12的时候,完全可以采取另外x, y值。
需要注意两点。一、函数对象返回的结果可能有两种类型,一种是context部分赋值的函数对象,一种是执行完毕之后得到的最终结果类型。这两种结果类型的区分,需要程序员自己根据语义在程序中处理。否则的话,就会引起运行期错误。
二、因为函数对象不允许context内容进行改变,只能产生一个新的context对象,从而产生一个新的函数对象。因此,所有的函数调用,不管返回的结果是一个数值对象还是函数对象,都是一个全新的对象。
10. Faramita语言的条件语句 if, switch
从前面的例子可以看出,Faramita对于每个语法块结构都定义了成对begin、end关键字。Python语言强制换行缩进,Faramita不强制缩进,但是强制换行。比如,前面的函数定义中,begin_function、end_funciton、begin_context、end_context、begin_code、end_code这些关键字都必须单独占用一行。这种做法使得语法稍显臃肿,不过,却可以让语义清楚,解析器处理起来也方便
Faramita的if语句秉承了begin end的风格,定义了if, else_if, else, end_if等关键字。
比如,
if a = b
do_something
end_if
又比如
if a = b
do_something
else_if a > b
do_otherthing
else
do_morething
end_if
同样,switch、case之类的条件分支语句也是类似定义。定义了switch、case、case_any、end_switch等关键字。比如。
switch a
case 1:
calculate_something
case ‘abc’:
do_something
case_any:
do_general
end_switch a
我们可以看到,switch语法块十分灵活,可以同时根据变量类型和变量值进行case分支分配。需要注意的是,case_any表示除了上述case之外的case。case_any这个分支也可以不写。switch块中的每个case分支执行完之后,都自动退出switch块,而不会向下漏。因此,switch不过是if语法块的简写形式。
11. Faramita的循环语句
Faramita的循环语句尽量采用简单的形式。
for i in 1..10
do_someting(i)
end_for
for a in list1 //注:list1是一个列表
do_something(a)
end_for
for
listen_on_port(80) //注:无限循环
end_for
另外,在多层循环嵌套语句中,break语句直接跳到更外层的循环体,也是支持的。这就需要用到label定义。比如。
a, b = 0 //注:Faramita支持这种临时变量的批量赋值语句
label: outter_loop
for
b = b + 2
for
a = a + b + 1
if a > 100
break outter_loop
a = a * 2
end_for
end_for
因此,for、break、break label这样的语句组合起来,可以实现高级的流程控制。
12. Faramita语言中的模块
Faramita语言的源文件管理方式是Java的Package和ErLang的module这两者的结合。
Faramita的源文件后缀是.far。比如,我们有这么一个源文件的整个路径。
sample/math/calculate.far
里面的内容应该是这样。
begin_module sample.math.calculate
中间是函数定义。
end_module sample.match.calculate
Faramita的源文件命名原则很严格。一个源文件中,只能包含一个module,而且module的名字必须和文件路径相对应。这种严格要求是为了源文件管理上的方便。
既然一个源文件中只有一个module,那么begin_module必然是第一行语句,end_module必然是最后一行语句,为什么还这么麻烦,一定要写begin_module和end_module呢?而且还一定要跟着整个module的全名?这样要求的原因在于,Faramita是为了代码迁移传输而设计的语言。这样的begin end module定义,可以方便地把不同的module打包在一起传输。比如,我们有可能在HTTP文本中传输这样的代码。
begin_module sample.math.calculate
中间是函数定义。
end_module sample.match.calculate
begin_module sample.math.calculate2
中间是函数定义。
end_module sample.match.calculate2
这样,我们就可以在一段文本协议中,同时传输多个源代码模块。
13. Faramita语言中的函数链接
Faramita中的函数调用,限制非常严格。除了最基本的系统函数之外,所有的函数调用都必须通过context定义或者参数传递进来。哪怕是本模块的函数也是如此。这是为了更加清晰地表达函数之间的链接关系。比如。
begin_module sample.math.calculate
begin_funciton count
begin_context
list
end_context
begin_code
count = 0
for item in list
count = count + 1
end_for
return count
end_code
end_function count
begin_funciton f
begin_context
count = sample.math.calculate.count
list
end_context
begin_code
if count(list) > 0
doSomething
else
return 0
end_if
end_code
end_function f
end_module sample.match.calculate
我们可以看到,函数f为了调用本模块中的count函数,必须在context中明确引入count函数的全名——模块名加上函数名。这样的定义虽然麻烦,但是使用起来却十分灵活。比如,我们可以这么调用函数f。
f(list=[1,3,5])
f(list=[2,7,8], count = anotherModule.count)
anotherModule.count是另外一个统计函数,有可能只统计奇数或偶数,也有可能只统计非零的数值。这就可以任意替换。
14. Faramita函数的打包发送
有了如上所述的语法元素,我们就可以把Faramita函数打包起来,发送到Web Service服务器端执行了。
比如,远端Web Service服务器提供了这样一组基本函数,service.sum, service.count, service.filter等。
客户端可以组织这么一段代码。
begin_module client
begin complex_query
begin_context
sum = service.sum, count = service.count, filter = service.filter, serviceContext,
myFilter = client.filter_out_0
end_context
begin_code
if sum(serviceContext) = 0
return 0
list1 = serviceContext.list1
quantity = count(myList=list1)
if(quantity < 100)
return list1
return filter(myList=list1, myFilter=myFilter)
// 当参数名和值相等的时候,可以省略赋值
// 可以写成 return filter(myList = list1, myFilter)
end_code
end_complex_query
begin_function filter_out_0
begin_context
myList
end_context
begin_code
result = []
for item in myList
if item = 0
continue
result.add(item)
end_for
return result
end_code
end_function filter_out_0
end_module client
这段代码会通过一个远程调用函数发送到Web Service服务器的某个服务端口。
RPC(intialContext = myContext, queryFunction = client.complex_query,
port = 80, host=”example.com”)
这个时候,complex_query就会被打包起来,发送到example.com的80端口。解析器会检查complex_query函数中引入的函数链接。如果解析器发现complex_query引用了客户端本地的函数的话,解析器就会把对应模块的对应函数一起打包进来,直到把所有相关函数全部打包进来。比如,上面的例子中,filter_out_0这个函数就会被打包进来。
因此,程序员一定要注意,发送到远程服务器的函数调用关系不要太复杂,否则会传输太多的代码。
对于这一行,
sum = service.sum, count = service.count, filter = service.filter
由于解析器在客户端本地找不到对应的函数定义,只会把service.sum这些值当做字符串保存起来,直到真正运行起来之后,才回去试图寻找对应的函数定义。那就是发生在服务器端了。这些函数名称的定义,可以放在函数的context定义中,也可以放在RPC函数的initialContext定义中。
远程服务器解析执行客户端脚本的时候,context的赋值过程如下。首先,先把intialContext里面的内容都覆盖到context中,产生一个新的context。然后,服务器端会把serviceContext也覆盖到context中,然后再试图执行client.complex_query。
因此,serviceContext中的定义优先级最好,其次是initalContext,最后是函数context中的缺省初始定义。
15. 远程调用的文本传输协议
SOAP的XML格式使用用来表达树形数据结构,却不适合用来表示代码。我们可以采用灵活的文本协议方式。当然,这就是底层协议层的工作了。不过,简单的文本协议,人读起来也更容易看懂。
当远程服务调用只有一个参数的时候,可以用REST风格。比如前面的例子。
当远程服务调用有多个简单参数的话,就可以直接采用Faramita代码脚本调用的方式。比如下面的文本。
The Faramita request:
POST /InStock HTTP/1.1
Host: www.example.org
Content-Type: application/soap+xml; charset=utf-8
Content-Length: nnn
service.GetStockPrice(StockName=IBM, month=July)
当initialContext参数具有复杂的树形结构时(这种情况很少见),我们还是需要用SOAP的XML格式来表达树形数据结构。当然,具体的代码调用部分,不管是直接调用服务器端的远程服务,还是把打包好的客户端脚本传过去,我们都可以用最直观的脚本语言来表示。这样,整个传输协议的文本的可读性就会高了很多。
具体的文本协议,本文暂不讨论。总之,遵守两个原则。人读起来要方便,计算机读起来也要方便。
16. Faramita语言中的运行期类型检查
Web Service是松耦合协议,如果用Java、C#等静态类型语言来实现的话,必须用到代码生成。如果用Python、Ruby、Javascript等动态类型语言来实现的话,就会简单了许多。
静态类型语言虽然在处理松耦合协议方面比较麻烦,但也有它的好处,那就是生成的代码能够强制类型匹配。
为了引入类型检查这个功能,Faramita允许函数context定义中包含类型检查信息。比如。
begin_function f
begin_context
number : // 必须单独成行
x, y, z
end_context
begin_code
return x + 2*y + 3*z
end_code
end_funciton f
在程序运行的时候,当函数f被调用的时候,解析器就会根据number: 这个类型定义信息,对传进来的参数进行类型检查。多种类型定义也是类似。比如。
begin_context
anything, something, // 不需要类型检查的变量
number :
x, y, z = 10,
w = 20,
string:
a, b, c,
function:
f1, f2
var:
otherting // var表示任意类型,即不需要类型检查
end_context
17. Faramita语言中的AOP
动态语言实现AOP十分简单,比如Ruby的mixin,Python、Javascript的运行期修改类型定义,等等。
Faramita中实现AOP尤其简单。因为Faramita是函数式编程语言,函数对象可以看作是只有一个方法的对象。我们知道,AOP的主要实现模式就是Proxy,需要包装对象的所有方法。函数对象只有一个方法,包装起来就尤其简单。而且,Faramita中所有的函数调用都是在Context中设置的,可以任意替换。由于这些优势,在Faramita中实现AOP非常轻松。
同时,Faramita还和Lisp一样,直接把解析器Interpreter内部的接口暴露出来,提供了Hook钩子函数定义,可以让程序员自己修改解析器的行为。这样的话,AOP实现起来,就更加简单了。
18. Faramita语言中的string
string是编程语言中最常见、最重要的数据类型,同样也是Faramita语言中的重要类型。
Faramita中的string,包含两个主要部分,一部分是byte[],一部分是enconding charset(编码)。Faramita中,string的缺省初始编码方式是utf-8。程序员可以自行定义每一个string对象的charset,也可以通过解析器Hook函数统一替换所有string的编码方式。
Faramita中的变量名和ruby的symbol、ErLang的atom类似。在Faramita中,字符串变量的赋值约定如下。
a = ‘1’
a = ‘bcd’
这些语句都是字符串赋值语句。
当bcd不是任何一个变量名的时候,我们也可以这样写。
a = bcd // 省略了引号
当字符串中含有 . , 之类的特殊字符时,最好用引号保护起来。假如字符串本身就含有单引号、双引号、逗号、换行符、斜杠这样的特殊字符,那么,最好用字符串常量定义的方法。
begin_text contant_str
this is
some , very , very complex
text, here….
end_text contant_str
Faramita中的string和python一样,都是不能改变的常量。每一次新的改动操作,都会生成一个新的string对象。
19. string中的变量替换
这里先提一下我以前做的一个叫做fastm的开源项目。有些网友给我发信息,问我fastm的现状。我感到十分歉意和汗颜。这里给出一个交代。
fastm是一个java语言编写的字符串模板工具,用做Java MVC Web中的View层。
在JSP、Velocity、Freemarker、PHP、ASP中,使用模式是
Data + Script = Text
在fastm中,使用模式是
Data + Template = Text
即,在fastm template中,不含有任何脚本逻辑,只包含占位符。
我在fastm中实现了很多特性,甚至提供了DOM、SAX和动态数据直接绑定的功能。做完了那一步之后,fastm就没有更多的可以升级的空间了。那时候,我明显地感觉到,Java这种静态类型语言用来开发Web这种松耦合应用,实在是不适合。为了实现对象属性数据的动态寻址,我大量地使用蹩脚的Reflection。而在动态语言中,这是轻而易举的事。
从那时起,我基本上就放弃了Java开发,而转去研究其他动态语言。比如,Python、Ruby、ErLang、Javascript等。在试用的过程中,我发现,每种语言都有各自的优缺点,都有各自对应的领域。Web Service、SOA这些buzz word又开始兴起。于是,我萌生了一种模糊的想法,那就是Faramita的雏形,集成了我想要的功能。当想法稍微成熟一些之后,我就写了出来。
Java中,有一个Message Text,可以简单地替换变量。还有些语言,支持字符串的通配符替换。还有些重量级的Template引擎,比如Velocity、Freemarker都可以用script来生成字符串。我想做一个简单的工作,把fastm的思想,引入faramita的字符串解析处理中。比如,我们可以这么定义字符串模板。
begin_text template1
hello, this is {$name}, please repeat following words {$n} times.
{begin_block: block1}war, {end_block: block1}
end_text template1
在这种模板中,只有两个动态部分定义。一个是变量占位符,一个块定义。写起来非常简单,使用起来也非常简单。只要把一个数据块和这个模板一匹配,最终结果就会出来。
20. Faramita解析器的实现
Faramita可以有多种运行方式。可以作为独立语言运行,也可以作为嵌入式语言运行。初步设想中,Faramita的宿主语言是Python和Ruby。Faramita和宿主语言之间,应该可以进行无缝集成,自由交互。这样的话,很多现成的复杂功能,都可以让宿主语言去做。
Faramita的语法定义比较简单,尤其在module、function、context、code级别,都提供了begin end成对的命名块。而且这些语法元素都是单独成行,更加容易处理。但是,到了具体的函数体内部的语句级别,就需要用到复杂的解析器了。
Faramita的解析器应该如何实现呢?可以采用语法定义生成静态的语法编译器。也可以采用ajoo编写的Haskell Parsec在多种语言上的移植。ajoo编写了JParsec、NParsec、Ruby Parsec,可惜,还没有编写Python Parsec。
Faramita的解析器实现起来不会太难,执行器实现起来也不会太难。Java中实现了Groovy、Javascript等动态解释语言的解析和执行,看了一下实现代码,基本上就是定义了一些对应的包装类。在Python、Ruby中,做到这一点更加容易。当然,也不会太简单,因为字符串解析工作,总是很繁琐很麻烦的工作。
也许等我方方面面都考虑清楚了之后,我会动手实现这个语言。我现在只有一个初步的还没有完全成熟的想法,先写在这里是为了吸取更多的意见。
广告语(不可当真):真正的可移动代码(mobile code),真正的分布式计算
名词缩写解释:
RPC: Remote Procedure Call 远程调用
DSL: Domain Specific Language 领域专用语言
AOP: Aspect Oriented Programming 面向方面编程
1. 为什么叫Faramita这个名字?
梵语波罗密特(Faramita)的意思是“到达彼岸”或“做事成功”。传说当时外国商人乘船远涉重洋,饱经大海风浪之险,每当他们达到“黄木之湾”的扶胥港,遥见山上的南海神庙时,不禁发出“Faramita”的欢呼!
(引自陈柏坚《广州是中国经久不衰的外贸港市》)
Faramita是彼岸的意思。开头的三个字母“Far”正好表示“远”的意思。Faramita这个名字非常适合表达这门编程语言的设计初衷——这门语言设计出来的目的就是要迁移到远程服务器端运行。
我在网上搜索了一下Faramita这个词,发现还没有一门叫做Faramita的编程语言,于是就选用了这个名字。
Faramita这门语言还不存在,还属于设想和草创阶段。为了避免引起“不自量力”的大惊小怪,这里先要做一个免责声明。这年头,DSL思想已经深入人心,随便一个人设计一门语言,真的不是什么大逆不道、不自量力的事情。
当然,设计语言是一回事,实现又是另外一回事。大部分头脑一时发热的语言设计者,都不具备实现一门编程语言的能力。比如,我就不具备这门能力。我做过语法解析的工作,我知道那工作有多难。更别说还要把一门语言运行起来。
好了,废话少说,言归正传。虽然Faramita这门语言距离真正实现还遥遥无期,但我还是想把这门语言的设计理念先写出来。
2. 数据库服务模式的启发
Faramita语言的典型应用场景是这样的。客户端把一段代码发到服务器端,服务器端接收到客户端发来的代码,解释执行,然后把结果返回给客户端。
我所知道的,采用这种使用模式的语言有两种,一种是关系数据库查询语言SQL,一种XML数据库查询语言XQuery。我不熟悉XQuery,就拿SQL做例子。
数据库客户端拼装一段SQL代码,发到数据库服务器,数据库服务器解释执行SQL,把结果返回给客户端。
我用JDBC编写程序的时候,曾经遇到过很多次这样的问题——我们需要根据前一条SQL的结果,来决定下一条SQL是什么。
在一般的编程模式中,客户程序先发一条SQL,拿到结果,然后再发下一条SQL。
有些数据库支持动态临时存储过程,这时候,为了减少查询次数,提高运行效率,客户端可以动态拼装一段存储过程,里面包含SQL以及并不复杂的判断逻辑。客户端把拼装的存储过程发给数据库,数据库就可以执行存储过程,直接返回最终结果。
从理论上来说,存储过程应该在数据库中定义,然后在客户端调用。但是,这种临时拼装存储过程的做法有它的灵活性和应用场景。
Faramita语言的工作方式就有些类似于这种模式。
3. 关于Web Service的设想
目前的Web Service实现方案有两种。一种是SOAP协议,一种REST风格。
我们先来看一个SOAP的典型例子。
The SOAP request:
POST /InStock HTTP/1.1
Host: www.example.org
Content-Type: application/soap+xml; charset=utf-8
Content-Length: nnn
<?xml version="1.0"?>
<soap:Envelope
xmlns:soap="http://www.w3.org/2001/12/soap-envelope"
soap:encodingStyle="http://www.w3.org/2001/12/soap-encoding">
<soap:Body xmlns:m="http://www.example.org/stock">
<m:GetStockPrice>
<m:StockName>IBM</m:StockName>
</m:GetStockPrice>
</soap:Body>
</soap:Envelope>
The SOAP response:
HTTP/1.1 200 OK
Content-Type: application/soap+xml; charset=utf-8
Content-Length: nnn
<?xml version="1.0"?>
<soap:Envelope
xmlns:soap="http://www.w3.org/2001/12/soap-envelope"
soap:encodingStyle="http://www.w3.org/2001/12/soap-encoding">
<soap:Body xmlns:m="http://www.example.org/stock">
<m:GetStockPriceResponse>
<m:Price>34.5</m:Price>
</m:GetStockPriceResponse>
</soap:Body>
</soap:Envelope>
我们可以看到,SOAP不过就是一种XML格式的RPC,和XML-RPC没有什么本质区别。
这么简单的一个RPC调用,我们为什么不用一段简单的脚本呢?
比如,
The Faramita request:
POST /InStock HTTP/1.1
Host: www.example.org
Content-Type: application/soap+xml; charset=utf-8
Content-Length: nnn
GetStockPrice(StockName=IBM)
The Faramita response:
HTTP/1.1 200 OK
Content-Type: application/soap+xml; charset=utf-8
Content-Length: nnn
Price=4.5
如果把上述的RPC写成REST风格,应该是这样的。
http://www.example.org/InStock/GetStockPrice/IBM
可以看到,REST风格却是简单了许多,所有参数都在URI中表现出来。但是,REST风格的限制同样也很明显,它无法表达复杂的数据类型。当然,据说,REST风格的目的是资源定位,而不是RPC。REST风格有其自己的资源查询语言,比如XPath。
Faramita语言针对的目标主要是SOAP,而不是REST。目前的Web Service本质上其实都是RPC函数库,提供的RPC接口都是比较高层的封装。客户端只能采用简单的RPC方式来调用Web Service的服务。
我有这么一个想法,Web Service为什么不能象数据库服务器那样工作呢?Web Service可以只提供一些基本函数,客户端可以自由组装这些基本函数,就象组装SQL一样。更进一步,Web Service甚至可以象支持动态存储过程的数据库那样工作,客户端可以自由组装Web Service基本功能函数,还可以加入复杂的控制逻辑,然后一股脑发到Web Service服务器执行,就象数据库客户端把拼装的动态存储过程发到数据库服务器一样。
4. 普通解释语言的局限
Faramita语言的使用模式和应用场景正是类似于SQL。只不过,Faramita语言是一种各种语法元素完整的通用编程语言,而不是一种特制的数据查询语言。
接受一段代码,然后解释执行,任何一门解释语言都可以做到这一点。比如,ErLang、Python、Ruby、Javascript等。这些语言都有这样的动态代码解释函数eval,具体调用方式如下:
eval(context, code)
其中 context就是包含变量表的运行环境,code就是解释执行的代码。具体的函数名不一定叫做eval,但很多解释语言都提供类似的函数。
那么,我为什么还要另外设计一门语言呢?主要有几个原因。
一是代码越界问题。这些解释语言都不是为了“发到远程服务器执行”这个目的而设计的,所以,代码可以越界访问超出本函数范围的外部变量或者全局变量。这会给代码的打包发送带来很大麻烦。为了保证代码执行,还需要把整个运行环境都打包发送过去。当然,这个问题可以由程序员自己来解决。程序员可以限制code的访问范围,从而可以缩减Context。但是,这种依靠人脑来进行约束的做法,不能总是保证万无一失。
二是函数的命名参数问题。我们通过前面的SOAP例子可以看到,在远程调用中,为了匹配参数,函数名的参数都是带着名字的。在现有的解释语言中,有不少语言都可以用间接的方式实现命名参数,比如ErLang、Python、Ruby等。但这些语言并没有在语法中直接支持命名参数,在参数包装方面总是会麻烦一些。
三是不支持Curry。几乎所有的动态解释语言都支持Closure(闭包)这个特性。比如Javascript、ErLang、Python、Ruby等。但是,这些语言却都不支持Curry。我只知道一门支持Curry的解释语言——Haskell,以及从Haskell派生出来的Jaskell。
5. 解释一下Curry
Curry是什么意思呢?Curry有点Partial Evaluation的意思。一个函数有多个参数,我们如果给够了参数,那么函数就会执行。如果我们给的参数少于函数需要的参数,那么我们得到的只是一个部分赋值的函数。下面举个例子。
如果我们只给出第一个参数,比如 f1(2),这个时候返回的结果是另一个有两个参数的函数。
f(x, y, z) {
return x + 2*y + 3*z;
}
如果我们再给出一个参数,比如 f1(2),这个时候返回的结果是另一个有一个参数的函数。
f12(z) {
return 1 + 2*2 + 3*z; // 即 5 + 3*z
}
如果我们再给出一个参数,整个函数就会执行完毕,返回的结果是一个数字。
整个过程串起来就是:f(1)(2)(3) = 1 + 2*2 + 3*3
Curry是函数式语言的特性。关于Curry和函数式语言的具体描述,不是本文描述的重点。如果有读者对此抱有疑惑的话,可以在网上找到很多简单易懂的例子。
我一直觉得,Curry比Closure(闭包)这个特性更加重要,更加实用。
对于Python、Ruby、Javascript这种面向对象语言来说,Closure(闭包)在大部分情况下,就是鸡肋一般的特性。当然,对于ErLang这种函数式语言来说,闭包是必需的。不过,如果ErLang能够支持Curry的话,就不用写那么宽的匿名闭包函数定义了。
关于什么是Closure(闭包),也请读者到网上自行寻找很多简单易懂的例子。
6. Faramita语言的设计概要
从上面的SOAP例子可以看出,Web Service远程调用本质上还是一种函数调用。因此,Faramita语言是一门函数式编程语言。函数就是一等公民的对象,函数对象可以作为参数和返回值传来传去。至于是否支持类定义这样的面向对象语言特性,倒不是必需的。
为了更好的映射甚至替代SOAP,Faramita语言从语法级别就支持命名参数。为了统一起见,Faramita强制要求所有函数调用都采用命名参数的方式。命名参数写起来虽然长一些,但是,用参数名字代替参数位置,能够让程序的可读性提高,尤其是在参数比较多的时候。
Faramita语言从语法级别上就支持Curry,但是,Faramita语言不支持Closure(闭包)。在Faramita语言中,名字非常重要,连函数调用都要强制采用命名参数呢,Faramita自然不允许定义匿名函数。为了解析、链接、打包的方便,Faramita语言甚至不允许定义嵌套函数。
为了防止函数代码访问越界,Faramita语言不允许函数访问任何外部变量和全局变量,Faramita语言也不允许函数代码对传进来的参数进行任何改变,也就是说,Faramita语言中,函数的参数都是只读的。这种设计借鉴了ErLang语言的一些特点,能够更好地支持多CPU并行,或者多核并行。
ErLang语言的设定非常严格,任何变量都是只读变量,只能赋值一次。以至于,ErLang连循环这样的控制语句都不支持。在Faramita语言中,函数参数是只读的,但是,函数体内部的局部变量却是可以改变的,而局部变量的改变不会扩散到函数的外部。因此,虽然Faramita不支持语句级别的并行,但是却能够支持函数级别的并行。
注意,Faramita并不限制函数内部的IO访问。而IO访问可能会引起全局状态的读写冲突。这时候,函数级别的并行特性就丧失了。
Faramita并不是一门为并行计算设计的语言,而是为远程解释执行设计的语言,因此,运行效率并不是Faramita考虑的因素。参数只读而导致函数级别的并行特性,只是Faramita为了防止函数代码越界访问的一种副产品而已。
同样,关于并行计算,还是请读者自行去网上搜索相关资料。
SOAP用XML表达程序语句虽然臃肿不堪,但是,不得不承认,XML的解析还是要比脚本程序的解析简单了一些。
Faramita语言一方面尽量讨好程序员,让程序写起来尽量简单,一方面讨好解析器,让程序解析起来尽量简单。
Faramita语言一方面借鉴了Python、Ruby、Javascript、ErLang的一些简单语法特性,尽量简化条件语句和循环语句等流程控制语句,一方面借鉴了XML首尾呼应的特性,引入了很多begin、end的语法,写起来稍显繁琐。
下面讲解Faramita的具体语法特性。
7. Faramita数据类型
Faramita是一门函数式编程语言,不支持类的定义,语法元素比较简单,但是,包含的概念却不简单。
Faramita是一门弱类型动态解释语言。变量没有类型,但是变量值却有类型。Faramita的类型包括如下。
number:表示数字类型,比如整型、浮点型。Faramita并不是一门科学计算语言,对于数字计算并没有特殊的能力,因此,Faramita并不区分数字类型,都统一为number。至于数字运算,完全按照约定俗成。
string: 字符串类型。内部包含byte[]和encoding charset。Faramita中的类型名称、变量名称、参数名称全都用字符串表达。
list:比如,[1,2,x,y]。 意义同python、ruby、ErLang的list定义一样。
function:函数类型。包含Context参数定义和函数代码。其意义和ErLang的function定义一样,只不过,Faramita的function支持Curry、Partial Evaluation。
diction:字典类型。这是Faramita语言中最特殊的类型。diction有些象HashMap这种数据类型,里面可以放很多成对的name – value。只不过,diction还提供了更丰富的信息,diction允许空名定义,即只定义名字,而不定义对应的值。diction是Faramita中最常用、最重要的类型。定义方式比如(a=12,b,c,d=4)。
8. Faramita函数定义
Faramita语言中,函数定义分为两个段——context和code。
比如,f(x,y,z){return x + 2*y + 3*z}这样的函数,用Faramita语言表现,就是下面的样子。
begin_function f
begin_context
x, y, z
end_context
begin_code
return x + 2*y + 3*z
end_code
end_funciton f
可以看到,一个简单的函数,用Faramita语言表现,显得繁琐了许多。从函数定义的方面来说,Faramita并不是一个对程序员非常友好的语言,而是这种函数定义的方式,一是为了讨好解析器,降低解析难度,代码打包容易,二是为了实现Curry。上面的函数定义中的Context,实际上就是函数对象的成员变量列表,数据类型是diction。
Faramita中,函数调用的语言就友好了许多,当然,命名参数的强制要求还是麻烦了一些。函数调用必须这么写。
比如,f(x = 1, y = 2, z = 3)
上述函数调用的结果为 1 + 2 * 2 + 3 * 3 = 14。
9. Faramita函数对Curry的灵活支持
那么,f(x=1)的调用结果如何呢?Faramita支持Curry,而且支持得非常灵活。
这里需要引起注意的是,f(x=1)这个调用。(x=1)这实际上就是diction对象的创建语句。
f(x=1)的真实含义是,把(x=1)这个diction对象赋给f这个函数对象。f(x=1)完全可以写成下面的形式。
dict_x = (x=1)
f(dict_x)
当f函数得到一个diction类型的参数列表的时候,f函数会拿传进来的参数d和自己的context进行一一对比。首先,参数名字一定要匹配上,如果参数名字不符,那么就会报错。其次,对参数个数进行匹配,如果传进来的参数个数少于context要求的参数个数,函数不会真正执行,而是会把传进来参数先记录在context中,产生一个新的函数对象。这时候,curry就发生了。比如,f(x=1)的结果只是另一个函数对象f1,相当于如下的函数定义。
begin_function f1
begin_context
x = 1, y, z
end_context
begin_code
return x + 2*y + 3*z
end_code
end_funciton f1
同样,f(x=1)(y=2)的执行结果也是一个新的函数对象,相当于如下的函数定义。
begin_function f12
begin_context
x = 1, y = 2, z
end_context
begin_code
return x + 2*y + 3*z
end_code
end_funciton f12
只有当参数完全圆满之后,函数的code代码段才会执行,产生结果。
比如,f(x=1)(y=2)(z=3)的执行结果就是1+2*2+3*3=14。
我们可以看到,Faramita对于curry的支持非常直观。另外,由于context里面的参数都有名字,Faramita的Curry并不强求参数传入的顺序,写成f(z=3)(x=1)(y=2)可以得到同样的结果。
另外,Faramita函数支持缺省初始参数值,比如,上面的f12函数定义中,x = 1, y = 2就可以看做x 和 y 的缺省参数值。调用f12的时候,完全可以采取另外x, y值。
需要注意两点。一、函数对象返回的结果可能有两种类型,一种是context部分赋值的函数对象,一种是执行完毕之后得到的最终结果类型。这两种结果类型的区分,需要程序员自己根据语义在程序中处理。否则的话,就会引起运行期错误。
二、因为函数对象不允许context内容进行改变,只能产生一个新的context对象,从而产生一个新的函数对象。因此,所有的函数调用,不管返回的结果是一个数值对象还是函数对象,都是一个全新的对象。
10. Faramita语言的条件语句 if, switch
从前面的例子可以看出,Faramita对于每个语法块结构都定义了成对begin、end关键字。Python语言强制换行缩进,Faramita不强制缩进,但是强制换行。比如,前面的函数定义中,begin_function、end_funciton、begin_context、end_context、begin_code、end_code这些关键字都必须单独占用一行。这种做法使得语法稍显臃肿,不过,却可以让语义清楚,解析器处理起来也方便
Faramita的if语句秉承了begin end的风格,定义了if, else_if, else, end_if等关键字。
比如,
if a = b
do_something
end_if
又比如
if a = b
do_something
else_if a > b
do_otherthing
else
do_morething
end_if
同样,switch、case之类的条件分支语句也是类似定义。定义了switch、case、case_any、end_switch等关键字。比如。
switch a
case 1:
calculate_something
case ‘abc’:
do_something
case_any:
do_general
end_switch a
我们可以看到,switch语法块十分灵活,可以同时根据变量类型和变量值进行case分支分配。需要注意的是,case_any表示除了上述case之外的case。case_any这个分支也可以不写。switch块中的每个case分支执行完之后,都自动退出switch块,而不会向下漏。因此,switch不过是if语法块的简写形式。
11. Faramita的循环语句
Faramita的循环语句尽量采用简单的形式。
for i in 1..10
do_someting(i)
end_for
for a in list1 //注:list1是一个列表
do_something(a)
end_for
for
listen_on_port(80) //注:无限循环
end_for
另外,在多层循环嵌套语句中,break语句直接跳到更外层的循环体,也是支持的。这就需要用到label定义。比如。
a, b = 0 //注:Faramita支持这种临时变量的批量赋值语句
label: outter_loop
for
b = b + 2
for
a = a + b + 1
if a > 100
break outter_loop
a = a * 2
end_for
end_for
因此,for、break、break label这样的语句组合起来,可以实现高级的流程控制。
12. Faramita语言中的模块
Faramita语言的源文件管理方式是Java的Package和ErLang的module这两者的结合。
Faramita的源文件后缀是.far。比如,我们有这么一个源文件的整个路径。
sample/math/calculate.far
里面的内容应该是这样。
begin_module sample.math.calculate
中间是函数定义。
end_module sample.match.calculate
Faramita的源文件命名原则很严格。一个源文件中,只能包含一个module,而且module的名字必须和文件路径相对应。这种严格要求是为了源文件管理上的方便。
既然一个源文件中只有一个module,那么begin_module必然是第一行语句,end_module必然是最后一行语句,为什么还这么麻烦,一定要写begin_module和end_module呢?而且还一定要跟着整个module的全名?这样要求的原因在于,Faramita是为了代码迁移传输而设计的语言。这样的begin end module定义,可以方便地把不同的module打包在一起传输。比如,我们有可能在HTTP文本中传输这样的代码。
begin_module sample.math.calculate
中间是函数定义。
end_module sample.match.calculate
begin_module sample.math.calculate2
中间是函数定义。
end_module sample.match.calculate2
这样,我们就可以在一段文本协议中,同时传输多个源代码模块。
13. Faramita语言中的函数链接
Faramita中的函数调用,限制非常严格。除了最基本的系统函数之外,所有的函数调用都必须通过context定义或者参数传递进来。哪怕是本模块的函数也是如此。这是为了更加清晰地表达函数之间的链接关系。比如。
begin_module sample.math.calculate
begin_funciton count
begin_context
list
end_context
begin_code
count = 0
for item in list
count = count + 1
end_for
return count
end_code
end_function count
begin_funciton f
begin_context
count = sample.math.calculate.count
list
end_context
begin_code
if count(list) > 0
doSomething
else
return 0
end_if
end_code
end_function f
end_module sample.match.calculate
我们可以看到,函数f为了调用本模块中的count函数,必须在context中明确引入count函数的全名——模块名加上函数名。这样的定义虽然麻烦,但是使用起来却十分灵活。比如,我们可以这么调用函数f。
f(list=[1,3,5])
f(list=[2,7,8], count = anotherModule.count)
anotherModule.count是另外一个统计函数,有可能只统计奇数或偶数,也有可能只统计非零的数值。这就可以任意替换。
14. Faramita函数的打包发送
有了如上所述的语法元素,我们就可以把Faramita函数打包起来,发送到Web Service服务器端执行了。
比如,远端Web Service服务器提供了这样一组基本函数,service.sum, service.count, service.filter等。
客户端可以组织这么一段代码。
begin_module client
begin complex_query
begin_context
sum = service.sum, count = service.count, filter = service.filter, serviceContext,
myFilter = client.filter_out_0
end_context
begin_code
if sum(serviceContext) = 0
return 0
list1 = serviceContext.list1
quantity = count(myList=list1)
if(quantity < 100)
return list1
return filter(myList=list1, myFilter=myFilter)
// 当参数名和值相等的时候,可以省略赋值
// 可以写成 return filter(myList = list1, myFilter)
end_code
end_complex_query
begin_function filter_out_0
begin_context
myList
end_context
begin_code
result = []
for item in myList
if item = 0
continue
result.add(item)
end_for
return result
end_code
end_function filter_out_0
end_module client
这段代码会通过一个远程调用函数发送到Web Service服务器的某个服务端口。
RPC(intialContext = myContext, queryFunction = client.complex_query,
port = 80, host=”example.com”)
这个时候,complex_query就会被打包起来,发送到example.com的80端口。解析器会检查complex_query函数中引入的函数链接。如果解析器发现complex_query引用了客户端本地的函数的话,解析器就会把对应模块的对应函数一起打包进来,直到把所有相关函数全部打包进来。比如,上面的例子中,filter_out_0这个函数就会被打包进来。
因此,程序员一定要注意,发送到远程服务器的函数调用关系不要太复杂,否则会传输太多的代码。
对于这一行,
sum = service.sum, count = service.count, filter = service.filter
由于解析器在客户端本地找不到对应的函数定义,只会把service.sum这些值当做字符串保存起来,直到真正运行起来之后,才回去试图寻找对应的函数定义。那就是发生在服务器端了。这些函数名称的定义,可以放在函数的context定义中,也可以放在RPC函数的initialContext定义中。
远程服务器解析执行客户端脚本的时候,context的赋值过程如下。首先,先把intialContext里面的内容都覆盖到context中,产生一个新的context。然后,服务器端会把serviceContext也覆盖到context中,然后再试图执行client.complex_query。
因此,serviceContext中的定义优先级最好,其次是initalContext,最后是函数context中的缺省初始定义。
15. 远程调用的文本传输协议
SOAP的XML格式使用用来表达树形数据结构,却不适合用来表示代码。我们可以采用灵活的文本协议方式。当然,这就是底层协议层的工作了。不过,简单的文本协议,人读起来也更容易看懂。
当远程服务调用只有一个参数的时候,可以用REST风格。比如前面的例子。
当远程服务调用有多个简单参数的话,就可以直接采用Faramita代码脚本调用的方式。比如下面的文本。
The Faramita request:
POST /InStock HTTP/1.1
Host: www.example.org
Content-Type: application/soap+xml; charset=utf-8
Content-Length: nnn
service.GetStockPrice(StockName=IBM, month=July)
当initialContext参数具有复杂的树形结构时(这种情况很少见),我们还是需要用SOAP的XML格式来表达树形数据结构。当然,具体的代码调用部分,不管是直接调用服务器端的远程服务,还是把打包好的客户端脚本传过去,我们都可以用最直观的脚本语言来表示。这样,整个传输协议的文本的可读性就会高了很多。
具体的文本协议,本文暂不讨论。总之,遵守两个原则。人读起来要方便,计算机读起来也要方便。
16. Faramita语言中的运行期类型检查
Web Service是松耦合协议,如果用Java、C#等静态类型语言来实现的话,必须用到代码生成。如果用Python、Ruby、Javascript等动态类型语言来实现的话,就会简单了许多。
静态类型语言虽然在处理松耦合协议方面比较麻烦,但也有它的好处,那就是生成的代码能够强制类型匹配。
为了引入类型检查这个功能,Faramita允许函数context定义中包含类型检查信息。比如。
begin_function f
begin_context
number : // 必须单独成行
x, y, z
end_context
begin_code
return x + 2*y + 3*z
end_code
end_funciton f
在程序运行的时候,当函数f被调用的时候,解析器就会根据number: 这个类型定义信息,对传进来的参数进行类型检查。多种类型定义也是类似。比如。
begin_context
anything, something, // 不需要类型检查的变量
number :
x, y, z = 10,
w = 20,
string:
a, b, c,
function:
f1, f2
var:
otherting // var表示任意类型,即不需要类型检查
end_context
17. Faramita语言中的AOP
动态语言实现AOP十分简单,比如Ruby的mixin,Python、Javascript的运行期修改类型定义,等等。
Faramita中实现AOP尤其简单。因为Faramita是函数式编程语言,函数对象可以看作是只有一个方法的对象。我们知道,AOP的主要实现模式就是Proxy,需要包装对象的所有方法。函数对象只有一个方法,包装起来就尤其简单。而且,Faramita中所有的函数调用都是在Context中设置的,可以任意替换。由于这些优势,在Faramita中实现AOP非常轻松。
同时,Faramita还和Lisp一样,直接把解析器Interpreter内部的接口暴露出来,提供了Hook钩子函数定义,可以让程序员自己修改解析器的行为。这样的话,AOP实现起来,就更加简单了。
18. Faramita语言中的string
string是编程语言中最常见、最重要的数据类型,同样也是Faramita语言中的重要类型。
Faramita中的string,包含两个主要部分,一部分是byte[],一部分是enconding charset(编码)。Faramita中,string的缺省初始编码方式是utf-8。程序员可以自行定义每一个string对象的charset,也可以通过解析器Hook函数统一替换所有string的编码方式。
Faramita中的变量名和ruby的symbol、ErLang的atom类似。在Faramita中,字符串变量的赋值约定如下。
a = ‘1’
a = ‘bcd’
这些语句都是字符串赋值语句。
当bcd不是任何一个变量名的时候,我们也可以这样写。
a = bcd // 省略了引号
当字符串中含有 . , 之类的特殊字符时,最好用引号保护起来。假如字符串本身就含有单引号、双引号、逗号、换行符、斜杠这样的特殊字符,那么,最好用字符串常量定义的方法。
begin_text contant_str
this is
some , very , very complex
text, here….
end_text contant_str
Faramita中的string和python一样,都是不能改变的常量。每一次新的改动操作,都会生成一个新的string对象。
19. string中的变量替换
这里先提一下我以前做的一个叫做fastm的开源项目。有些网友给我发信息,问我fastm的现状。我感到十分歉意和汗颜。这里给出一个交代。
fastm是一个java语言编写的字符串模板工具,用做Java MVC Web中的View层。
在JSP、Velocity、Freemarker、PHP、ASP中,使用模式是
Data + Script = Text
在fastm中,使用模式是
Data + Template = Text
即,在fastm template中,不含有任何脚本逻辑,只包含占位符。
我在fastm中实现了很多特性,甚至提供了DOM、SAX和动态数据直接绑定的功能。做完了那一步之后,fastm就没有更多的可以升级的空间了。那时候,我明显地感觉到,Java这种静态类型语言用来开发Web这种松耦合应用,实在是不适合。为了实现对象属性数据的动态寻址,我大量地使用蹩脚的Reflection。而在动态语言中,这是轻而易举的事。
从那时起,我基本上就放弃了Java开发,而转去研究其他动态语言。比如,Python、Ruby、ErLang、Javascript等。在试用的过程中,我发现,每种语言都有各自的优缺点,都有各自对应的领域。Web Service、SOA这些buzz word又开始兴起。于是,我萌生了一种模糊的想法,那就是Faramita的雏形,集成了我想要的功能。当想法稍微成熟一些之后,我就写了出来。
Java中,有一个Message Text,可以简单地替换变量。还有些语言,支持字符串的通配符替换。还有些重量级的Template引擎,比如Velocity、Freemarker都可以用script来生成字符串。我想做一个简单的工作,把fastm的思想,引入faramita的字符串解析处理中。比如,我们可以这么定义字符串模板。
begin_text template1
hello, this is {$name}, please repeat following words {$n} times.
{begin_block: block1}war, {end_block: block1}
end_text template1
在这种模板中,只有两个动态部分定义。一个是变量占位符,一个块定义。写起来非常简单,使用起来也非常简单。只要把一个数据块和这个模板一匹配,最终结果就会出来。
20. Faramita解析器的实现
Faramita可以有多种运行方式。可以作为独立语言运行,也可以作为嵌入式语言运行。初步设想中,Faramita的宿主语言是Python和Ruby。Faramita和宿主语言之间,应该可以进行无缝集成,自由交互。这样的话,很多现成的复杂功能,都可以让宿主语言去做。
Faramita的语法定义比较简单,尤其在module、function、context、code级别,都提供了begin end成对的命名块。而且这些语法元素都是单独成行,更加容易处理。但是,到了具体的函数体内部的语句级别,就需要用到复杂的解析器了。
Faramita的解析器应该如何实现呢?可以采用语法定义生成静态的语法编译器。也可以采用ajoo编写的Haskell Parsec在多种语言上的移植。ajoo编写了JParsec、NParsec、Ruby Parsec,可惜,还没有编写Python Parsec。
Faramita的解析器实现起来不会太难,执行器实现起来也不会太难。Java中实现了Groovy、Javascript等动态解释语言的解析和执行,看了一下实现代码,基本上就是定义了一些对应的包装类。在Python、Ruby中,做到这一点更加容易。当然,也不会太简单,因为字符串解析工作,总是很繁琐很麻烦的工作。
也许等我方方面面都考虑清楚了之后,我会动手实现这个语言。我现在只有一个初步的还没有完全成熟的想法,先写在这里是为了吸取更多的意见。