使用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個.

相關文章
相關標籤/搜索