gophp解释器_【干货】Gisp 解释器 Golang 辅助开发工具
Gisp 是一個提供給 golang 使用的 Lisp 類 DSL 解釋器。在 Lisp 的基本語法基礎上,針對 go 環境稍作了一點語法糖。主要目標是提供一個盡可能便于與 golang 互操作的微型DSL工具。
簡介
Gisp用go語言編寫,是一個DSL 解釋器,這個 DSL 基本上就是 LIsp 的基礎語法,針對go程序的互操作需要稍微做了一點擴展。它的主要設計目標是盡可能方便的在 go 程序中調用 gisp 解釋器,執行 dsl。
我們的項目,目前后臺主要用 golang 開發。開發過程中,golang 確實達到了我們期待的易用、易維護。但是有幾個具體的問題阻礙了我們更好的使用它。這是我們開發一個內嵌解釋器的基本動機。我們希望用這種方式提升編程效率,更快的推進工作。
golang 使用過程中的問題
Golang 是一門很好的工程語言,整合了幾十年來工程界一些已被證明行之有效的經驗。成為一門非常適合網絡服務開發的后端工程語言。但是 golang 也存在一些具體的問題,影響了我們團隊的工作效率。
在 golang 中,沒有異常拋出和捕獲的機制,通常通過函數返回多個參數的方式,在 error 返回值中傳遞錯誤狀態。這樣的好處是錯誤不中斷程序,對于一些連續處理的程序邏輯非常方便。但是相應的,沒有強制中斷機制,對于一些依賴程序狀態,出錯需要跳出流程,但是不中斷整個程序進程的場合,就無能為力了。典型的,當我們需要組合大量的小函數調用的時候,幾乎每一步都要寫一個狀態判斷。
if err != nil {
return nil, err
}
在類似 parsec 解析這樣密集使用自定義 Parser、用 Bind 組合子傳遞狀態時,這種固定的錯誤處理代碼可以超過程序代碼行數的一半以上。這浪費了開發人員的工作,也影響代碼閱讀,提高了維護難度。
另一個問題是類型推導過于簡單。缺少泛型和 overload 機制。這樣固然學習簡單,編譯器的性能和質量容易有保障,但是代價是一些編程需求比較難實現。例如我們需要實現帶單位的商用數據的統計計算,就要處理非數值類型的累加。在我們的項目中,我們期望這個邏輯可以在運行期間不修改程序代碼,穩定可靠的適應,這對于golang比較困難。
要在不修改 golang 編譯器和語法的前提下,緩解這些由 golang 語法限制的問題,就要提供一個可以方便調用的 DSL 環境。這里我們選擇實現一個基本的 Lisp 解釋器。
安裝和環境構造
gisp依賴 github.com/Dwarfartisan/goparsec 。使用 gisp 前需要安裝 goparsec 和 gisp 。
go get github.com/Dwarfartisan/gisp
go get github.com/Dwarfartisan/goparsec
導入gisp時引用:
import (
"github.com/Dwarfartisan/gisp"
)
上面這個示例代碼只傳入了基本的公理操作。其中包括了 Lisp 七公理中的六個(cons 在這個環境中沒有實用的價值,直接實現為 append的封裝 concat)。當然我們甚至可以連公理體系都不加入,那時 gisp 仍可以作為一個詞法解析工具使用。
技術選擇與設計
選擇 Lisp ,主要是考慮兩個方面。
Lisp 的前端容易實現。之前為了業務后臺,我們在 golang 中實現了文本解析工具 parsec 。這里可以復用。
另一方面,Lisp 的程序即數據結構,這對于我們處理混合編程非常方便,可以將數據和程序調用在外部組裝后傳入解釋器環境。
調用接口
我們為 gisp 提供兩個執行程序的接口,Parse 接受代碼文本:
pi, err := gisp.Parse("box[\"c\"]")
if err != nil {
t.Fatalf("except got pi is 3.14 but error: %v", err)
}
而Eval是傳遞golang對象:
func TestMulAutoOverload(t *testing.T) {
in := Float(30.9)
ratio := Float(0.8)
out := in * ratio
g := NewGisp(map[string]Toolbox{
"axioms": Axiom,
"props": Propositions,
})
g.Defun("*", mrmul())
mulx, ok := g.Lookup("*")
if !ok {
t.Fatalf("except got overloaded function *")
}
ret, err := g.Eval(List{mulx, in, ratio})
if err != nil {
t.Fatalf("except %v * %v is %v but error %v", in, ratio, out, err)
}
if !reflect.DeepEqual(ret, out) {
t.Fatalf("except %v * %v is %v but %v", in, ratio, out, ret)
}
}
看起來我們仍然要處理 Parse 和 Eval 中傳遞出來的 error 狀態,但是 gisp 會自動監測每一個代碼語句的執行結果,一旦有錯誤就跳出,我們只需要在每次調用 Parse 或 Eval 后監測一次。
上例中的代碼我們后面再做進一步講解。這里我們可以看到,代碼中演示了乘法運算符重載。
gisp 運行機制
1.環境
首先,這里介紹 Gisp 環境的概念,Gisp 文本代碼或者 gisp 代碼序列,都執行在解釋器對象中,而解釋器解釋代碼序列,需要使用環境(gisp.Env)。一個 gisp.Env ,需要實現 Lookup,Local、Global、Set、Defvar、Defun 方法。
Local 方法查找本地是否有給定命名,這要求實現 gisp.Env 時應自己實現一個命名管理機制。
Global 方法查找當前環境的外部環境是否有給定命名。這要求實現 gisp.Env 時應實現外部環境的引用管理。
Set 實現賦值操作,被賦值的命名必須已經存在(已定義)。
Defvar 聲明一個變量
Defun 聲明一個函數,因為需要實現函數重載,這里將函數和變量命名區分開。
2.解析和求值
Gisp 遵循一個簡單的機制。通過文本分析過后,代碼解析成一組 gisp 值,到此為止是 Parse 特有的過程。此后進入 Eval,對每一個解析結果順序求值。
各長度整數一律解析為 gisp.Int
各長度浮點數一律解析為 gisp.Float
如果是 Lisp 接口對象,將當前環境(初始是 gisp 解析器對象)傳入,返回求值結果。特別的,如果是 List ,首先將第一個元素求值,然后將后續元素作為參數,嘗試傳遞給第一個元素的求值結果,將其作為一個函數執行,返回求值結果。如果解釋器不知道如何調用這個元素,返回錯誤;如果是 Quote ,返回其包含的元素,這是常見的傳遞數據的封裝方法;gisp 不支持標準的 Lisp (a . b)語法,形如 a.b 的表達式被解析為 gisp.Dot 表達式。該表達式求值遵循以下方式:1)首先,將 a 視作一個 Atom,對 a 求值2)如果a的值是 reflect.Value,嘗試獲取名為 b 的 method 或 field3)如果是 map ,嘗試獲取其鍵值(這里的行為類似 javascript)4)如果是gisp模塊(即 toolkit ,其實其內容基于 map[string]interface{} ),嘗試獲取對應的成員。5)如果不屬于任何 gisp 可解析的類型,返回原值。gisp 將中括號 [] 用于一個語法糖——引入 golang 的索引操作:1)它可以對List、map[string]interface{} 做普通的索引操作;2)對List,支持負索引和切片3)對于其它 reflect.Kind 為 array, slice 和 map 的數據結構,用反射嘗試進行索引操作,這部分還沒有經過充分的測試;未來希望可以支持對嵌套的 List/[]interface{} 和 map[string]interface{} 支持連續索引操作,這樣可以方便的處理 JSON;3)如果僅給出 [...] ,中括號表達式左邊沒有給出對象,則解析為一個 brackets 函數,它接受一個容器類型作為參數,對其進行前述的索引操作。即 x[...] 等同于 ([...] x)。
如果不屬于任何 gisp 可解析的類型,返回原值。
基本概念和主要數據類型
Atom
List
Quote
函子、函數和 Lambda
內置模塊和功能
gisp 公理
公理(axioms)模塊主要用于實現 Lisp 語系必須的幾個基本操作。這里沒有完整的實現 Lisp 公理,因為 gisp 的語義和實現內核都不是基于完整的 Lisp ,而是 golang runtime 。這是出于實用的考慮而非優雅。
quote
quote 操作接受任意的數據,將其封裝為一個 Quote。Quote 在 Eval的時候返回其內部保存的數據。它常用于 Lisp 的數據傳遞,在 gisp 的內部也經常用個類型直接封裝數據用于傳遞。在golang中可以調用 gisp.Q(x itnerface{}) 函數,得到一個 Quote{x} 。(quote x) 等價于 'x 。
var
var 在最里的一個 Env 中定義一個命名。它可以使用以下幾種形式:
(var x)
(var x::type) 這里需要注意的是,一般來說 Lisp 是弱類型的,而 gisp 其實是強類型的,而且是靜態類型。不過gisp并不能在解釋器中直接用 gisp 腳本定義新類型,它只能在 golang 環境中擴展,這是為了讓 gisp 解釋器盡可能保持簡單。
(var x value) 在定義的時候可以給出 x 的值,這里其實內部是順序作了 def 和 set 操作
定義x的時候,如果同名的變量或函數已經存在于當前環境,就會報錯。
set
set 操作比較好理解, (set x value) 就是對x進行賦值,x需要預先已經存在。在 gisp 環境內部,def 會生成一個 gisp.Var 接口的 slot 對象,這個對象內部通過反射管理賦值,如果 x 和value 類型不匹配,會導致panic。
equal
euqal 內部其實調用的是 reflect.DeepEqual 。
cond
cond 就是普通的 lisp cond 操作符,相當于 golang 的 value switch case 。gisp 沒有實現 type switch。而且目前使用的案例中其實沒有用到過 cond ,這部分沒有經過充分的測試。
car
car 取給定 list 的第一個元素,類似于 haskell 的 head 操作。等價于 Lisp 通常意義上的 car 操作符。
cdr
cdr 取 List 除了第一個以外剩下的元素,等同于通常意義的 cdr 操作符,也就是 Haskell 的 tail操作。即 list[1:] 。由于實際使用中還沒有遇到,這里也沒有經過充分的測試,從代碼中看對空列表做 cdr 會 panic。
atom
atom 等同于通常意義的 Lisp atom 操作符,如果給定的參數是 List ,返回false,否則返回true。這個操作符也沒有經過充分的測試。
concat
Lisp 的公理 cons ,用于將 head 和 (tail.()) 結合成一個 list。但是這個功能在 gisp 面向 golang 做互操作的需求前提下沒有存在意義,這里 gisp 實現了一個 concat 操作,內部調用 append,將給定的參數連接成一個 gisp.List 。
Gisp 定理
定理(propositions)其實是一些基礎操作,主要是比較操作和數學運算。這個可以參見其定義代碼:
var Propositions Toolkit = Toolkit{
Meta: map[string]interface{}{
"name": "propositions",
"category": "package",
},
Content: map[string]interface{}{
"lambda": BoxExpr(LambdaExpr),
"let": BoxExpr(LetExpr),
"+": EvalExpr(ParsexExpr(addx)),
"add": EvalExpr(ParsexExpr(addx)),
"-": EvalExpr(ParsexExpr(subx)),
"sub": EvalExpr(ParsexExpr(subx)),
"*": EvalExpr(ParsexExpr(mulx)),
"mul": EvalExpr(ParsexExpr(mulx)),
"/": EvalExpr(ParsexExpr(divx)),
"div": EvalExpr(ParsexExpr(divx)),
"cmp": EvalExpr(cmpExpr),
"less": EvalExpr(lessExpr),
"": EvalExpr(greatExpr),
">?": EvalExpr(gtoExpr),
">=": EvalExpr(geExpr),
">=?": EvalExpr(geoExpr),
"==": EvalExpr(eqsExpr),
"==?": EvalExpr(eqsoExpr),
"!=": EvalExpr(neqsExpr),
"!=?": EvalExpr(neqsoExpr),
},
}
這里有兩個函數單獨拿出來討論一下,一個是let ,一個是lambda。
let
Let 在 lisp 中構造一個封閉的環境,可以指定若干初始化變量,其作用域僅限于let內。
func TestParsecBasic(t *testing.T) {
g := NewGispWith(
map[string]Toolbox{
"axiom": Axiom, "props": Propositions, "time": Time},
map[string]Toolbox{"time": Time, "p": Parsec})
digit := p.Bind(p.Many1(p.Digit), p.ReturnString)
data := "344932454094325"
state := p.MemoryParseState(data)
pre, err := digit(state)
if err != nil {
t.Fatalf("except \"%v\" pass test many1 digit but error:%v", data, err)
}
src := "(let ((st (p.state \"" + data + `")))
(var data ((p.many1 p.digit) st))
(p.s2str data))
`
gre, err := g.Parse(src)
if err != nil {
t.Fatalf("except \"%v\" pass gisp many1 digit but error:%v", src, err)
}
t.Logf("from gisp: %v", gre)
t.Logf("from parsec: %v", pre)
if !reflect.DeepEqual(pre, gre) {
t.Fatalf("except got \"%v\" from gisp equal \"%v\" from parsec", gre, pre)
}
}
通常來講,在實用項目中使用 gisp 解釋器,可以用let得到一個比較干凈和安全的沙箱環境,用let隔離每一次腳本的運行,使之不會互相干擾。
lambda
lambda 的含義和用法不用太多介紹,就是 Lisp 實現中通常的形式。不過有幾點需要注意:
gisp 中變量可以附帶類型,這是定義函數重載的方式,但是實踐上我目前為止都是在 go 中構造函子。所以這部分沒有經過充分測試。原理上,gisp函數是各種同名但不同類型的 lambda 的集合容器。
lambda 一般來講可以不攜帶類型直接使用,在ginq等工具應用場合,lambda往往是用來封裝一段規則,不需要復雜的約束。
func TestGinqWhereSelect(t *testing.T) {
data := QL(
L(0, 1, 2, 3, 4, 5),
L(1, 2, 3, 4, 5, 6),
L(2, 3, 4, 5, 6, 7),
L(3, 4, 5, 6, 7, 8))
g := NewGispWith(
map[string]Toolbox{"axiom": Axiom, "props": Propositions, "utils": Utils},
map[string]Toolbox{"time": Time})
g.DefAs("data", data)
ginq, err := g.Parse(`
(ginq
(where (lambda (r) (< 1 r[0])))
(select (fs [1] [2] [4]))
)
`)
if err != nil {
t.Fatalf("except got a ginq query but error %v ", err)
}
re, err := g.Eval(L(ginq, data))
if err != nil {
t.Fatalf("except got columns from data but error %v", err)
}
t.Logf("ginq select got %v", re)
}
常用工具
Parsec
Parsec是我們項目中使用到的重要工具之一。它用于文本和規則解析。目前 goparsec 的表現尚可,基本完成了預期目標。但是限于go的語法,有一些地方并不盡如人意。
go 的強類型靜態檢查,使得 parsec 的 Parser 構造能夠基于一個比較嚴謹的輸入約束。但是因為go沒有泛型。文本和[]interface{} 的解析器只能各自實現,而在 Haskell 中這些只需要寫一次。
由于沒有泛型,為了讓 goparsec 能夠適用各種不同的解析場合,每個 Parser 的返回值只能寫成 interface{} 。
go 沒有 throw 和 try catch,每訪問一次 state ,都要檢查返回狀態是否有錯。雖然有大量組合子用于減少這個工作量,例如 Bind_, Bind, ManyTil 都是有力的工具。但是一旦我們需要在狀態傳遞中加入稍復雜一點的業務規則,就要實現自己的 Bind Keep 函數。在這個過程中我們總是要編寫大量的 if err != nil {return nil, err} 。
事實上,在使用 Parsec 的過程中遇到的各種不便,特別是錯誤處理,是我開發 gisp 的最主要動機。
限于 golang 項目在實用中的性能考慮,目前我們仍然將 string 和 List 的 Parsec 分別實現為 parsec 和 parsex 。當前只是對 goparsec 的封裝,未來可能會根據 gisp 的實踐經驗,向 gisp 化改變。
在 gisp 中調用 parsec ,最大的好處是省去錯誤監測(這個工作由 gisp 自然的接管了),于是就可以用類似haskell 版本的風格去自然的編寫解析過程:
func TestParsecRune2(t *testing.T) {
g := NewGispWith(
map[string]Toolbox{
"axiom": Axiom, "props": Propositions, "time": Time},
map[string]Toolbox{"time": Time, "p": Parsec})
//data := "Here is a Rune : 'a' and a is't a rune. It is a word in sentence."
data := "'a' and a is't a rune. It is a word in sentence."
state := p.MemoryParseState(data)
pre, err := p.Between(p.Rune('\''), p.Rune('\''), p.AnyRune)(state)
if err != nil {
t.Fatalf("except found rune expr from \"%v\" but error:%v", data, err)
}
src := `
(let ((st (p.state "` + data + `")))
((p.rune '\'') st)
(var data (p.anyone st))
((p.rune '\'') st)
data)
`
//fmt.Println(src)
gre, err := g.Parse(src)
if err != nil {
t.Fatalf("except \"%v\" pass gisp '' but error:%v", src, err)
}
t.Logf("from gisp: %v", gre)
t.Logf("from parsec: %v", pre)
if !reflect.DeepEqual(pre, gre) {
t.Fatalf("except got \"%v\" from gisp equal \"%v\" from parsec", gre, pre)
}
}
Ginq
Ginq 模塊也是開發 gisp 的動機之一,我們項目中主要使用的是 go-linq ,這個項目質量很高。但是我們需要多步簡單操作的時候,go風格的linq結構仍顯有點笨拙。在 ginq 中可以簡潔很多。
func TestGinqWhereSelect(t *testing.T) {
data := QL(
L(0, 1, 2, 3, 4, 5),
L(1, 2, 3, 4, 5, 6),
L(2, 3, 4, 5, 6, 7),
L(3, 4, 5, 6, 7, 8))
g := NewGispWith(
map[string]Toolbox{"axiom": Axiom, "props": Propositions, "utils": Utils},
map[string]Toolbox{"time": Time})
g.DefAs("data", data)
ginq, err := g.Parse(`
(ginq
(where (lambda (r) (< 1 r[0])))
(select (fs [1] [2] [4]))
)
`)
if err != nil {
t.Fatalf("except got a ginq query but error %v ", err)
}
re, err := g.Eval(L(ginq, data))
if err != nil {
t.Fatalf("except got columns from data but error %v", err)
}
t.Logf("ginq select got %v", re)
}
Ginq 的機制和使用
Ginq 的結構比較特殊,可以把 ginq 函數看成一個特化的 lambda。它接受一組 ginq 子句,將其串成一個處理序列,生成一個接受 List 參數的lambda函子,我們稱之為 ginq 查詢。給這個查詢傳入一個 List ,它會順序調用每個子句,最終返回結果。
在這個過程中,ginq的一級子句很重要。它們接受List,并將輸出結果返回到 ginq ,ginq 再將其輸出到下一個子句。目前這里沒有做優化,每一步都會生成一個中間 List 。所以使用的時候盡量將 where 這樣的過濾子句放在前面,可以提高效率,節省內存。
select
Select 子句接受一個函數,然后生成一個函數。新的函數接受 ginq 傳入的 list,再返回一個list。特別的,我們提供一個 fs (即 fields)函數,這個函數接受一組函數,生成一個接受單個數據,返回List 的函數。這個函數可以跟 select 組合,形成一個類似 SQL 的列選擇功能。示例如下:
func TestGinqSelectFields(t *testing.T) {
data := QL(
L(0, 1, 2, 3, 4, 5),
L(1, 2, 3, 4, 5, 6),
L(2, 3, 4, 5, 6, 7),
L(3, 4, 5, 6, 7, 8))
g := NewGispWith(
map[string]Toolbox{"axiom": Axiom, "props": Propositions, "utils": Utils},
map[string]Toolbox{"time": Time})
g.DefAs("data", data)
ginq, err := g.Parse(`
(ginq (select (fs [1] [2] [4])))
`)
if err != nil {
t.Fatalf("except got a ginq query but error %v ", err)
}
re, err := g.Eval(L(ginq, data))
if err != nil {
t.Fatalf("except got columns from data but error %v", err)
}
t.Logf("ginq select got %v", re)
}
where
where 子句接受一個判斷函數為參數,返回一個過濾器。它對傳入的 List 中的元素逐個調用給定的判斷函數,只有返回值為 true 的才放到輸出結果中,最終生成一個 List,其中的內容是所有通過判斷的數據。
func TestGinqWhereSelect(t *testing.T) {
data := QL(
L(0, 1, 2, 3, 4, 5),
L(1, 2, 3, 4, 5, 6),
L(2, 3, 4, 5, 6, 7),
L(3, 4, 5, 6, 7, 8))
g := NewGispWith(
map[string]Toolbox{"axiom": Axiom, "props": Propositions, "utils": Utils},
map[string]Toolbox{"time": Time})
g.DefAs("data", data)
ginq, err := g.Parse(`
(ginq
(where (lambda (r) (< 1 r[0])))
(select (fs [1] [2] [4]))
)
`)
if err != nil {
t.Fatalf("except got a ginq query but error %v ", err)
}
re, err := g.Eval(L(ginq, data))
if err != nil {
t.Fatalf("except got columns from data but error %v", err)
}
t.Logf("ginq select got %v", re)
}
groupby
groupby 執行分組統計操作。下例為了更清楚的表現Ginq的串行操作,將groupby中的分組子句拆解成一個新的qinq,其實后面的例子我們會看到更簡潔的寫法。
func TestGinqGroupBy(t *testing.T) {
data := QL(
L(0, 1, 2, 3, 4, 5),
L(1, 2, 3, 4, 5, 6),
L(0, 1, 2, 3, 4, 5),
L(1, 2, 3, 4, 5, 6),
L(2, 3, 4, 5, 6, 7),
L(1, 2, 3, 4, 5, 6),
L(2, 3, 4, 5, 6, 7),
L(3, 4, 5, 6, 7, 8))
g := NewGispWith(
map[string]Toolbox{"axiom": Axiom, "props": Propositions, "utils": Utils},
map[string]Toolbox{"time": Time})
g.DefAs("data", data)
ginq, err := g.Parse(`
(ginq
(groupby [0] (ginq (select [5]) sum))
)
`)
if err != nil {
t.Fatalf("except got a ginq query but error %v ", err)
}
re, err := g.Eval(L(ginq, data))
if err != nil {
t.Fatalf("except got columns from data but error: %v", err)
}
t.Logf("ginq select got %v", re)
}
統計函數
為了更方便的在ginq中對一個 List 進行統計計算,我們實現了對應的一級子句 sums、maxs、mins、avgs。它們接受fs這樣的行處理函數,可以先用行處理函數對單個數據項進行計算后,再做統計。在我們的業務中,典型如訂單,每一個消費項先進行結算,再做總計。
下例演示了groupby、sums、where和中括號表達式的組合。(sums [5]) 隱藏了內部的 select fs 和sum等多步操作。
func TestGinqGroupBySumSelectWhere(t *testing.T) {
data := QL(
L(0, 1, 2, 3, 4, 5),
L(1, 2, 3, 4, 5, 6),
L(0, 1, 2, 3, 4, 5),
L(1, 2, 3, 4, 5, 6),
L(2, 3, 4, 5, 6, 7),
L(1, 2, 3, 4, 5, 6),
L(2, 3, 4, 5, 6, 7),
L(3, 4, 5, 6, 7, 8))
g := NewGispWith(
map[string]Toolbox{"axiom": Axiom, "props": Propositions, "utils": Utils},
map[string]Toolbox{"time": Time})
g.DefAs("data", data)
ginq, err := g.Parse(`
(ginq
(groupby [0] (sums [5]))
(where (lambda (x) (> 10 x[1])))
)
`)
if err != nil {
t.Fatalf("except got a ginq query but error %v ", err)
}
re, err := g.Eval(L(ginq, data))
if err != nil {
t.Fatalf("except got group sum from data but error: %v", err)
}
t.Logf("ginq group sum select got %v", re)
}
而單列的“平凡”數據集,其實groupby sum過程是這樣的:
func TestGinqGroupBy(t *testing.T) {
data := QL(
L(0, 1, 2, 3, 4, 5),
L(1, 2, 3, 4, 5, 6),
L(0, 1, 2, 3, 4, 5),
L(1, 2, 3, 4, 5, 6),
L(2, 3, 4, 5, 6, 7),
L(1, 2, 3, 4, 5, 6),
L(2, 3, 4, 5, 6, 7),
L(3, 4, 5, 6, 7, 8))
g := NewGispWith(
map[string]Toolbox{"axiom": Axiom, "props": Propositions, "utils": Utils},
map[string]Toolbox{"time": Time})
g.DefAs("data", data)
ginq, err := g.Parse(`
(ginq
(groupby [0] (ginq (select [5]) sum))
)
`)
if err != nil {
t.Fatalf("except got a ginq query but error %v ", err)
}
re, err := g.Eval(L(ginq, data))
if err != nil {
t.Fatalf("except got columns from data but error: %v", err)
}
t.Logf("ginq select got %v", re)
}
這里需要注意的是,sum、max、min、avg、count等函數不同于 sums 這樣的統計組合子函數,它直接構成 List 到 統計結果的函數,不另組合行處理函數。
排序
同樣,ginq也提供了處理簡單序列的sort:
func TestGinqSort(t *testing.T) {
data := QL(
L(0, 1, 2, 3, 4, 5),
L(1, 2, 3, 4, 5, 6),
L(0, 1, 2, 3, 4, 2),
L(1, 2, 3, 4, 5, 6),
L(2, 3, 4, 5, 6, 7),
L(1, 2, 3, 4, 5, 3),
L(2, 3, 4, 5, 6, 4),
L(3, 4, 5, 6, 7, 8))
g := NewGispWith(
map[string]Toolbox{"axiom": Axiom, "props": Propositions, "utils": Utils},
map[string]Toolbox{"time": Time})
g.DefAs("data", data)
ginq, err := g.Parse(`
(ginq
(select [4])
sort
)
`)
if err != nil {
t.Fatalf("except got a ginq sort but error %v ", err)
}
re, err := g.Eval(L(ginq, data))
if err != nil {
t.Fatalf("except got ginq sort from data but error: %v", err)
}
t.Logf("ginq sort got %v", re)
}
和基于自定義判斷函數的 sortby
func TestGinqSortBy(t *testing.T) {
data := QL(
L(0, 1, 2, 3, 4, 5),
L(1, 2, 3, 4, 5, 6),
L(0, 1, 2, 3, 4, 2),
L(1, 2, 3, 4, 5, 6),
L(2, 3, 4, 5, 6, 7),
L(1, 2, 3, 4, 5, 3),
L(2, 3, 4, 5, 6, 4),
L(3, 4, 5, 6, 7, 8))
g := NewGispWith(
map[string]Toolbox{"axiom": Axiom, "props": Propositions, "utils": Utils},
map[string]Toolbox{"time": Time})
g.DefAs("data", data)
ginq, err := g.Parse(`
(ginq
(select (fs [3] [1] [5]))
(sortby (lambda (x y) (< x[2] y[2])))
)
`)
if err != nil {
t.Fatalf("except got a ginq sortby but error %v ", err)
}
re, err := g.Eval(L(ginq, data))
if err != nil {
t.Fatalf("except got ginq sortby from data but error: %v", err)
}
t.Logf("ginq sort got %v", re)
}
各個ginq子句其實可以作為獨立的函數調用,使用ginq環境主要是它會根據子句判斷求值方式,寫起來可以比較簡潔。提高一致性。我們也可以嘗試定制一些新的ginq子句組合使用。
并發
我們也提供了 go 關鍵字和 chan 關鍵字的封裝,不過目前應用中完全沒有用到,所以沒有經過測試。
擴展
gisp 的擴展主要是兩部分,一個是通過在 gisp 內注冊 go 類型,實現類型擴展。
func TestTypeFound(t *testing.T) {
m := money{9.99, "USD"}
g := NewGisp(map[string]Toolbox{
"axioms": Axiom,
"props": Propositions,
})
g.DefAs("money", reflect.TypeOf(m))
_, err := g.Parse("(var bill::money)")
if err != nil {
t.Fatalf("except define a money var but error: %v", err)
}
g.Setvar("bill", m)
mny, ok := g.Lookup("bill")
if !ok {
t.Fatalf("money var bill as %v not found ", m)
}
if !reflect.DeepEqual(m, mny) {
t.Fatalf("except got money var bill as %v but %v", m, mny)
}
}
上例可以看到,只要定義一個值為reflect.Type 的變量,就可以將其視為一個類型。這里借鑒了一些動態語言的做法。
或者編寫自己的 gisp.Functor 函子實現,作為函數使用:
type Functor interface {
Task(env Env, args ...interface{}) (Lisp, error)
}
在gisp中調用函數時,是從 Task 傳入參數,此時函數可以不執行,只是將要執行的代碼封裝成一個新的 Lisp 返回,這個設計是為了兩方面,一個是在出現函數重載時,先做參數檢查,有錯誤的話及早返回,也可以在不執行代碼的情況下先校驗參數是否匹配。其次將來實現 go 關鍵字時,可以盡可能在異步任務之外先排除一些錯誤,然后讓任務執行在無參數的環境下,理想情況時這可以是一個封閉的沙箱。
自定義函子通常是若干個組成一個模塊,放進gisp調用,示例可以參見 axiom.go 等內部實現。典型的,Axioms模塊實現的非常簡單,而 Gisp 模塊則非常完整和復雜。可以看到兩種不同實現方式的利弊。
解釋器
目前默認的解釋器,設計目標是盡可能輕量。它有buildin的概念,如果將模塊(通常是一個 gisp.Toolkit 實現) 放到 buildin模板,調用它的成員時不需要 m.fun 這樣的dot 表達式,直接給出命名就可以。否則要指定模塊名。構造 Gisp 解釋器對象,有兩個工具方法。NewGisp接受一個map[string]interface{} 作為buildin模塊,而 NewGispWith 則多接受一個ext字典,作為需要顯示引用模塊名的模塊定義。
前面幾個例子中都有引入一些buildin或ext模塊的行為,而下面這個例子甚至沒有引入任何模塊,gisp仍然可以執行一些邏輯。
func TestParseFloat(t *testing.T) {
g := NewGisp(map[string]Toolbox{})
gisp := *g
data := "3.14"
ret, err := gisp.Parse(data)
if err != nil {
t.Fatalf("except Float(3.14) but error: %v", err)
}
if ret.(Float) != Float(3.14) {
t.Fatalf("except got Float(3.14) but %v", ret)
}
}
總結
以上是生活随笔為你收集整理的gophp解释器_【干货】Gisp 解释器 Golang 辅助开发工具的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 电脑上如何使用耳机麦克风(耳机怎么在电脑
- 下一篇: phpst安装memcache扩展_在