使用NewLisp设计Key-Value数据库系统

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个.

相关文章
相关标签/搜索