邱俊的空间

Simple is beautiful.
posts - 15, comments - 61, trackbacks - 0, articles - 4
  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理

公告

2009年4月18日

前言

Javascript,有人称其为C+LISP,C只怕是尽人皆知,但是一直活跃在人工智能领域的另一个古老而优美的语言LISP,掌握的恐怕不是很多.这个倒不是因为这个语言太难或者用途不广泛,而是大多数人在接受计算机语言启蒙的时候都走的是图灵机模式,而LISP,做为一种函数式编程语言,是另一个体系:lambda演算体系.这个体系的运算能力跟图灵机的运算能力是相当的。

所以Javascript本身是一种很自由的,支持函数式编程的一个神奇的语言,在WEB中的应用只是它的以个小小的部分。脚本可以用来脚本化很多 东西,最主要的应用是在UI层面,很灵活(这也是Javascript用来脚本化HTML的一个重要原因)。我们这里要说的是一个100%java实现的 javascript引擎rhino,当然重点不是引擎本身,而是在其上解释JavaScript的函数式编程。(rhino可以在此处 http://www.mozilla.org/rhino/找到)。

函数式编程概览

我们先看几个例子,从感官上对其有一个了解,看一个幂计算函数,用命令式语言书写(命令式语言如C,Java等),大概就是下面这个样子:

function expt(b, n){
    
if(n == 0){return 1;}
    
else{
        
return expt(b, n-1)*b;//正常的递归调用
    }
}

再看看函数式编程的写法:

function expt(b, n){
    
if(n == 0){
        
return 1;
    }
else{
        
return mul(expt(b, dec(n)), b);//所有操作均为函数
    }
}

 

可以很明显的看到,有一大堆的括号,没有操作符(如+-*/等),这是因为,操作符在函数式编程中被认为是函数,与其他函数(数学函数,串处理函数等)的地位是同等的,当然这个不是最主要的,在函数式编程中最主要的是函数可以做为一个基本类型被返回,这一点时命令式语言无法完成的。

比如,定义一个函数,输入两个参数,计算这两个数的平方和:

 

function(x, y){ return add( expt(x, 2), expt(y, 2) ); }

你可以将这个函数赋值给一个变量,如下:

 

 

var func = function(x, y){ return add( expt(x, 2), expt(y, 2) ); }

然后,最神奇的是,下边这样:

 

 

func(34);//这个表达式将返回25! 此时的func已经是一个函数了!

好了,简单的概述就此为止,下面我们看一些更高级的主题:高阶函数。

 

高阶函数

事实上,所有的有关函数式编程的文章必须要涉及到这个主题,这是因为,在命令式语言中,我们的抽象是根据"类"(这正是现在流行的OO的基本思想)来进行的,但是,在函数式编程中,没有办法表示类的概念,但是同样可以进行高级的抽象方式,使得一个函数更加泛化,可以被"实例化"成其他的函数,这个就是高阶函数。

来看个例子,我们有这样几种求和运算:

 

function intSum(a, b){
    
function inc(x){ return x + 1; }
    
function identity(x){ return x; }
    
    
if(a > b){
        
return 0;
    }
else{
        
return intSum(inc(a) , b) + identity(a);
    }
}

function cubeSum(a, b){
    
function inc(x){ return x + 1; }
    
function cube(x){ return x * x * x; }
    
if(a > b){
        
return 0;
    }
else{
        
return cubeSum(inc(a) , b) + cube(a);
    }
}

function piSum(a, b){
    
function piTerm(x){ return 1/((x+2)*x); }
    function piNext(x){ return x+4; }
    
    
if(a > b){
        
return 0;
    }
else{
        
return piSum(piNext(a) , b) + piTerm(a);
    }
}

第一个函数用来计算从a-b的数的总和,步长为1,第二个函数计算a-b的立方和,步长为1,第三个函数计算a-b的一个方程的和(将a-b中的每一个数带入此方程进行计算),步长为4.

 

从函数的形式以及函数的作用来看,这三个函数有很大的共性,所以我们考虑是否可以将这些共性抽取出来,将每次计算的步长和方程传入,进行求和计算??答案当然是肯定的,下面我们来抽象:

  1. 定义下一个参与计算的数(通过步长函数的定义)
  2. 定义求什么的和(函数体的定义)

有了这两个函数,我们就可以计算任意的方程,指定区间的求和操作,将上述的两个函数做为参数传入,输出即为运算结果:

 

function sum(term, a, next, b){
    
if(a > b){
        
return 0;
    }
else{
        
return sum(term, next(a), next, b)+term(a);
    }
}

这个函数需要四个参数,一个是关于计算子的定义term,一个是步长函数next,另外两个即为区间的两个端点a,b,这样我们可以重新定义上述的三个函数如下:

 

 

function intSum(a, b){
    
function inc(x){return x + 1;}
    
function identity(x){return x;}

    
return sum(identity, a, inc, b);//调用通用的抽象接口
}

function cubeSum(a, b){
    
function inc(x){return x + 1;}
    
function cube(x){return x * x * x;}

    
return sum(cube, a, inc, b);//调用通用的抽象接口
}

function piSum(a, b){
    
function piTerm(x){ return 1/((x+2)*x); }
    function piNext(x){ return x+4; }

    
return sum(piTerm, a, piNext, b);//调用通用的抽象接口
}

高阶函数提供了更高级的抽象,从而使得程序的结构更加清晰。下面我们再看看函数式语言的优雅的代码,匿名函数:

 

匿名函数

我们先对一些简单的操作进行简单的包装(如四则运算,boolean运算等操作):

function abs(x){ return x>0?x:-x;}
function add(a, b){ return a+b; }
function sub(a, b){ return a-b; }
function mul(a, b){ return a*b; }
function div(a, b){ return a/b; }
function rem(a, b){ return a%b; }
function inc(x){ return x + 1; }
function dec(x){ return x - 1; }
function equal(a, b){ return a==b; }
function great(a, b){ return a>b; }
function less(a, b){ return a<b; }
function negative(x){ return x<0; }
function positive(x){ return x>0; }

然后,在这些共用的语法糖(并非严格意义上的语法糖,但是它们的确是!)的基础上,做一些简单的函数定义:

// n*(n-1)*(n-2)*...*3*2*1
function factorial(n){
    
if(equal(n, 1)){
        
return 1;
    }
else{
        
return mul(n, factorial(dec(n)));
    }
}

//对上边的函数的另一种定义方式
/*
 *  product <- counter * product
 *  counter <- counter + 1
 * 
*/
function factorial(n){
    
function fact_iter(product, counter, max){
        
if(counter > max){
            
return product;
        }
else{
            fact_iter(mul(counter, product), inc(counter), max);
        }
    }

    
return fact_iter(11, n);
}


function expt(b, n){
    
if(n == 0){
        
return 1;
    }
else{
        
return mul(expt(b, dec(n)), b);
    }
}


function gcd(a, b){
    
if(b == 0){
        
return a;
    }
else{
        
return gcd(b, rem(a, b));
    }
}

function search(fx, neg, pos){
    
function closeEnough(x, y){return less( abs( sub(x, y) ), 0.001)};
    
var mid = (function(x, y){return div( add(x, y), 2);})(neg, pos);
    if(closeEnough(neg, pos)){
        
return mid;
    }
else{
        
var test = fx(mid);
        
if(positive(test)){
            
return search(fx, neg, mid);
        }
else if(negative(test)){
            
return search(fx, mid, pos);
        }
else{
            
return mid;
        }
    }
}

function halfIntervalMethod(fx, a, b){
    
var av = fx(a);
    
var bv = fx(b);

    
if(negative(av) && positive(bv)){
        
return search(fx, a, b);
    }
else if(negative(bv) && positive(av)){
        
return search(fx, b, a);
    }
else{
        print(
"error happend!!");
    }
}

//计算一个函数的不动点
function fixedPoint(fx, first){
    
var tolerance = 0.00001;
    
function closeEnough(x, y){return less( abs( sub(x, y) ), tolerance)};
    
function Try(guess){
        
var next = fx(guess);
        
//print(next+" "+guess);
        if(closeEnough(guess, next)){
            
return next;
        }
else{
            
return Try(next);
        }
    };
    
return Try(first);
}

// magic function sqrt, a little hard to read, hah?
function sqrt(x){ 
    
return fixedPoint(
        
function(y){
            
return function(a, b){ return div(add(a, b), 2);}(y, div(x, y)); 
        },
        
1.0); 
}

如果上边的几个都可以完全理解,那么常识看看最下面的这个计算一个函数的平方根的函数sqrt(x),你会发现这个函数很有意思,其中的那个匿名函数最有意思,我写了个测试函数:

 

function funcsTester(){
  //计算两个数的平方和的匿名函数
    
var y = (function(x, y){ return add( expt(x, 2), expt(y, 2) ); })(34);
    print(y);
    print(halfIntervalMethod(sin, 
2.04.0));
    print(halfIntervalMethod(
function(x){return expt(x, 3- mul(2, x) - 3;}, 1.02.0));//x^3-2x-3
    print(fixedPoint(cos, 
1.0));//cos的不动点
    print(fixedPoint(
function(x){return add( sin(x), cos(x)); }, 1.0));
    print(sqrt(
100));
}

运行结果如下:

 

js> funcsTester()
25
3.14111328125
1.89306640625
0.7390822985224024
1.2587315962971173
10
js
>  

好了,关于函数式编程就大概介绍到这里,下面简单说说这个rhino包

 

关于Rhino

rhino是一个纯java的javascript引擎的实现,下载之后,将js.jar加入classpath,然后在命令行中输入:

java org.mozilla.javascript.tools.shell.Main

即可启动,可以使用load(path/of/script)来加载,加载完成后即可使用脚本中定义的函数,非常方便,当然DOM中的一切是不能用的,比如alert什么的,但是javascript不只是用来脚本化WEB页面的。可以使用print进行打印,使用quit()退出等,下面做一个关于rhino的例子:

user = {
    name:
"abruzzi",
    password:
"123456",
    address:{
        zip:
"612345",
        street:
"west HuangQuan road"    
    },
    getName: 
function(){return this.name;},
    getPassword: 
function(){return this.password;},
    getAddress: 
function(){ return this.address.zip+"\n"+this.address.street;}
}

$ java org.mozilla.javascript.tools.shell.Main
Rhino 
1.7 release 2 2009 03 22
js
> load('~/development/myLib/HighOrderFunc/json.js')
js
> user      
[object Object]
js
> user.getName()
abruzzi
js
> user.getPassword()
123456
js
> user.getAddress()
612345
west HuangQuan road
js
> 
在rhino的发行包中有一些特别有趣的例子,大家不妨自己动手做一下,看看效果,同时体会一下函数式编程的优美,简介。

 

posted @ 2009-04-18 22:18 abruzzi 阅读(2477) 评论(13) 编辑

Hash表这种数据结构在java中是原生的一个集合对象,在实际中用途极广,主要有这么几个特点:

  1. 访问速度快
  2. 大小不受限制
  3. 按键进行索引,没有重复对象
  4. 用字符串(id:string)检索对象(object)

今天整理以前在学校写的一些算法,翻出来一个hash表的实现,就贴出来,自己也温习温习。
先看看头文件,也就是数据结构的定义,相当于java中的接口的概念:

#include <stdio.h>

#define    HASHSIZE 256

//定义hash表中的节点的类型
struct    nlist{
    
struct    nlist    *next;
    
char    *name;
    
char    *defn;
};

//定义接口中的函数,也就是对外来说,这个程序可以做什么
unsigned    hash(
char *s);//计算一个串的hash值
struct    nlist    *lookup(char *s);//查找一个value,根据key
struct    nlist    *install(char *name,char *defn);//插入一个key=value的对象


然后是具体实现:

#include <string.h>
#include 
"list.h"

static struct nlist *hashtab[HASHSIZE];

unsigned    hash(
char *s)
{
    unsigned    hashval;

    
for(hashval = 0*!= '\0';s++)
            hashval 
= *+ 31 * hashval;
    
return hashval % HASHSIZE;
}

struct    nlist    *lookup(char *s)
{
    
struct    nlist    *np;

    
for(np = hashtab[hash(s)];
        np 
!= NULL;
        np 
= np->next)
            
if(strcmp(s,np->name) == 0)
                    
return np;
    
return NULL;
}

struct    nlist    *install(char *name,char *defn)
{
    
struct    nlist    *np;
    unsigned    hashval;

    
if((np = lookup(name)) == NULL){
        np 
= (struct nlist *)malloc(sizeof(struct nlist));
        
if(np == NULL || (np->name = strdup(name)) == NULL)
                
return NULL;
        hashval 
= hash(name);
        np
->next= hashtab[hashval];
        hashtab[hashval] 
= np;
    }
else
        free((
void *)np->defn);
    
if((np->defn = strdup(defn)) == NULL)
            
return NULL;
    
return np;
}

很简单,只有两个外部接口,

  1. install(key, value),用来插入一个新的节点
  2. lookup(key),根据一个键来进行搜索,并返回节点

代码很简单,主要用到的hash算法跟java中的String的hashcode()方法中用到的算法一样,使用:

 

unsigned    hash(char *s)
{
    unsigned    hashval;

    
for(hashval = 0*!= '\0';s++)
            hashval 
= *+ 31 * hashval;
    
return hashval % HASHSIZE;
}

 

这里的31并非随意,乃是一个经验值,选取它的目的在于减少冲突,当然,hash冲突这个问题是不能根本避免的。这里只是一个人们在测试中发现的可以相对减少hash冲突的一个数字,可能以后会发现更好的数值来。

posted @ 2009-04-18 17:28 abruzzi 阅读(1352) 评论(0) 编辑

 

打算写一个系列,比较系统的介绍一下一个脚本引擎的设计和实现过程,本来打算使用lex/yacc来举例子,但是由于最近对java语言有了新的认识,故决定使用javacc这个工具来做,这个系列中就是以javacc中的一个比较复杂的例子来进行解说的。

这篇文章是本系列的第一篇,主要说几个概念的定义,有了这些定义,后边就容易理解了。

对一个语言的源文件进行解析,主要是做这样几件事:

  1. 词法分析
  2. 语法分析
  3. 语义分析

当然,每个步骤中都有自己的错误检查机制,这里暂且不表。下边分别对这几个过程进行解说:

词法分析

词法分析过程中,读入的是一个个的字符,返回的是记号(token).记号是语法分析所认识的一种抽象的概念。
语言中的关键字,变量名,数字,操作符,串等都是记号。

int x;
= 0;

上边这些代码,被词法分析器分析之后,应该生成这样的记号:

<int>,空格,<x>,分号,回车换行,<x>,<=>,<0>,分号

一般而言,在这个过程中,空白字符(空格,tab)都应该被跳过。

词法分析中会用到大量的正则表达式,因为正则表达式的表达能力是非常强大的。在此略微举几个例子,如对数字,变量名的定义,用正则表达式表示如下:

 

NUMBER ::= [0-9][0-9]*
VAR ::
= [a-zA-Z_][a-zA-Z0-9_]*

方括号"[]"表示一个区间,取这个区间中的任意一个字符,"*"表示零次或多次匹配,连字符"-"表示连接此连字符前后的一个区间,如0-9表示从0到9的任意一个字符。所以,NUMBER记号的正则表达式正好可以表示,以0-9中任意一个字符开头,后边跟任意多个0-9之间的数字(含0和9).而VAR的定义正如大部分语言要求的那样,以字母或者下划线开始,后边是任意多个字母或者数字或者下划线。如果需要限定VAR的长度为32位,则可以修改成这样:

 

VAR ::= [a-zA-Z_][a-zA-Z0-9_]*{31}

语法分析

语法分析过程中,读入的是词法分析器返回的记号,返回的是规则(rule),这些规则是语义分析所需要的,在语法分析中,一般会使用BNF来表示规则,比如赋值语句是一个合法的语句,则在此处需要定义关于语法的BNF:

 

assignment ::= INT VAR EQ NUMBER END
INT ::
= "int"
VAR ::
= [a-zA-Z_][a-zA-Z0-9_]*
EQ 
= "="
NUMBER ::
= [0-9][0-9]*
END ::= ";"

就是说,如果读入了这样一个记号的序列:INT VAR EQ NUMBER END,则这个序列被识别成一个赋值的语句,但是,这个还是一个简单的规则,对于这个语句的意思还是不明确的,也就是说,虽然它可以认识所有的赋值语句,但是没有意义,意义的定义在语义分析部分。

 

BNF的表达能力也是非常强大的,比如一个四则混合语算的BNF可以定义如下:

 

expr ::= term ((+|-) term)*  
term ::
= factor ((*|/) factor)*  
factor ::
= number | expr   
number ::
= [0-9]+

这个BNF可以支持乘除运算优先级高于加减法运算,括号优先级高于乘除法,且可以支持对四则混合运算的任意层次的扩展,比如:

 

( ( 12 + 3 ) / 6 ) + ( 1/5 + 155 ) 等的正确解析。

语义分析

在语义分析中,我们需要对在语法分析过程中得出的规则进行解释,比如赋值语句,我们需要将起翻译成:将NUMBER这个记号中的‘值’赋给名叫VAR的一个INT型变量。这个过程完成实际的语法解析,使得语言本身对外有意义。语义分析是脚本型的语言的最高级形式,即脚本解析到此为止将被转换成实际的语言内容并交付给宿主语言编译器进行编译并执行。又或者语义分析发现了一个语法分析过程中规约出来的四则运算表达式,那么它会根据语法中的规则对这个表达式进行求值,计算出最终结果来。

在这个系列中我们用到的一个语言叫做SPL(Stupid Programming Language),这是javacc中给它起的名字,我们不妨将其改名为SPL(Simple Programming Language),呵呵。

关于概念就解释到这里,如果你对正则表达式,BNF等概念不是很熟悉,可以先找找资料来学习。好了,第一篇先介绍这么多吧,后边再续。

 

posted @ 2009-04-18 14:42 abruzzi 阅读(1744) 评论(3) 编辑