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)

}

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/329355.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Oracle入门(十四.8)之迭代控制:基本循环Loop

一、迭代控制&#xff1a;LOOP语句 循环多次重复一个语句或一系列语句。 PL / SQL提供了以下几种类型的循环&#xff1a;•没有全面条件执行重复操作的基本循环 •FOR循环&#xff0c;基于计数器执行迭代操作•WHILE循环根据条件执行重复操作二、基本循环LOOP语句的最简单形式…

phpst安装memcache扩展_在 Ubuntu/Debian 下安装 PHP7.3 教程

介绍最近的 PHP 7.3.0 已经在 2018 年12月6日 发布 GA&#xff0c;大家已经可以开始第一时间体验新版本了&#xff0c;这里先放出 PHP7.3 安装的教程以便大家升级。适用系统&#xff1a; Ubuntu 18.04 LTS / Ubuntu 16.04 LTS &#xff0f; Ubuntu 14.04 LTS / Debian 9 stretc…

升级.Net Core RC1的类库项目

微软终于发布了.Net Code RC2了&#xff0c;作为一个软粉当然是第一时间升级了。《升级.Net Core RC2的那些事》系列文章主要是记录本人升级RC2的相关步骤以及遇到过的坑。 第一篇先写类库项目&#xff08;Nuget包项目&#xff09;的升级 升级VS工具 这里只提供一个下载地址&am…

Oracle入门(十四.9)之迭代控制:WHILE和FOR循环

一、WHILE循环您可以使用WHILE循环重复一系列语句&#xff0c;直到控制条件不再为TRUE。 条件在每次迭代开始时进行评估。当条件为FALSE或NULL时&#xff0c;循环终止。 如果条件在循环开始时为FALSE或NULL&#xff0c;则不会执行进一步的迭代。 WHILE condition LOOPstatement…

为TFS配置跨平台的生成服务器Xplat (Ubuntu Linux)

1. 概述 从TFS 2015开始&#xff0c;微软开始支持跨平台的构建代理。你可以使用TFS的Xplat代理&#xff0c;方便的在基于IOS, Unix和Linux的服务器上搭建生成代理&#xff0c;实现构建、发布等功能。本文档已Ubuntu为例&#xff0c;指导如何安装和运行Xplat代理。 2. 配置TFS的…

分数优先遵循志愿php源码_分数优先 遵循志愿

本报讯 昨日&#xff0c;广东省考试院发布2019年我省普通高校招生平行志愿投档及录取实施办法。今年我省依旧实行普通高校招生平行志愿投档录取模式&#xff0c;按照“分数优先、遵循志愿”的原则&#xff0c;根据考生高考成绩高低排序和院校志愿先后顺序投档&#xff0c;投出…

Oracle入门(十四.10)之显式游标简介

一、上下文区域和游标Oracle服务器分配一个称为上下文区域的私有内存区域来存储由SQL语句处理的数据。 每个上下文区域&#xff08;因此每个SQL语句&#xff09;都有一个与其关联的游标。您可以将游标视为上下文区域的标签&#xff0c;或者将其作为指向上下文区域的指针。 事实…

1.(转)canal背景与工作原理

【README】 1.canal是一个工具&#xff0c;由阿里开源&#xff0c;用于解析mysql的binlog增量日志&#xff0c;重放日志还原出业务数据&#xff0c;下游可以送入 es&#xff0c;mysql&#xff0c;hbase等&#xff1b; 2.本文以下内容转自&#xff1a;GitHub - alibaba/canal:…

Dapper、Entity Framework 和混合应用

你大概注意到了&#xff0c;自 2008 年以来&#xff0c;我写过许多关于 Entity Framework&#xff08;即 Microsoft 对象关系映射器 (ORM)&#xff09;的文章&#xff0c;ORM 一直是主要的 .NET 数据访问 API。市面上还有许多其他 .NET ORM&#xff0c;但是有一个特殊类别因其强…

html让时间只展示年月日_如何用html写代码,使得在网页上显示当前的时间和日期...

展开全部在网页62616964757a686964616fe59b9ee7ad9431333363363537中动态的显示日期时间&#xff0c;一般都是使用js来实现&#xff0c;很简单&#xff0c;一看就会。网页中动态的显示系统日期时间function startTime(){var todaynew Date();//定义日期对象var yyyy today.get…

Oracle入门(十四.11)之使用显式游标属性

一、游标和记录 此示例中的游标基于SELECT语句&#xff0c;该语句仅检索每个表行的两列。 如果它检索了六列或七&#xff0c;八&#xff0c;二十个呢&#xff1f; DECLAREv_emp_id employees.employee_id%TYPE;v_last_name employees.last_name%TYPE;CURSOR emp_cursor ISSEL…

(转 )centos8安装mysql

【1】下载 mysql rpm包 MySQL :: Download MySQL Yum Repositoryhttps://dev.mysql.com/downloads/repo/yum/ 【2】安装mysql 根据官方文档安装&#xff0c;如下&#xff1a; MySQL :: A Quick Guide to Using the MySQL Yum Repositoryhttps://dev.mysql.com/doc/mysql-yu…

IIS负载均衡-Application Request Route详解第一篇: ARR介绍

说到负载均衡&#xff0c;相信大家已经不再陌生了&#xff0c;本系列主要介绍在IIS中可以采用的负载均衡的软件&#xff1a;微软的Application Request Route模块。 其实Application RequestRoute已经有很多文章介绍过了&#xff0c;但是有很多的文档都是英文的&#xff0c;笔者…

单位矩阵的逆矩阵是它本身吗_矩阵运算、单位矩阵与逆矩阵(二)

逆矩阵什么是逆矩阵&#xff1f;数有倒数&#xff1a;逆矩阵也是相同的概念&#xff0c;但我们写为A-1逆矩阵的定义计算逆矩阵我们怎么知道计算结果是正确的&#xff1f;我们把矩阵和逆矩阵相乘来看看&#xff1a;我们为什么需要逆矩阵&#xff0c;举个例子&#xff1a;一帮人坐…

Oracle入门(十四.12)之游标FOR循环

一、游标FOR循环游标FOR循环处理显式游标中的行。 这是一个快捷方式&#xff0c;因为游标被打开&#xff0c;循环中的每次迭代都会获取一次行&#xff0c;当处理最后一行时会退出循环&#xff0c;并且游标会自动关闭。 当最后一行被提取时&#xff0c;循环本身在迭代结束时自动…

结合Jexus + Kestrel 部署 asp.net core 生产环境

ASP.NET Core 是微软的全新的框架。这一框架的目标 ︰ 跨平台针对云应用优化解除 System.Web 的依赖。 获得下面三个方面的优势&#xff0c;你可以把它认为是一个C# 版本的NodeJS&#xff1a; 1&#xff09; 模块化实现 2&#xff09; 一切都尽可能的-异步 3&#xff09; 依赖关…

centos普通用户修改文件权限_用户管理(特殊权限、特殊属性、umask 默认权限 )

特殊权限suidLinux 系统文件除了9位基本权限&#xff0c;还有额外3位特殊权限&#xff0c;分别是  SUID(set uid)&#xff0c; -rwsr-xr-x  SGID(set gid)&#xff0c; -rw-r-sr-x  SBIT(sticky bit), -rw-r-xr-t  这3位特殊权限不建议使用(除系统默认的特殊权限可…

Oracle入门(十四.13)之带参数的游标

一、带参数的游标参数是一个变量&#xff0c;其名称用于游标声明中。 当游标打开时&#xff0c;参数值被传递给Oracle服务器&#xff0c;Oracle服务器使用它来决定要将哪些行检索到光标的活动集中。这意味着您可以在块中多次打开和关闭显式光标&#xff0c;或者在同一个块的不同…

4.canal抽取失败报is blocked because of many connection errors; unblock with ‘mysqladmin flush-hosts‘

【README】 1.本文po出了报错【messageXXX is blocked because of many connection errors; unblock with mysqladmin flush-hosts】的解决方法&#xff1b; 2.解决方法参考了 StackOverflow&#xff1a; mysql - How to unblock with mysqladmin flush hosts - Stack Overfl…

raid配置ssd为缓存_超融合硬件选配推荐指南 | 第三期:SSD 与 HDD

本期《SMTX OS 硬件选配推荐指南》继续与大家分享单机硬件配置中存储相关的内容——固态硬盘 SSD 和机械硬盘 HDD 章节。前两期摘录了单机硬件配置中的 CPU 选择和内存选择参考&#xff0c;感兴趣的读者可点击回顾。如果希望获取完整《指南》&#xff0c;可点击这里下载。单机硬…