《实用common-lisp编程》对js编码的一点启发

1 lisp与javascript

javascript是披着lisp外衣的C,所以就去看了眼lisp。由田春翻译的《实用common-lisp编程》 javascript能在函数调用的时候,指定上下文this,当看到common-lisp中的funcall和funapply的时候,只从名字上看,感到格外亲切

2 函数式编程带来的效果

如果一种编程概念或方法论,它在确实更好的解决了问题,那么它就是好的。这里看到lisp写的数据查询函数我确实被震撼了。

2.1 唱片数据库——《common lisp 实用编程》例子

一组放在内存中的唱片数据库*db*

(defvar *db* nil) ;; 定义一个全局变量  星号是lisp全局变量名约定

;; 创建cd函数
(defun makcd (title artist rating ripped)
  (list :title title :artist artist :rating rating :ripped ripped))

;; 添加唱片数据函数
(defun add-record(cd)
  (push cd *db*))

(add-record (make-cd "Roses" "Kathy Mattea" 7 t))  
(add-record (make-cd "Fly" "Dixie Chicks" 8 t))  

;; 现在*db*看起来是这样的
((:TITLE "Roses" :ARTIEST "Kathy Mattea" 7 t)
 (:TITLE "Fly" :ARTIEST "Dixie Chicks" 8 t))

添加了很多数据之后……

2.2 如何查询?

先看一个函数

(remove-if-not #'evenp '(1 2 3 4 5 6 7 8 9 10))
;; -> (2 4 6 8 10)

函数evenp是一个过滤函数:当其参数为偶数,返回真。也可以写成传入一个匿名函数作为过滤器

(remove-if-not #(lambda (x) (= 0 (mod x 2))) '(1 2 3 4 5 6 7 8 9 10))
;; -> (2 4 6 8 10)

js与之对比的函数便是

[1,2,3,4,5,6,7,8,9].filter(function(n){
return n%2 == 0 ;
})

有了remove-if-not这样一个过滤函数,回到上面的数据查询,其实就是添加一个过滤器,比如查询*db*中artist为 "Dixie Chicks"的数据,lisp风格的代码,更上面的偶数过滤函数类似,封装在一个lambda匿名函数里

(defun select-by-artist (artist)
  (remove-if-not
   #'(lambda (cd) (equal (getf cd :artist) artist)) *db*))

现在就可以调用

(select-by-artist "Dixie Chicks")

来获取某个artist为"Dixie Chicks"的数据了。 值得注意的是,上面的函数只能查询artist关键词,如果你要查询其它关键词,比如评分rating为7的,类似的你还要写一系列 类似与select-by-artist的函数,select-by-title select-by-rating etc. 于是,可以写一个通用的函数,接受一个函数作为参数

(defun select (selector-fn)
  (remove-if-not slector-fn *db*))

select-by-artist函数中的匿名函数可以抽离出来

(defun artist-selector (artist)
  #'(lambda (cd) (equal (getf cd :artist) artist)))

那么这样形式的调用

(select-by-artist "Dixie Chicks")

就变为

(select (artist-selector "Dixie Chicks"))

类似的,可以写一个 title-selector

(defun title-selector (title)
  #'(lambda (cd) (equal (getf cd :title) title)))

可以查询title了

(select (title-selector "Fly"))

但是,若需要查询的字段很多,岂不是要傻傻的写很多xxx-selector? 能不能写一个函数,动态生成函数:根据传入的参数,生成对应参数过滤的函数 比如传入title,就生成一个title-selector,若传入artist就生成artist-selector 两者都传入,那么就是一个and的关系 lisp 写出这样代码非常自然,虽然看起来有点晕~ 有了这样的函数,那么查询操作看起来就是这样的

(select (where :title "Fly" :artist "Dixie Chicks"))

where即是需要实现的函数——如果你了解过SQL语句的话,你就知道为什么叫where了

(defun where (&key title artist rating)
  #'(lambda (cd)
      (and
       (if title (equal (getf cd :title) title) t)
       (if artist (equal (getf cd :artist) artist) t)
       (if rating (equal (getf cd :rating) rating) t))))

不纠结与语法细节,单从消除重复这样的效果上看,给人印象是很深刻的,一个where函数就干掉了众多的xxx-selector

(select (where :title "Fly"))

就相当于

(select (title-selector "Fly"))

显然前者非常通用

2.3 对应js实现

函数是第一类型的,函数可以作为参数。js也是具备这样的特性的 js在实现where这样的功能时,其表现力如何?我试着实现了下

2.3.1 数组作为数据库

var db = [];

function makecd(title,artist,rating,ripped){
    return {title:title,artist:artist,rating:rating,ripped:ripped};
}

function addRecord(makecd,title,artist,rating,ripped){
    db.push(makecd(title,artist,rating,ripped));
}

addRecord(makecd,"Roses","Kathy Mattea",7,true)
addRecord(makecd,"Fly","Dixie Chicks",8,true);

/* 现在db中的数据是:
   [ { title: 'Roses',artist: 'Kathy Mattea',rating: 7,ripped: true },
   { title: 'Fly', artist: 'Dixie Chicks', rating: 8, ripped: true } ]
*/

2.3.2 查询操作

js的filter函数和lisp的remove-if-not 对比两种实现 lisp way:

(defun select-by-artist (artist)
  (remove-if-not
   #'(lambda (cd) (equal (getf cd :artist) artist)) *db*))

javascript way:

function selectByArtist(artist){
  return db.filter(function(cd){
           return cd.artist == artist;
         });
}

/*
console.log(selectByArtist('Kathy Mattea'));
[ { title: 'Roses', artist: 'Kathy Mattea', rating: 7, ripped: true } ]
*/

为避免写很多的判断,比如上面如果还要判断title,那么js的实现,还需要加上

function selectByArtist(artist,title){
  return db.filter(function(cd){
           return cd.artist == artist && cd.title == title;
         });
}

可是,title是可选的啊,在进行 && 操作前还的判断 title传进来没有.这里的

function(cd){
    return cd.artist == artist && cd.title == title;
}

相当于

(defun artist-selector (artist)
  #'(lambda (cd) (equal (getf cd :artist) artist)))

不够通用,就像lisp那样,实现一个where条件过滤器

/**
 * 接受一组函数和参数
 * 当所有的函数对参数的运行都为真时,返回真,否这返回假
 */
function all(){
    var fns = [].slice.call(arguments),
    len = fns.length;
    return function(){
        var i=0,
        fn
        while(fn = fns[i]){
            i++;
            if(!fn.apply(null,arguments)){
                return false;
            }
        }
        return true;
    };
}

这是一个工具函数,以任意个函数做为参数,返回一个合并后的函数,只有这任意个函数的值为真,这个合并函数的值才为真 函数作为参数传递,函数的返回值也是一个函数 下面的演示

var retfn = all(
function(n){
  return n>0;
},
function(n){
  return n<10}
);
var ret = [11,-1,5,4,9,11];
ret = ret.filter(retfn);
console.log(ret);
->[ 5, 4, 9 ]

出于演示的目的,把本可以放在一起判断的逻辑

ret.filter(function(n){
    return n>0 && n<10;
})

拆成了两个函数

2.3.3 查询函数生成器

上面lisp 实现的 where 函数可以根据参数, (&key title artist rating) 生成一个合并后的查询函数 像(&key title artist rating)这样的带有名字的参数列表,在js中可以用对象{key1:val1,key2:val2} 来模拟

/**
 * 查询函数生成器
 */
function makequery(o){
    var fns = [];
    for(var x in o){
        fns.push(function(cd){
            return o[x] == cd[x]
        });
    }
    return fns;
}

/*
  var allquery = makequery({rating:8});

  var allfn = all.apply(null,allquery);

  var queryresult = db.filter(allfn);

  console.log(queryresult);
*/

比如,makequery({rating:8})返回一个函数,这个函数当传入对象的rating字段为8时,返回真,否则返回假 若传入两个字段,则返回包含两个函数的数组。 工具函数all能合并多个过滤函数为一个,这样,js版本的where函数也就大功告成了

/*
 * 合并上面的得到
 */
function where(o){
    return db.filter(all.apply(null,makequery(o)));
}

/**
   var ret = where({rating:8})
   console.log(ret);
*/

select功能实现后,顺带也把update功能也实现

/**
 * 更新操作,希望的接口是这样的
 * update(db,where({artist:'Dixie Chicks'}),{rating:8.5})
 */
function update(db,queryresult,to){
    queryresult.forEach(function(item){
        var idx = db.indexOf(item);
        db.splice(idx,1,merge(item,to));
    });
}

function merge(original,extra){
    for(var x in extra){
        original[x]  = extra [x];
    }
    return original;
}

/*
  update(db,where({artist:'Dixie Chicks'}),{rating:8.5});
  console.log(db);
*/

function deldb(db,queryresult){
    queryresult.forEach(function(item){
        var idx = db.indexOf(item);
        db.splice(idx,1);
    });
}

/*
  deldb(db,where({artist:'Dixie Chicks'}));
  console.log(db);
*/

3 总结

学了一点lisp,就想试着折腾下函数,用函数生成函数,也是重用代码,减少冗余的有效方法

Author: tom

Date: 2012-08-20 23:22:11 CST

HTML generated by org-mode 6.33x in emacs 23

posted on 2012-08-20 23:21  wewe.Tom  阅读(669)  评论(0编辑  收藏  举报