[language] Faramita语言草案0.3(民科。不喜民科者慎入,喜棒打民科者乱入)
buaawhl
2011-09-17
好久没来了。先发一篇又臭又长的民科文,供大家批判和娱乐。
Faramita语言草案0.3(民科。不喜民科者慎入,喜棒打民科者乱入) 声明: 本文介绍了一种叫做Faramita的语言的设计方案。Faramita是作者自己拍脑袋瓜想出来玩的,属于玩票性质,没有严格的BNF规范定义,也没有任何实现方案。 随着各种语言的层出不穷和蓬勃发展,越来越多的普通程序员也开始勇于表达自己打算设计语言的想法。懂得谦逊的程序员会称自己设计的语言为DSL(领域专用语言)。平日不喜玩水、不知深浅的程序员就会直愣愣地称自己设计的是一门语言。没有特别说明,那就是一门通用语言。这类人经常会被冠以一个时兴的名词——民科。即民间科学家的简称。 民科的一个含义就是,专业基础和科学素养不够,只是自己瞎琢磨,写一些自以为是、实则漏洞摆出的科学理论。 专业素养不谈,我对于民科的精神是非常景仰的。他们不拿工资,没有项目基金,没有国家拨款,只凭着一腔对科学的热爱之情,就孜孜不倦地研究下去。我这里借用了民科的桂冠,一是指我的专业素养接近于民科的水平,二是为了表达对民科精神的向往和追求(这种精神我现在还不具备,尽管我已经具备了民科的素养)。 本文叫做草案0.3,那么,前面肯定还有0.1和0.2,我之前已经发过了。只不过,那两篇草案中很多想法都粗陋得我自己都看不过去,这里就不引出来了。在本文中,我会把0.1和0.2中经得起时间考验的部分再写一遍。 声明就到这里,以下是正文。 1. 为什么叫Faramita这个名字? 梵语波罗密特(Faramita)的意思是“到达彼岸”或“做事成功”。传说当时外国商人乘船远涉重洋,饱经大海风浪之险,每当他们达到“黄木之湾”的扶胥港,遥见山上的南海神庙时,不禁发出“Faramita”的欢呼! (引自陈柏坚《广州是中国经久不衰的外贸港市》) Faramita是彼岸的意思。开头的三个字母“Far”正好表示“远”的意思。Faramita这个名字非常适合表达这门编程语言的设计初衷——这门语言设计出来的目的就是要迁移到远程服务器端运行。 Faramita的目标就是真正的可移动的代码(Mobile Code)。 我在网上搜索了一下Faramita这个词,发现还没有一门叫做Faramita的编程语言,于是就选用了这个名字。 Faramita这门语言还不存在,还属于设想和草创阶段。为了避免引起“不自量力”的大惊小怪,这里先要做一个免责声明。这年头,DSL思想已经深入人心,随便一个人设计一门语言,真的不是什么大逆不道、不自量力的事情。 当然,设计语言是一回事,实现又是另外一回事。大部分头脑一时发热的语言设计者,都不具备实现一门编程语言的能力。比如,我就不具备这门能力。我做过语法解析的工作,我知道那工作有多难。更别说还要把一门语言运行起来。 好了,废话少说,言归正传。虽然Faramita这门语言距离真正实现还遥遥无期,但我还是想把这门语言的设计理念先写出来。 2. 数据库服务模式的启发 Faramita语言的典型应用场景是这样的。客户端把一段代码发到服务器端,服务器端接收到客户端发来的代码,解释执行,然后把结果返回给客户端。 我所知道的,采用这种使用模式的语言有两种,一种是关系数据库查询语言SQL,一种XML数据库查询语言XQuery。我不熟悉XQuery,就拿SQL做例子。 数据库客户端拼装一段SQL代码,发到数据库服务器,数据库服务器解释执行SQL,把结果返回给客户端。 我用JDBC编写程序的时候,曾经遇到过很多次这样的问题——我们需要根据前一条SQL的结果,来决定下一条SQL是什么。 在一般的编程模式中,客户程序先发一条SQL,拿到结果,然后再发下一条SQL。 有些数据库支持动态临时存储过程,这时候,为了减少查询次数,提高运行效率,客户端可以动态拼装一段存储过程,里面包含SQL以及并不复杂的判断逻辑。客户端把拼装的存储过程发给数据库,数据库就可以执行存储过程,直接返回最终结果。 从理论上来说,存储过程应该在数据库中定义,然后在客户端调用。但是,这种临时拼装存储过程的做法有它的灵活性和应用场景。 Faramita语言的工作方式就有些类似于把SQL和存储过程动态组装好之后发到服务器执行的种模式。 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例子可以看到,在远程调用中,为了匹配参数,函数名的参数都是带着名字的。在现有的解释语言中,有不少语言都可以用直接或者间接的方式实现命名参数,其中命名参数支持得比较好的语言有Python,另外,比如Ruby、ErLang等语言也可以用间接的方式支持命名参数。但是,在这些语言中,命名参数只是类似于语法糖的地位,并非语言中的核心概念,在参数包装方面总是会麻烦一些。 三是不支持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(闭包),也请读者到网上自行寻找很多简单易懂的例子。 简单来说,Closure(闭包)就是函数体内部的匿名函数。 6. Faramita语言的设计概要 Faramita的语法特性借鉴了Haskell、ErLang、Javascript、Python等语言的特性。 Faramita是一门函数式编程语言,遵循函数式语言的基本特性——变量只能赋值一次。 元组(Tuple)是函数式编程语言中的核心数据类型,结构上类似于定长数组。这里不赘述。 Faramita的核心数据类型是命名元组(Named Tuple)。其概念类似于ErLang的record宏和Haskell的带有属性名字的自定义Tuple类型。 在ErLang和Haskell中,这种可以用名字来进行元素访问的Tuple类型叫做Tagged Tuple(贴了标签的元组)。在ErLang和Haskell中,这种Tagged Tuple只是一种针对编译器的语法糖,并不具有运行期的特征。你无法用Reflection或者Introspection机制在运行期查看该Tuple内部的某个元素名对应的数据。 Faramita的Named Tuple则不同。在Faramita中,所有的Tuple都必须是Named Tuple,而且,程序员可以用Reflection或者Introspection机制在运行期查看Named Tuple数据内部的元素名。 Faramita要求Tuple中所有的元素都必须有元素名。而且,Named Tuple支持Curry,即支持部分元素赋值。由于有了元素名,因此不要求部分赋值的元素顺序。 从这个角度来说,Faramita的Named Tuple的特性非常接近于Javascript Object和Java的HashMap,只是Faramita的Named Tuple只能赋值一次,而不能多次赋值,每次部分元素赋值之后,就会产生一个新的Named Tuple。比如: t1 = (a, b) 这个定义中,只定义了Named Tuple的元素名,没有定义任何数据,允许定义“空值”(Null)的元素。 t2 = (a = 10, b) t3 = (a = 10, b = 1) t4 = (a, b = 1) 上述的定义都是合法的。下面的部分复制的定义也是合法的。 t5 = t2(b = 1) // 得到的结果就是 (a = 10, b = 1) Named Tuple还支持多个元素的批量复制。比如, t6 = (a = 1) (b = 2) 相当于把(b = 2)这个元组的内容复制到(a = 1)这个元组中,得到一个新的元组(a = 1, b = 2)。 t7 = (a = 1, b = 3) (b = 2) 相当于把(b = 2)这个元组的内容复制到(a = 1, b = 3)这个元组中,得到一个新的元组(a = 1, b = 2)。 我们还可以这么写。 t8 = (a = 1, b = 3) t9 = t8(b = 2) 我们甚至还可以这么写。 t10 = (a = 1, b = 3) t11 = (b = 2) t12 = t10(t11) 这一步的括号()中没有写元素名,即表明,参数t11本身是一个Tuple,需要整个复制到t10元组中。如果不是这种情况,解释器就会报错。 从上面的例子可以看出,Named Tuple和Java HashMap是多么的相像。Tuple之间的元素复制,和HashMap中的元素复制是多么的相像。 7. Faramita函数定义 Faramita函数只接受一个参数,而且只接受一种参数类型——Named Tuple。Named Tuple中的元素名就相当于函数的参数名。 Faramita的函数定义形式与Haskell完全相同,都是用 = 来定义函数。在定义函数体方面,Faramita和Haskell有些不同。 在Haskell中,函数体中只有一个表达式。其他的变量函数定义都在let…in或者where结构中。在Faramita中,函数体中可以包括一连串的顺序定义,只要保证最后一条语句是一个表达式就行。 在一般情况下,Faramita函数同Haskell函数一样,都是Lazy的。 Faramita函数只执行最后一个表达式。当遇到表达式中的变量名时,才到前面的定义中去寻找对应的变量名定义。Faramita函数相当于把Haskell函数中的let…in中的定义直接放到函数体当中了。 Faramita的函数体的形式同Python一样,通过空格来表示代码的层次结构,看起来好像支持顺序语句,实际上只是执行最后一个表达式,前面的语句全都是定义。 Faramita函数支持缺省参数值,即支持某个元素的缺省值。缺省参数定义出现在第一行,而且后面不能跟着函数体。比如: factorial( N = 20 ) factorial( N = 1 ) = 1 factorial( N) when N > 1 = N * f (N = N – 1) 在上述的函数定义中,第一行没有函数体,N = 20 就表示缺省参数。后面的行有函数体,N = 1 就表示模式匹配,表示当 N = 1的情况下,进入该函数体分支。可以看到,Faramita对模式匹配的支持和Haskell是基本一致的。另外,Faramita也支持类似于Haskell的Guard,比如代码中的when N > 1。其余的语法,如if, case of都类似于Haskell。 需要特别说明的是 factorial( N) = N * f (N = N – 1) 这个函数定义分支。 在这个定义分支中,有一个 f(N = N – 1)的调用。这里的两个N的作用域是不同的。= 左边的N表示元素名,而 = 后面的N表示当前运行环境中的变量,即factorial(N)的参数值。 为了避免混淆,以上的函数定义可以写成这样。 factorial( N = 20 ) factorial( N = 1 ) = 1 factorial( N) when N > 1 = M = N - 1 N * f (N = M) 关于参数元素名同名,还有这样一种情况。 f(N) = 2 * g(N = N) 在g(N = N)这个函数调用中,=左边的N表示元素名,=右边的N则表示当前运行环境中的变量,即f (N)的参数值。 这种特殊情况下,函数定义可以简写为 f(N) = 2 * g(N) 8. Faramita函数的Curry特性 Faramita函数的Curry特性是建立在Named Tuple的Curry特性上的,因此,比Haskell的Curry更进了一步,不需要遵守部分赋值的顺序。 假设有这么一个函数定义。 f(a, b, c = 10, d) f(a = 1, b, c, d) = b f(a = 2, b, c, d) = c f(a = 3, b, c, d) = d 在这个函数定义中,第一行没有函数体,定义了参数c的缺省值。后面的三行是三个函数定义分支。其中的a= 1, a= 2, a = 3都是表示模式匹配的等式。 我们可以这么调用f。 f(a = 2, b = 50, d = 20) 由于c已经有了缺省值10,f函数参数的所有元素名都已经有了值,f函数就可以执行,得到的结果就是10。 我们还可以部分赋值。 f(a = 1, d = 50) 这时候,f函数参数的元素名b还没有值,这时候,返回的结果就是另一个部分赋值的函数,而不是最终的结果。 为了更明晰,我们可以这么写。 g = f(a = 3, d = 50) 那么 g(b = 30) 的结果,就是30。 如果我们这么写。 g(c = 60),那么得到的结果还是一个部分赋值的函数,只不过是把c元素值替换掉的一个新的函数。 同Named Tuple的复制特性一样,函数也支持类似的特性。 g = f(a = 1)(b = 2, c = 3) Faramita解释器先把(a = 1)和(b = 2, c = 3)这两个元组合并在一起,再和f函数结合。 还可以这么写。 t1 = (b = 2, c = 3) f1 = f(a = 1) g = f1(t1) 这里,要注意f1(t1)的调用形式。当t1是函数体内的一个元组类型的局部变量名的时候,f1(t1)就表示把t1中的元素都复制到f1的参数中。当t1是函数参数的时候,f1(t1)就表示一个f1(t1 = t1)的调用,即元素名为t1,传给元素名的值也是t1。 为了避免引入过多的关键字,很多情况下,Faramita只是使用一些形式上的约定,而不是引入新的关键字。 从上面的例子,我们可以看出,Faramita中,不仅Named Tuple表现得很像Javascript Object,函数也表现得非常像Javascript Object。 9. Faramita的数据类型 同其他函数式编程语言一样,Faramita支持List类型,其构成基础也是二元组。不过,在Faramita中,二元组也是命名的二元组。Faramita的List二元组是一个特殊的二元组,定义为(head, tail)。即,有两个元素,第一个元素名为head,第二个元素名为tail。 Faramita的List表达方式基本类同于Haskell。只是在二元组形式下的模式匹配式,不可以使用 _ 这样的通配符,必须显式写出 head 和 tail 这两个元素名。 为了效率,Faramita支持string类型,其内容为一个byte[]和一个编码名(charset)。为了支持string类,Faramita支持定长数组类型,可以用下标访问某一个数组中的某一个元素。 从语义上来说,定长数组类型和元组类型有重复之嫌。不过,Faramita的元组不同,是有名字的,更类似于HashMap。所以,引入定长数组类型也没什么。 数组的元素类型,目前只有一种,就是byte。对应的数组类型就是byte_array(即byte[]) 在我目前的想法中,Faramita只允许程序通过两种方式获取数组类型的数据,不允许程序员自己创建数组类型的数据。一种是通过IO函数获取,一种是从string数据中获取。 string类型的数据,可以通过IO函数获取,也可以在资源文件中定义。在Faramita中,不允许直接在代码中定义string类型的数据。 在Faramita中,string类型的数据,不允许直接定义在代码模块中,必须定义另外的一种特殊的资源文件中。因为一切字符串资源都可能涉及到编码。字符串资源文件和Faramita代码虽然不可以写在同一个模块文件中,但Faramita代码可以通过字符串资源模块的引入,非常轻松使用字符串资源。 字符串资源的编码指定,Faramita提供一套灵活的控制机制。可以单独定义某个资源文件的编码,也可以定义一批资源文件的编码。在Faramita中,string是可以自定义编码类型的,所有的string数据都带有charset属性。这一点和Java不同。在JVM中,String的charset都是unicode,因此,java的String对象不需要charset这个属性。 Faramita的基本类型包括bool、number、integer、float、byte等。数组类型包括char_array、byte_array。特殊的复合类型包括string,就是一种特殊的Named Tuple,其中含有两个元素名,charset和bytes。 Faramita的自定义类型包括两种——Named Tuple和函数。 Named Tuple不用说了,和Javascript Object一样,随时定义随时使用。 函数定义需要说明一下。Faramita函数定义支持类型声明和检查。比如: f :: (a :: integer, b:: integer, c :: integer, d :: integer) f(a, b, c = 10, d) f(a = 1, b, c, d) = b f(a = 2, b, c, d) = c f(a = 3, b, c, d) = d 或者 f :: (a, b, c, d :: integer) f(a, b, c = 10, d) f(a = 1, b, c, d) = b f(a = 2, b, c, d) = c f(a = 3, b, c, d) = d 有了这些类型定义之后,Faramita解释器就会进行类型检查。没有类型定义,就不检查类型,也不进行类型推断,这点和Haskell不一样。所以,Faramita是动态类型语言。 同Haskell一样,Faramita支持类型参数(抽象类型,类似于泛型)。 有了Named Tuple和函数,Faramita既可以享有函数式编程的便利,也可以达到面向对象编程的效果。比如: contruct_object(a, b, c, d) = multipleA ( N ) = a * N addB ( N ) = b + N minusC (N)= N - c divided (N) = N / d (getA, getB, getC, getD) 这个函数就相当于一个构造函数。返回一个元组,其中的元素是四个函数,提供四种操作。调用方法如下。 myObj = construct_object(4, 6, 8, 2) i = myObj.multipleA( 15) // 结果是60 j = myObj.divideD( 20) // 结果是 10 可以看到,除了不能更改对象状态之外,Faramita完全可以达到面向对象编程的效果。 10. I/O,顺序,strict 同Haskell一样,Faramita是lazy的,函数体中,最后一个表达式之前的所有定义语句都是没有执行顺序的。 Haskell用 !、$、seq等strict标记,用来强制某个数据或者函数成为strict的,即必须执行完毕,不能偷懒。 同时,Haskell引入了Monad和Action的概念,模拟出了顺序执行的语义,从而可以处理IO。 Faramita该如何做呢? Faramita引入了一个关键字strict。这个关键字是加在函数定义前面的。如果函数定义前面加了strict,那么,该函数立刻就变成和ErLang的函数定义一样,从头到尾依次执行,而且,每条语句都必须执行完毕,这就满足了IO要求的语义。比如: f(1) = 1 f(N) = i = N – 2 j = i * 3 - 5 4 * j 本来是lazy的。最后一条语句之前的两条语句顺序可以随便。可以写成: f(1) = 1 f(N) = j = i * 3 - 5 i = N – 2 4 * j 如果加上了strict,上述的语句必须按照顺序写。 strict f(1) = 1 f(N) = i = N – 2 j = i * 3 - 5 4 * j 如果写成这样。 strict f(1) = 1 f(N) = j = i * 3 - 5 i = N – 2 4 * j 就错了。因为,j 用到了 i,必须写在 i 定义的后面。 在lazy函数中,无限列表结构无需执行完毕,因此不会引起死循环。而在strict函数中,无限列表结构需要执行完毕,就会引起死循环。因此,不应在strict函数中执行无限列表结构。 Faramita的IO通过IO原语来实现。 Faramita支持两种IO原语,一种叫做同步IO原语,一种叫做异步IO原语。 同步IO原语就是调用IO之后,一直等到结果。 异步IO原语就是调用IO之后,不等结果,继续执行。这就需要在异步IO原语的参数中设置一个Receiver,用来接受异步消息。 IO原语的形式和函数相同。 同步IO原语的形式是这样。 sync_io(protocol) protocol(即协议)参数是一个Named Tuple,其中包括协议名、资源定位信息、Faramita移动代码等信息。 异步IO原语的形式是这样。 async_io(protocol, receiver) protocol(即协议)参数含义与同步IO原语同。receiver是一个函数类型的参数。Receiver函数可以任意定义。比如: f( a, b) = save(content = a + b) async_io(protocol = …, receiver = f) Faramita解释器会根据函数f的函数签名,把获取到的数据流解析为a和b两个参数,传给f函数,并执行f函数。 Web应用的响应程序也可以类似定义。 reply(x, y, z) = x + y + z 这个reply函数可以配置到web网站中,响应某个url。Faramita解释器会根据函数reply的函数签名,将request参数解析成x, y, z传给reply并调用reply,然后把reply的结果写入到response中。 Faramita中所有的IO函数,包括文件、网络、消息队列服务、屏幕、打印机等等,全都是建立在IO原语上的。比如,前面的save(content)函数,也是建立在IO原语上的IO函数。 所有调用到(无论是直接调用还是间接调用)IO原语的代码,都必须出现在strict函数中。因为只有strict函数才能保证顺序,才能保证IO的正确执行。 Faramita是一门高级语言,甚至高级到了DSL的程度。Faramita没有通用语言那么多的底层功能。Faramita不允许程序员对进程、线程进行创建和调度,也不允许程序员接触到IO设备。一切线程调度、外部IO的需求,都必须通过IO原语来完成。一切细节都埋在protocol(协议)里面。程序员不可以用Faramita实现一个Web Server。因为Faramita的长处根本就不在这个方面。这些底层服务端的实现,都只能由Faramita解释器(虚拟机)来提供。 整个Faramita解释器(虚拟机)就相当于一个由protocol定义的命名服务。当一个函数模块A通过IO原语的protocol,寻找另一个函数模块B时,那么,B的位置对于A来说,是透明的。无论B是在本解释器(虚拟机)中,还是在千里之外的某个网站上,对于A来说,都是一样的。区别只在于protocol中的服务定位上。 这也是Faramita的设计目的。Faramita代码可以任意在互联网上迁移,从而可以更好地支持分布式计算,更好地利用互联网上的空闲计算资源。 Faramita是解释语言,在单机上的效率肯定是比不上编译语言的。但是,在互联网范围内的大规模分布式计算上,却可能更有效率。 11. Faramita模块 Faramita模块(module)的定义形式如下。 begin_module net.db.mymodule (io = sys.io, userlib = com.user.lib) export(…..) read_file = io.read_file strict f(N) = N + read_file(“config.txt”) end_module net.db.mymodule 在上述的模块定义中,mymodule是当前的文件名。mymodule.far。 net.db是package name。表示目录结构net/db。类似于java的package。 模块定义由begin_module和end_module包起来。后面都必须跟完整的module名字。这是为了打包方便。因为,faramita代码发送到时候,是以Module为单位发送的。 (io = sys.io, userlib = com.user.lib) 这一行,表示引用其他的module。sys.io和com.user.lib都是完整module名。 其作用类似于import。为什么不用import,而采用这种类似于参数定义的形式呢? 这是为了可以替换引入模块的实现,从而可以方便地实现AOP等功能。 Faramita支持模块的调用,类似于函数的调用。比如: m1 = net.db.mymodule m2 = m1(io = mypackage.io) 这就用mypackage.io替换了sys.io的实现。 m2.f 调用的就是 mypackage.io 模块中的read_file实现。 export部分表示公开哪些函数,以便其他模块可以引用。这部分定义没什么特殊的,参照Haskell和ErLang。区别在于,ErLang的函数签名中带有参数个数,Haskell的函数签名中带有参数类型,Faramita的函数签名中带有参数名,也可以带有参数类型。 12. 资源模块 Faramita的资源模块就是字符串资源模块,或者说,文本资源模块。其定义很简单。比如,对应net.db.mymodule模块(\net\db\mymodule.far文件)的资源模块文件就是 \net\db\mymodule.rsc 其内容为 begin_resouce net.db.mymodule str1 = hello str2 = hello world begin xml1 <hi> <hey> something <hey> <hi> end xml1 end_resource net.db.mymodule net.db.mymodule模块中的代码可以直接引用这个资源文件中定义的任何字符串常数。 如果是单行字符串,首尾没有空格的,可以直接用 = 来定义。如上例。如果超过一行,那么就用begin和end来包装。同样,begin和end后面要跟着资源的名称。 资源模块中的所有字符串资源都是公开的,不需要export。其他资源模块也可以引用这个资源模块。其引用方法和引入代码模块是一样的。 块。 begin_resource test1 (codeModule = net.db.mymodule) a = “<head>” + textModule.xml1 + “</head>” end_resouce test1 注意,资源模块只能引入资源模块。资源模块中的字符串定义只能是字符串定义或者简单的字符串拼装定义。 如果是复杂的字符串生成函数,就只能使用代码模块来定义了。 资源模块虽然可能缺省对应一个代码模块。但是,其他的代码模块,也可以引入这个资源模块,比如: begin_module test2 (codeModule = net.db.mymodule) end_module test2 这种引用方法就把net.db.mymodule的代码模块和字符串资源一起引用进来了。如果某个代码模块只想引用资源模块。那么,只能把资源模块定义为独立的资源模块,不对应任何一个代码模块,这样就可以避免引入代码模块。 资源模块的编码设置,资源模块的加密和权限设置,代码模块的加密和权限设置,都有专门的配置方式。配置文件可以跟代码和资源在一起,也可以集中放到一个地方用模式匹配的方式批量配置。 12. Faramita AOP 为了运行效率,目前的AOP实现都不同程度上采用了代码生成技术,即把重复代码回填到需要铺设AOP的方法定义前后。 函数式语言中,每个函数对象只有一个方法,用代理模式来包装,更加容易。从重复代码(日志、事务等)的回填位置的声明上来说,回填位置定义中的通配符,等同于函数式编程中的模式匹配(Pattern Match)。从这两个方面看,函数式语言更有利于实现AOP。 Faramita可以很容易引入AOP特性。用函数的形式来表现。 weave(interceptors, locations) 具体例子就是: weave (interceptors = [LogInterceptor, TransationInterceptor], locations = [ com.*, net.db.*, - net.db.test.* ] ) 这条语句的含义就是,把[LogInterceptor, TransationInterceptor]这个列表里面的所有拦截器,都织入到[ com.*, net.db.* ]这些位置的所有方法里面。 net.db.test.*前面加了一个负号(-)。这就表示,把net.db.test.*里面的所有方法都排除出去。 由于Faramita是解释语言,不需要用XML之类的AOP声明定义。直接调用上述的AOP函数API接口就可以了。weave函数的作用就是把重复代码铺设到locations定义的函数前后。 具体做法就是代码生成。生成新的模块,每个模块都用proxy模式包装对应的函数,生成同名函数,并把interceptors插入到原始函数的前后。 然后,Faramita解释器对程序入口点调用的时候,首先产生一套新的proxy模块,然后用这套新生成的proxy模块代替用户原来的模块进行执行,就实现了AOP的功能。 13. Faramita应用场景 Faramita应用场景包括但不限于前面所讲的Web Service和DB Query。 著名的网页设计软件dreamweaver的用户界面具有很高的可定制性。其菜单定义用XML描述,其菜单响应程序用Javascript编写。我非常喜欢这种模式,尽管这种模式的运行速度比较慢。 我希望,Faramita也能支持这样的图形用户界面开发。菜单资源就用XML定义,代码逻辑就用Faramita实现。 我最欣赏的用户界面是电子表格。我希望,Faramita可以用来开发电子表格。Javascript都可以做到在线电子表格,同为解释语言的Faramita应该也能做到。 我理想中的电子表格存储格式是这样的。内容、结构、格式、图片资源、代码全都分开。 良好的HTML设计中,内容和结构(HTML文本)、格式(CSS)、图片资源、代码(Javascript)全都是分开的。但是,内容和结构还是在一起的。HTML元素结构和其中的文本资源混在了一起。这个很难分开。一旦分开,HTML存在的基础就没了。 在Faramita电子表格设计中,内容(txt)、结构(文本块定义)、位置(单元格的二维坐标和大小)、格式(style)、图片资源、代码(Faramita)全都分开。 Faramita电子表格的内容和结构分开,采用的是Flyweight设计模式。比如,我们有这么一段文本。 Long long ago, there was living a king…….. blabla 要这段文本划分成一个树形结构,可以在里面添加xml元素定义。但是,Faramita电子表格不这么做。Faramita采用Flyweight模式,另外定义了一套用标志文本位置的数字,组成一个树形结构。假设那段文本的长度为1000。我们可以这么来划分。 1---1000 1---100 101 --- 500 100---200 201---400 401--500 501---1000 这样就把文本分成了一个树形结构的多块文本。我们可以对这些文本块分别设置格式。当我们想取用某一块文本时,我们需要把位置信息和文本信息组合起来,才能得到。 在读取文本结构并显示的时候,这种设计没什么问题。但是,当编辑文档的时候,这种设计方式就有很大的效率问题。任意插入一个文字,都可能引起整个树形分块的位置信息的变化。因此,上述分块方式,必须采用相对位置的方式。 我的想法是,相对位置(树形结构中的某个相对索引) + 本块文本长度。一些开源的纯文本编辑器中,编辑的文本并没有结构信息,其实现似乎就采用了这种思路。这只是实现细节问题,无关大局。继续往下看。 Faramita电子表格的目标是完全能够代替HTML的功能。Faramita电子表格既能够支持电子表格(Excel)的二维表形式,也可以支持文档(Word)的文章形式。我想了一下,这两种格式是可以统一为同一种格式的。电子表格每一行都只有一列,看起来就是一个多行的信纸样式,再加上适当的文本换行操作,就可以当做文档协作工具来用。 至于布局,电子表格的布局有多么方便多么强大,用过的人都应该很清楚。那远不是HTML所能够比拟的。 关于电子表格的公式,我想发表一下自己的看法。很多电子表格模仿Excel,直接在单元格中写入公式。我不觉得这是好的做法。公式属于代码逻辑部分。我希望,代码和文本结构和位置完全分开。 我设想的结构是 source -> target 的结构。一块电子表格区域作为source,另一块电子表格区域作为 target。中间有一段对应的Faramita代码,读取source数据,生成结果到target中。无论是source还是target,都存放直接数据,没有任何公式。 当source中任何数据变化了,就会触发这段代码,更新target。用户可以编辑target区域中的内容,但是,一旦那段代码运行了,就会产生结果,重新覆盖掉target的内容。所以,最好把target设成只读区域。 这种做法的好处是,不需要把公式复制到target的所有单元格中。坏处就是不那么直观。不过,这个问题可以想办法克服的。 现在来看存储格式的问题。Faramita电子表格不支持excel、word那样的复合文档结构。如同HTML一样,Faramita的文件格式全都是分成一个个的资源文件,文本文件(txt), 结构定义文件,单元格位置定义,Faramita代码文件,图片文件等等,全都是分开的文件。这些文件放在一个目录下,可以打成一个zip包。 有了这样的分而治之的结构,就为文档数据库化和版本控制管理提供了巨大方便。 首先,划分得十分清楚的各种资源可以分门别类放到设计良好的文档数据库中。其次,文本文件的版本管理和各个版本之间的比较,是非常便利的。Faramita可以实现类似于SVN和CVS的版本管理功能,可以对各种资源进行分类版本管理。 这样,整个文档结构就可以设计成基于本地文件、局域网、互联网的文档数据库的具有版本管理功能的电子表格系统。数据共享、数据加密、代码加密、权限管理、版本管理都可以分而治之。 Faramita被设计为能够在互联网上各个资源点(数据库、文档库、文档数据库、版本管理库、电子表格中的source->target转换逻辑,等等)之间任意迁移的可移动代码,正是为了达到这样的目的。 理想中的应用场景是这样的。每个用户面对的都是一个Faramita电子表格界面。通过这个界面,用户可以获取(check out)互联网上任意一个资源点上的文本数据、格式数据、图片资源或者代码数据(这可以通过各种各样的点对点协议和中心服务协议来实现)。 用户可以编辑任何资源,并提交(check in)到对应的资源点(通常是中心服务资源点)。用户提交的资源可能是文本内容,也可能是功能代码,也可能是格式模板,也可能是图片资源,等等。用户可以对资源(无论是什么资源,内容、代码、格式、图片,都行)进行加密,并提供公钥,从而保障版权和后续权益。 这不仅仅是一个共享的版本控制的电子表格文档数据库系统,而且还可以扩充成基于互联网的应用程序,完全不输于web网站的功能。 web网站应用程序的工作模式是一种同步及时响应模式,是一步一步响应用户的请求,需要在服务端保持大量的状态信息,这严重影响了系统的可扩展性。因为状态是可分布式并行计算的天敌。为了解决这个问题,人们尽量把状态推到客户端,形成了交互功能强大的富客户端。这是一个不错的方法。不过,由于浏览器结构和Javascript安全机制的天生限制,这种功能的加强是有限的。 Faramita电子表格系统的工作模式是一种异步延时提交模式。用户不需要一次次点击网站,一次次获取内容,而是一次可以选取一个内容列表,一次将需要的所有资源列表都下载到本机临时文档数据库上(相当于浏览器的缓存空间)。这就是check out。用户可以一次浏览大量内容,并填进去自己能够填入的所有数据,并定制自己的查询和更新策略(Faramita代码)。然后,用户再把自己的数据和代码一起提交上去(有可能一次提交和访问很多个资源点),然后就不用管了,可以去做别的事情。这些数据和代码将在远端(有可能是分布在互联网上的多个计算资源)运行很久,最后,将最终的运算结果返回到用户端整合起来。这就是真正意义上的的Web Service计算。 这种模式的好处是,尽量减少了远端交互,从而节省了网络资源和计算资源,最大限度地提高了互联网的分布式计算能力。 |