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