【干货】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(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)
	}
}

 

 

本文来自:ITEYE资讯

感谢作者:mengyidan1988

查看原文:【干货】Gisp 解释器 Golang 辅助开发工具

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。