《实用common-lisp编程》对js编码的一点启发
Table of Contents
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,就想试着折腾下函数,用函数生成函数,也是重用代码,减少冗余的有效方法
Date: 2012-08-20 23:22:11 CST
HTML generated by org-mode 6.33x in emacs 23