Lisp是一个很好的语言,最强大的就是其S-表达式,能够说是Lisp能活到今天的惟一一个缘由。其次就是函数为基本类型和后来的闭包。固然Lisp也有很大的缺点,即:通常的设计师难以免Lisp的缺点。git
Lisp有不少方言,不少子系列,能够说百花齐放,也能够说是散沙一盘。这就是Lisp的优势之一,同时也是其缺点之一,可是这些缺点主要是用Lisp的人形成的,而之因此会这样,是由于Lisp太容易滥用了(其缺点正是由于其优势致使的)。数据库
NewLisp是一个很是强大的Lisp实现,也能够称为一个方言,是一个很是简单而又能力十足的方言。你能够用来编写各类脚本、能够用来制做小工具,能够用来设计桌面应用,能够用来设计本地软件、甚至还能胜任大型软件(只要你想,就能够作到)。为何呢,由于其不但有着十足的灵活性,并且还能极其容易的和其余语言合做,好比你用C语言写底层库,就能在NewLisp中轻易的使用。固然NewLisp有一个致命的缺点:没有完善的错误信息,一旦出现错误,就如同C++模板同样让你神经失常。关于这一点,须要NewLisp之后改善,或者是有一个IDE之类的工具提供支持。这是NewLisp不适合设计商业软件的致命缘由,再加上原本Lisp就是没多少人用的语言,并且还这么多方言分支,因此目前应该是只适合我的开发了。安全
项目地址: https://git.oschina.net/nneolc/lisp-sdb闭包
NewLisp官方网站: http://www.newlisp.orgapp
* 本文讲述的是使用NewLisp设计一个简单的本地Key-Value数据库系统的过程,这个东西的名称是sdb框架
这里的框架指的是让软件运行起来的那种框架,由于我须要让成品不依赖NewLisp的东西,本身就能运行,也就是说编译成可执行程序。less
这在主流静态语言中易如反掌的事,在Lisp系列语言中简直比登天还难,只有极少的一星半点的Lisp方言实现能支持,并且效果还很差。以致于新的Lisp方言实现干脆直接挂上JVM平台了,好比Clojure。函数
而NewLisp是我遇到的最强大的Lisp方言实现,由于我能够像喝水同样极其轻易的编译可执行程序,并且没有任何负面的问题存在。NewLisp编译的原理很是简单,就是复制自身,而后在后面加上代码,这样一个可执行程序就出来了。工具
; hello.lisp (print "hello world!\n") (exit) >newlisp -x hello.lisp hello.exe >hello.exe hello world! >_
固然直接在发行版中附带newlisp.exe,而后用newlisp main.lisp同样能够,但这并很差。编译后的程序不须要额外的命令启动,并且不会被newlisp本来的参数所干扰,这是很重要的一点。oop
可是这有一个缺点,就是NewLisp只能编译单个lisp文件为可执行程序,至少我目前没有发现编译多个文件的方法。不过这个缺点很容易破解,并且其并不算缺点。咱们只须要创建一个框架就能够了:
; main.lisp (load "a.lisp") (load "b.lisp") ... (exit (app:run (rest (main-args))))
这样就作出了一个框架,启动时加载模块,而后用app:run来开始执行,app:run定义在某个模块中,做为程序入口。这样,咱们就只须要编译main.lisp:newlisp -x main.lisp demo.exe,然而将全部模块放在demo.exe的目录中,这些模块可随时修改。
可是这样就好了吗?固然不行,由于这是硬编码,因此,咱们须要考虑更好的设计。我最终决定使用二级指针来完成,因而整个框架只须要两行代码:
; main.lisp (load "app.lisp") ; 加载一级指针,经过这个模块加载其余模块 (exit (app:run (rest (main-args))))
其中app.lisp就是一级指针,也能够说是模块加载器:
; app.lisp (load "a.lisp") (load "b.lisp") (load "c.lisp") ...
在app.lisp中加载须要的模块,也能够同时作一些事务,或者是定义一些东西等等。这样,除了app.lisp这个文件的文件名不能修改,其余的均可以随意修改,想加模块随意加,想修改模块随意改。
首先我对sdb的定义是一个简单的Key-Value数据库系统,并且是面向本地的。因此我决定像SQLite同样,一个文件对应一个数据库。既然是Key-Value数据库,那么就须要定义好模型。
一个数据库中,平淡的存储Pair就完事了吗,不,我须要个性一点。因此我在Pair的上面加了一层:空间。
一个数据库中能够用多个空间,Pair存储在空间之中。这看起来就像是全部表都只有2列的SQL数据库。
而后是如何操做数据库,我决定采用最简单的方法,由于这原本就只是练手之做。使用一个REPL客户端,经过执行用户的命令来处理事务。固然这个不会有SQL的那些东西,采用最简单的命令结构:
// 设想 >get a.p // 获取空间a中key为p的Pair的值 ... >set a.p 100 // 更新空间a中key为p的Pair的值或创建新的Pair ...
最核心的两个命令:get、set。做为一个Key-Value数据库来讲,是核心的东西了。
这几乎就说明了须要作什么了,提供一个REPL客户端,用户经过命令来处理事务,那么设计好对这些命令的处理就差很少是作好sdb了。
如下是目前定义的全部命令:
get <spacename>.<key> // 获取值 set <spacename>.<key> <value> // 更新值或创建Pair list <spacename> // 列出一个空间中的全部Pair erase <spacename>.<key> ... // 删除全部指定的Pair eraseall <spacename> ... // 删除全部指定的空间中的全部Pair spaces // 列出全部空间 dup <source-spacename> <new-spacename> // 复制一个空间到一个新的空间 new <spacename> ... // 创建一至多个空间 delete <spacename> ... // 删除全部指定的空间 link <file> // 链接到一个数据库文件 unlink // 断开当前链接并保存更新 relink // 断开当前链接,仅重置 h or help <command> // 显示命令帮助信息 q or quit // 退出sdb,自动保存更新 q! or quit! // 仅退出sdb
而后是sdb的程序参数,目前倒没什么可定义的,因此只有两个:
link:<file> 启动时链接到一个数据库文件 help 显示使用帮助信息
例子:
sdb // 直接进入REPL sdb help // 显示使用帮助信息,虽然你已经知道了 sdb link:db.txt // 启动时链接数据库文件db.txt
sdb自己是一个Key-Value数据库系统,同时提供REPL客户端,那么咱们就划分两个模块,一个是客户端模块,一个是数据库模块。客户端模块处理用户输入,调用数据库模块来处理事务并输出结果,数据库模块负责数据模型和文件存储。
因而sdb须要2个模块,总共4个代码文件:
sdb/ app.lisp ; 一级指针,模块加载器 client.lisp ; 客户端模块 db.lisp ; 数据库模块 main.lisp ; 软件框架
; main.lisp (load "app.lisp") (exit (app:run (rest (main-args)))) ; app.lisp (load "db.lisp") (load "client.lisp") ; db.lisp (context 'db) ... (context 'MAIN) ; client.lisp (context 'app) ... (define run (lambda (.args) )) (context 'MAIN)
在NewLisp中定义模块很简单,使用(context)函数就好了,(context)会创建一个模块,或者切换到已有的模块,访问一个模块的东西时,须要用<模块名>:<符号名>,就好比app:run,就是访问app模块中的run,从而调用该函数。每一个模块开始和结尾都调用了一次(context)函数,这是一种模块设计方式。第一次是创建模块,第二次是切换到顶层做用域。在这里,main.lisp是框架,app.lisp是加载器,因此不须要定义成模块。
client.lisp主要负责处理用户的输入,并调用db.lisp来处理事务,而后输出结果,不断的循环,直到用户砸了机器为止。固然确定不须要砸机器,这是一个标准的Read->Exec->Print->Loop循环,咱们已经提供了quit/quit!两个命令让用户能退出。在框架中,已经预先限定了,须要app模块中有个run函数,这将做为程序的入口。简单起见,直接在run中作好REPL循环,顺带检查参数。
(define run (lambda (.args) ; 若是有参数,调用do-options解析并处理 (unless (empty? .args) (do-options .args)) ; 显示软件标题 (show-title) ; 是否要继续循环,是就继续 (while (nextrun?) ; 显示当前上下文环境 (链接的数据库) (echo-context) ; 等待用户输入而后处理 (query (read-line))) 0))
从代码已经能够清晰的看出整个流程了,可是你可能以为这个(nextrun?)是否是多余呢,其实很少余。由于整个调用链不小,为了确保正确性,因此使用了一个全局变量。固然这是安全的,由于读写是经过专用函数操做的:
(define _isnextrun true) ; 一种命名风格,前面加_表示为模块内部符号 (define nextrun? (lambda () _isnextrun)) (define stoprun (lambda () (setf _isnextrun false)))
query函数主要负责解析命令、检查命令,执行命令:
(define query (lambda (txt) (letn ((tokens (get-tokens txt)) ; 解析命令文本,转换成单词列表 (cmd (cmd-prehead (first tokens))) ; 是什么命令 (cmdargs (rest tokens))) ; 这个命令的参数列表 (case cmd ; 因为细节较多,因此部分命令的处理过程省略 ("get" ...) ("set" ...) ("list" ...) ("erase" ...) ("eraseall" ...) ("spaces" (map println (db:list-spaces))) ("dup" ...) ("new" ...) ("delete" ...) ("link" ...) ("unlink" (cmd-request (db:unlink))) ("relink" (cmd-request (db:relink))) ("help" (cmd-help cmdargs)) ("quit" (begin (db:unlink) (stoprun))) ("quit!" (stoprun)) (true (unless (empty? cmd) (println "Unknow command, enter h or help see more Information.")))))))
(cmd-request)是对一次数据库操做的过程封装:
(define cmd-request (lambda (result) (if result (println "ok") ; 操做成功,显示ok (println (db:get-last-error))))) ; 失败,显示错误信息
(cmd-prehead)是对命令的预处理:
(define cmd-prehead (lambda (txt) (let ((cmd (lower-case txt))) ; 转换成小写 (case cmd ("h" "help") ; h替换成help ("q" "quit") ; q替换成quit ("q!" "quit!") ; q!替换成quit! (true cmd))))) ; 其余的返回自身
基本上这就是client.lisp的所有了,其余未列出来的东西可有可无。
在db.lisp中,最核心的问题就是,如何表示数据模型,使用什么结构来构造数据,也能够说是,怎么设计get/set函数。只要get/set设计好了,其余的所有就不是问题了。
若是只是纯粹的Key-Values,那么直接用一个列表就好了,NewLisp有着原生的支持来构造Key-Value表:
> (setf x '()) () > (push '("a" 1) x) // 创建键值对 (("a" 1)) > (push '("b" 2) x) -1) (("a" 1) ("b" 2)) > (assoc "a" x) // 查询键值对 ("a" 1) > (setf (assoc "a" x) '("a" "one")) // 更新键值对 ("a" "one") > (pop-assoc "a") // 删除键值对 ("b" 2) > x (("a" "one"))
对于每个空间,使用原生提供的push/assoc/pop-assoc就能够完成所有的键值对操做了。可是一个数据库有多个空间,那么这些空间怎么放呢?直接嵌套Key-Value表吗?
'(("s1" // 空间s1,有a=1, b=2两个键值对 (("a" 1) ("b" 2))) ("s2" // 空间s2,有c=1, d=2两个键值对 (("c" 1) ("d" 2))) ("s3" // 空间s3,有e=1, f=2两个键值对 (("e" 1) ("f" 2))))
这样,访问一个键值对就须要以下形式:
(assoc "a" (last (assoc "s1" _spaces))) ; 访问空间s1中的a ... (map (lambda (s) (first s)) _spaces) ; 列出全部空间的名称
对我来讲,这样很差,还有一个问题是,更新一个空间中的键值对时,须要(setf (assoc spacename _spaces) newspace)。这意味每一次对空间里键值对的增/删/改操做,都会生成一个新的空间。
在尝试这样设计get/set以后,我决定放弃这种结构,而使用另外一种方式。
这种方式很不直接,但颇有效。具体的作法是,让每个空间都对应着一个符号,创建空间时生成一个符号和一个空间对象(Key-Value表),让这个符号指向空间对象,删除一个空间时,直接删除对应的符号就能够了。
这是什么意思呢?就是至关于在运行时定义变量,这个变量的名称都是临时生成的。通过一番设计,最终的结构以下:
(define _spaces '())
你没看错,就只是定义一个列表,_spaces中只存放空间的名称。那么如何创建空间呢?创建的空间要怎么访问呢?
(define new-space (lambda (name) (if (have-space? name) ; 这个空间已经存在了吗? (ERROR _SPACE_HAS_EXIST name) ; 已经有了,不能重复创建 (begin ; 若是还未存在 (set (get-space-symbol name) '()) ; 定义一个符号,符号的名称根据这个空间的名称来生成,指向一个空的Key-Value表 (push name _spaces))))) ; 加入这个空间的名称
是否是有点困惑,为何这样就创建了一个空间呢?让咱们再看下(get-space-symbol)
(define get-space-symbol (lambda (name) (sym (string "_space." name)))) ; 返回一个符号,这个符号的名称为_space.加上空间的名称
就好比刚才示范的三个空间,一一创建的话,就会在NewLisp的运行时环境中,定义三个新的符号,这些符号指向各自的Key-Value表。
因而,就能够在代码中使用(setf (assoc keyname (get-space-symbol spacename)) (list keyname value))之类的形式来增/删/改一个空间的键值对。
每一次创建空间都会生成一个新的符号,但咱们并不能预先知道这个符号是什么名称。上面三个空间在创建时生成_space.s一、_space.s二、_space.s3三个符号,但咱们没法直接使用,因此咱们须要(eval)来完成对空间的访问。这样的结构,使得get/set也轻松的设计出来:
(define kvs-get (lambda (spacename keyname) (if (have-space? spacename) ; 这个空间存在吗? (let ((kv ; 若是存在 (eval ; 生成[获取键值对的函数调用] 并执行 (list 'assoc 'keyname (get-space-symbol spacename))))) (if kv ; 这个键值对存在吗? (last kv) ; 存在,返回值 (ERROR _PAIR_NO_IN_SPACE keyname spacename))) ; 不存在,提供错误信息 (ERROR _SPACE_NO_EXIST spacename)))) ; 这个空间不存在,提供错误信息 (define kvs-set (lambda (spacename keyname value) (if (have-space? spacename) ; 这个空间存在吗? (begin ; 若是存在 (eval (list ; 生成[删除这个键值对的函数调用] 并执行 'pop-assoc 'keyname (get-space-symbol spacename))) (eval (list ; 生成[加入新键值对的函数调用] 并执行 'push '(list keyname value) (get-space-symbol spacename)))) (ERROR _SPACE_NO_EXIST spacename)))) ; 这个空间不存在,提供错误信息
为何须要eval呢?由于咱们须要访问一个运行时创建的符号,就好比:
var x = 100;
咱们能够在源代码中直接对x进行任何操做,可是若是这个x是运行时定义的呢?为何要运行时定义,由于咱们不知道用户会用什么名称来创建空间,咱们须要将运行时创建的符号,融合进日常的操做中:
(eval (list assoc 'keyname (get-space-symbol spacename))) ; 若是spacename传入的是s1 ; 那么就会生成这样的函数调用: (assoc keyname _space.s1) ; 看到这里应该全部人都明白了
为何这么麻烦?由于NewLisp没有提供指针变量的支持,若是能用指针,就能有更简单好用的设计。这是Lisp的缺点之一,主要是由于不少方言实现都避免指针致使的。就像不少Lisp中易如反掌的事,在C这些语言中须要绕出病来同样,不少在C这些语言中易如反掌的事,一样在Lisp中会绕出病。
就在这时,我发现这样的一个提示:当前已输入9840个字符, 您还能够输入160个字符。因而我只好草草结束。但还好的是,这个sdb最关键的几个地方都讲解了一遍。
固然这里讲解比较浅显,具体的东西,仍是要看代码才会明白。文章写的这么多字,远不如直接看代码来的清晰。
NewLisp是一个颇有价值的Lisp方言实现,虽然目前还有不少缺点。
您还能够输入3个.