Gox語言語法速覽-GX4.2 Q Language Manual (Q語言手冊) 附錄

Gox語言的語法基本脫胎於初始的Q語言(Qlang,現已改變路線,感謝原作者),下面是Q語言語法速覽供參考。

Q Language Manual (Q語言手冊)

運算符

基本上 Go 語言的操作符都支持,且操作符優先級相同。包括:

  • '+'、'-'、'*'、'/'、'%'、'='
  • ''、'&'、'&'、'|'、'<<'、'>>'
  • '+='、'-='、'*='、'/='、'%='、'++'、'--'
  • '='、'&='、'&='、'|='、'<<='、'>>='
  • '!'、'>='、'<='、'>'、'<'、'=='、'!='、'&&'、'||'
  • '<-' (chan操作符)

類型

原理上支持所有 Go 語言中的類型。典型有:

  • 基本類型:int、float (在 Go 語言裏面是 float64)、string、byte、bool、var(在 Go 語言裏面是 interface{})。
  • 複合類型:slice、map、chan
  • 用戶自定義:函數(閉包)、類成員函數、類

常量

  • 布爾類型:true、false(由 builtin 模塊支持)
  • var 類型:nil(由 builtin 模塊支持)
  • 浮點類型:pi、e、phi (由 math 模塊支持)

變量及初始化

基本類型

a = 1 // 創建一個 int 類型變量,並初始化爲 1
b = "hello" // string 類型
c = true // bool 類型
d = 1.0 // float 類型
e = 'h' // byte 類型

string 類型

a = "hello, world"

和 Go 語言類似,string 有如下內置的操作:

a = "hello" + "world" // + 爲字符串連接操作符
n = len(a) // 取 a 字符串的長度
b = a[1] // 取 a 字符串的某個字符,得到的 b 是 byte 類型
c = a[1:4] // 取子字符串

slice 類型

a = [1, 2, 3] // 創建一個 int slice,並初始化爲 [1, 2, 3]
b = [1, 2.3, 5] // 創建一個 float slice
c = ["a", "b", "c"] // 創建一個 string slice
d = ["a", 1, 2.3] // 創建一個 var slice (等價於 Go 語言的 []interface{})
e = make([]int, len, cap) // 創建一個 int slice,並將長度設置爲 len,容量設置爲 cap
f = make([][]int, len, cap) // 創建一個 []int 的 slice,並將長度設置爲 len,容量設置爲 cap
g = []byte{1, 2, 3} // 創建一個 byte slice,並初始化爲 [1, 2, 3]
h = []byte(nil) // 創建一個空 byte slice

和 Go 語言類似,slice 有如下內置的操作:

a = append(a, 4, 5, 6) // 含義與 Go 語言完全一致
n = len(a) // 取 a 的元素個數
m = cap(a) // 取 slice a 的容量
ncopy = copy(a, b) // 複製 b 的內容到 a,複製的長度 ncopy = min(len(a), len(b))
b1 = b[2] // 取 b 這個 slice 中 index=2 的元素
b[2] = 888 // 設置 b 這個 slice 中 index=2 的元素值爲 888
b[1], b[2], b[3] = 777, 888, 999 // 設置 b 這個 slice 中 index=1, 2, 3 的三個元素值
b2 = b[1:4] // 取子slice

特別地,在 qlang 中可以這樣賦值:

x, y, z = [1, 2, 3]

結果是 x = 1, y = 2, z = 3。

實際上,qlang 支持多返回值就是通過 slice 的多賦值完成:

  • 對於那些返回了多個值的 Go 函數,在 qlang 會理解爲返回 var slice,也就是 []interface{}。

舉個例子:

f, err = os.Open(fname)

這個例子,在 Go 裏面返回的是 (*os.File, error)。但是 qlang 中是 var slice。

map 類型

a = {"a": 1, "b": 2, "c": 3} // 得到 map[string]int 類型的對象
b = {"a": 1, "b", 2.3, "c": 3} // 得到 map[string]float64 類型的對象
c = {1: "a", 2: "b", 3: "c"} // 得到 map[int]string 類型的對象
d = {"a": "hello", "b": 2.0, "c": true} // 得到 map[string]interface{} 類型的對象
e = make(map[string]int) // 創建一個空的 map[string]int 類型的對象
f = make(map[string]map[string]int) // 創建一個 map[string]map[string]int 類型的對象
g = map[string]int16{"a": 1, "b": 2} // 創建一個 map[string]int16 類型的對象

和 Go 語言類似,map 有如下內置的操作:

n = len(a) // 取 a 的元素個數
x = a["b"] // 取 a map 中 key 爲 "b" 的元素
x = a.b // 含義同上,但如果 "b" 元素不存在會 panic
a["e"], a["f"], a["g"] = 4, 5, 6 // 同 Go 語言
a.e, a.f, a.g = 4, 5, 6 // 含義同 a["e"], a["f"], a["g"] = 4, 5, 6
delete(a, "e") // 刪除 a map 中的 "e" 元素

需要注意的是,a["b"] 的行爲和 Go 語言中略有不同。在 Go 語言中,常見的範式是:

x := map[string]int{"a": 1, "b": 2}
a, ok := x["a"] // 結果:a = 1, ok = true
if ok { // 判斷a存在的邏輯
    ...
}
c, ok2 := x["c"] // 結果:c = 0, ok2 = false
d := x["d"] // 結果:d = 0

而在 qlang 中是這樣的:

x = {"a": 1, "b": 2}
a = x["a"] // 結果:a = 1
if a != undefined { // 判斷a存在的邏輯
    ...
}
c = x["c"] // 結果:c = undefined,注意不是0,也不是nil
d = x["d"] // 結果:d = undefined,注意不是0,也不是nil

chan 類型

ch1 = make(chan bool, 2) // 得到 buffer = 2 的 chan bool
ch2 = make(chan int) // 得到 buffer = 0 的 chan int
ch3 = make(chan map[string]int) // 得到 buffer = 0 的 chan map[string]int

和 Go 語言類似,chan 有如下內置的操作:

n = len(ch1) // 取得chan當前的元素個數
m = cap(ch1) // 取得chan的容量
ch1 <- true // 向chan發送一個值
v = <-ch1 // 從chan取出一個值
close(ch1) // 關閉chan,被關閉的chan是不能寫,但是還可以讀(直到已經寫入的值全部被取完爲止)

需要注意的是,在 chan 被關閉後,<-ch 取得 undefined 值。所以在 qlang 中應該這樣:

v = <-ch1
if v != undefined { // 判斷chan沒有被關閉的邏輯
    ...
}

流程控制

if 語句

if booleanExpr1 {
    // ...
} elif booleanExpr2 {
    // ...
} elif booleanExpr3 {
    // ...
} else {
    // ...
}

switch 語句

switch expr {
case expr1:
    // ...
case expr2:
    // ...
default:
    // ...
}

或者:

switch {
case booleanExpr1:
    // ...
case booleanExpr2:
    // ...
default:
    // ...
}

for 語句

for { // 無限循環,需要在中間 break 或 return 結束
    ...
}

for booleanExpr { // 類似很多語言的 while 循環
    ...
}

for initExpr; conditionExpr; stepExpr {
    ...
}

典型例子:

for i = 0; i < 10; i++ {
    ...
}

另外我們也支持 for..range 語法:

for range collectionExpr { // 其中 collectionExpr 可以是 slice, map 或 chan
    ...
}

for index = range collectionExpr {
    ...
}

for index, value = range collectionExpr {
    ...
}

函數

函數和閉包

基本語法:

funcName = fn(arg1, arg2, argN) {
    //...
    return expr
}

這就定義了一個名爲 funcName 的函數。

本質上來說,函數只是和 1、"hello" 類似的一個值,只是值的類型是函數類型。

所有的用戶自定義函數,在 Go 裏面實際類型均爲 func(args ...interface{}) interface{}。

你可以在一個函數中引用外層函數的變量。如:

x = fn(a) {
    b = 1
    y = fn(t) {
        return b + t
    }
    return y(a)
}

println(x(3)) // 結果爲 4

但是如果你直接修改外層變量會報錯:

x = fn(a) {
    b = 1
    y = fn(t) {
        b = t // 這裏會拋出異常,因爲不能確定你是想定義一個新的 b 變量,還是要修改外層 x 函數的 b 變量
    }
    y(a)
    return b
}

如果你想修改外層變量,需要先引用它,如下:

x = fn(a) {
    b = 1
    y = fn(t) {
        b; b = t // 現在正常了,我們知道你要修改外層的 b 變量
    }
    y(a)
    return b
}

println(x(3)) // 輸出 3

不定參數

和 Go 語言類似,qlang 也支持不定參數的函數。例如內置的 max、min 都是不定參數的:

a = max(1.2, 3, 5, 6) // a 的值爲 float 類型的 6
b = max(1, 3, 5, 6) // b 的值爲 int 類型的 6

也可以自定義一個不定參數的函數,如:

x = fn(fmt, args...) {
    printf(fmt, args...)
}

這樣就得到了一個 x 函數,功能和內建的 printf 函數一模一樣。

多賦值

形式上,qlang 和 Go 語言一樣支持多賦值,也支持函數返回多個值:

x, y, z = 1, 2, 3.5
a, b, c = fn() {
    return 1, 2, 3.5 // 返回的是 var slice
}()

另外我們也支持:

x, y, z = [1, 2, 3.5]
a, b, c = fn() {
    return [1, 2, 3.5] // 返回的是 float slice
}()

需要注意的是,帶上[]進行多賦值和不帶[]進行多賦值在語義上有一點點不同。下面是例子:

x1, y1, z1 = 1, 2, 3.5
x2, y2, z2 = [1, 2, 3.5]
println(type(x1), type(x2))

結果表明 x1 的類型爲 int,而 x2 的類型是 float。

defer

是的,qlang 也支持 defer。這在處理系統資源(如文件、鎖等)釋放場景非常有用。一個典型場景:

f, err = os.Open(fname)
if err != nil {
    // 做些出錯處理
    return
}
defer f.Close()

// 正常操作這個 f 文件

值得注意的是:

在一個細節上 qlang 的 defer 和 Go 語言處理並不一致,那就是 defer 表達式中的變量值。在 Go 語言中,所有 defer 引用的變量均在 defer 語句時刻固定下來(如上面的 f 變量),後面任何修改均不影響 defer 語句的行爲。但 qlang 是會受到影響的。例如,假設你在 defer 之後,調用 f = nil 把 f 變量改爲 nil,那麼後面執行 f.Close() 時就會 panic。

匿名函數

所謂匿名函數,是指:

fn {
    ... // 一段複雜代碼
}

它等價於:

fn() {
    ... // 一段複雜代碼
}()

以前在 defer 要執行一段很複雜的代碼段時,我們往往這樣寫:

defer fn() {
    ... // 一段複雜代碼
}()

有了匿名函數,我們可以簡寫爲:

defer fn {
    ... // 一段複雜代碼
}

一個用戶自定義類型的基本語法如下:

Foo = class {
    fn SetAB(a, b) {
        this.a, this.b = a, b
    }
    fn GetA() {
        return this.a
    }
}

有了這個 class Foo,我們就可以創建 Foo 類型的 object 了:

foo = new Foo
foo.SetAB(3, "hello")
a = foo.GetA()
println(a) // 輸出 3

構造函數

在 qlang 中,構造函數只是一個名爲 _init 的成員方法(method):

Foo = class {
    fn _init(a, b) {
        this.a, this.b = a, b
    }
}

有了這個 class Foo 後,我們 new Foo 時就必須攜帶2個構造參數了:

foo = new Foo(3, "hello")
println(foo.a) // 輸出 3

goroutine

和 Go 語言一樣,qlang 中通過 go 關鍵字啓動一個新的 goroutine。如:

go println("this is a goroutine")

一個比較複雜的例子:

wg = sync.NewWaitGroup()
wg.Add(2)

go fn {
    defer wg.Done()
    println("in goroutine1")
}

go fn {
    defer wg.Done()
    println("in goroutine2")
}

wg.Wait()

這是一個經典的 goroutine 使用場景,把一個 task 分爲 2 個子 task,交給 2 個 goroutine 執行。

include

在 qlang 中,一個 .ql 文件可以通過 include 文法來將另一個 .ql 的內容包含進來。所謂包含,其實際的能力類似於將代碼拷貝粘貼過來。例如,在某個目錄下有 a.ql 和 b.ql 兩個文件。

其中 a.ql 內容如下:

println("in script A")

foo = fn() {
    println("in func foo:", a, b)
}

其中 b.ql 內容如下:

a = 1
b = 2

include "a.ql"

println("in script B")
foo()

如果 include 語句的文件名不是以 .ql 爲後綴,那麼 qlang 會認爲這是一個目錄名,併爲其補上 "/main.ql" 後綴。也就是說:

include "foo/bar.v1"

等價於:

include "foo/bar.v1/main.ql"

模塊及 import

在 qlang 中,模塊(module)是一個目錄,該目錄下要求有一個名爲 main.ql 的文件。模塊中的標識(ident)默認都是私有的。想要導出一個標識(ident),需要用 export 語法。例如:

a = 1
b = 2

println("in script A")

f = fn() {
    println("in func foo:", a, b)
}

export a, f

這個模塊導出了兩個標識(ident):整型變量 a 和 函數 f。

要引用這個模塊,我們需要用 import 文法:

import "foo/bar.v1"
import "foo/bar.v1" as bar2

bar.a = 100 // 將 bar.a 值設置爲 100
println(bar.a, bar2.a) // bar.a, bar2.a 的值現在都是 100

bar.f()

qlang 會在環境變量 QLANG_PATH 指示的目錄列表中查找 foo/bar.v1/main.ql 文件。如個沒有設置環境變量 QLANG_PATH,則會在 ~/qlang 目錄中查找。

將一個模塊 import 多次並不會出現什麼問題,事實上第二次導入不會發生什麼,只是增加了一個別名。

include vs. import

include 是拷貝粘貼,比較適合用於模塊內的內容組織。比如一個模塊比較複雜,全部寫在 main.ql 文件中過於冗長,則可以用 include 語句分解到多個文件中。include 不會通過 QLANG_PATH 來找文件,它永遠基於 __dir__(即 include 代碼所在腳本的目錄) 來定位文件。

import 是模塊引用,適合用於作爲業務分解的主要方式。import 基於 QLANG_PATH 這個環境變量搜尋被引用的模塊,而不是基於 __dir__

與 Go 語言的互操作性

qlang 是一個嵌入式語言,它的定位是作爲 Go 語言應用的運行時嵌入腳本。

作爲 Go 語言的伴生語言,它與 Go 語言有極佳的互操作性。任何 Go 語言的函數,可以幾乎不做任何包裝就可以直接在 qlang 中使用。

這太爽了!

定製 qlang

除了 qlang 語言的 import 支持外,qlang 的 Go 語言開發包也支持 Go package 編寫 qlang 模塊。

qlang 採用微內核設計,大部分你看到的功能,都通過 Go package 形式編寫的 qlang 模塊提供。你可以按需定製 qlang。

你可以自由定製你想要的 qlang 的樣子。在沒有引入任何模塊的情況下,qlang 連最基本的 '+'、'-'、'*'、'/' 都做不了,因爲提供這個能力的是 builtin 包。

在前面“快速入門”給出的精簡版本基礎上,我們可以自由添加各種模塊,如:

import (
    "qlang.io/lib/math"
    "qlang.io/lib/strconv"
    "qlang.io/lib/strings"
    ...
)

func main() {

    qlang.Import("math", math.Exports)
    qlang.Import("strconv", strconv.Exports)
    qlang.Import("strings", strings.Exports)

    ...
}

這樣,在 qlang 中就可以用 math.sin, strconv.itoa 等函數了。

如果你嫌 math.sin 太長,還可以將 math 模塊作爲 builtin 功能導入。這隻需要略微修改下導入的文法:

qlang.Import("", math.Exports) // 如此,你就可以直接用 sin 而不是 math.sin 了

製作 qlang 模塊

製作 qlang 模塊的成本極其低廉。我們打開 qlang.io/lib/strings 看看它是什麼樣的:

package strings

import (
    "strings"
)

var Exports = map[string]interface{}{
    "Contains":  strings.Contains,
    "Index":     strings.Index,
    "IndexAny":  strings.IndexAny,
    "Join":      strings.Join,
    "Title":     strings.Title,
    "ToLower":   strings.ToLower,
    "ToTitle":   strings.ToTitle,
    "ToUpper":   strings.ToUpper,
    "Trim":      strings.Trim,

    "NewReader":   strings.NewReader,
    "NewReplacer": strings.NewReplacer,
    ...
}

值得注意的一個細節是,我們幾乎不需要對 Go 語言的 strings package 中的函數進行任何包裝,你只需要把這個函數加入到導出表(Exports)即可。你也無需包裝 Go 語言中的類,比如上面的我們導出了 strings.NewReplacer,但是我們不必去包裝 strings.Replacer 類。這個類的所有功能可以直接使用。如:

strings.NewReplacer("?", "!").Replace("hello, world???") // 得到 "hello, world!!!"

這是 qlang 最強大的地方,近乎免包裝。甚至,你可以寫一個自動的 Go package 轉 qlang 模塊的工具,找到 Go package 所有導出的全局函數,加入到 Exports 表即完成了該 Go package 的包裝,幾乎零成本。

導出 Go 結構體

假設我們有 Go 結構體如下:

package foo

type Bar struct {
    X int
    Y string
}

func (p *Bar) GetX() int {
    return p.X
}

我們一樣可以把它加入到 Export 表進行導出:

import (
    qlang "qlang.io/spec"
)

var Exports = map[string]interface{}{
    "Bar": qlang.StructOf((*foo.Bar)(nil)),
}

這樣你就可以在 qlang 裏面使用這個結構體:

bar = &foo.Bar{X: 1, Y: "hello"}
x = bar.GetX()
y = bar.Y
println(x, y)

反射

在任何時候,你都可以用 type 函數來查看一個變量的實際類型,結果在 Go 語言中是 reflect.Type。如:

t1 = type(1) // 相當於調用 Go 語言中的 reflect.TypeOf

用 type 可以很好地研究 qlang 的內在實現。比如:

t2 = type(fn() {})

我們得到了 *Function。這說明儘管用戶自定義的函數原型多樣,但是其 Go 類型是一致的。

我們也可以看看用戶自定義的類型:

Foo = class { fn f() {} }
t1 = type(Foo)
t2 = type(Foo.f)

foo = new Foo
t3 = type(foo)
t4 = type(foo.f)

可以看到,class Foo 的 Go 類型是 *Class,而 object foo 的 Go 類型是 *Object。而 Foo.f 和普通用戶自定義函數一致,也是 *Function,但 foo.f 不一樣,它是 *Method 類型。

附錄

樣例代碼

求最大素數

輸入 n,求 < n 的最大素數。用法:

  • qlang maxprime.ql <N>
primes = [2, 3]
n = 1
limit = 9

isPrime = fn(v) {
    for i = 0; i < n; i++ {
        if v % primes[i] == 0 {
            return false
        }
    }
    return true
}

listPrimes = fn(max) {

    v = 5
    for {
        for v < limit {
            if isPrime(v) {
                primes = append(primes, v)
                if v * v >= max {
                    return
                }
            }
            v += 2
        }
        v += 2
        n; n++
        limit = primes[n] * primes[n]
    }
}

maxPrimeOf = fn(max) {

    if max % 2 == 0 {
        max--
    }

    listPrimes(max)
    n; n = len(primes)

    for {
        if isPrime(max) {
            return max
        }
        max -= 2
    }
}

// Usage: maxprime <Value>
//
if len(os.Args) < 2 {
    fprintln(os.Stderr, "Usage: maxprime <Value>")
    return
}

max, err = strconv.ParseInt(os.Args[1], 10, 64)
if err != nil {
    fprintln(os.Stderr, err)
    return 1
}
if max < 8 { // <8 的情況下,可直接建表,答案略
    return
}

max--
v = maxPrimeOf(max)
println(v)

計算器

實現一個支持四則運算及函數調用的計算器:


grammar = `

term = factor *('*' factor/mul | '/' factor/quo | '%' factor/mod)

doc = term *('+' term/add | '-' term/sub)

factor =
    FLOAT/pushFloat |
    '-' factor/neg |
    '(' doc ')' |
    (IDENT '(' doc %= ','/ARITY ')')/call
`

fntable = nil

Stack = class {

    fn _init() {
        this.stk = []
    }

    fn clear() {
        this.stk = this.stk[:0]
    }

    fn pop() {
        n = len(this.stk)
        if n > 0 {
            v = this.stk[n-1]
            this.stk = this.stk[:n-1]
            return v, true
        }
        return nil, false
    }

    fn push(v) {
        this.stk = append(this.stk, v)
    }

    fn popArgs(arity) {
        n = len(this.stk)
        if n < arity {
            panic("Stack.popArgs: unexpected")
        }
        args = make([]var, arity)
        copy(args, this.stk[n-arity:])
        this.stk = this.stk[:n-arity]
        return args
    }
}

Calculator = class {

    fn _init() {
        this.stk = new Stack
    }

    fn grammar() {
        return grammar
    }

    fn stack() {
        return this.stk
    }

    fn fntable() {
        return fntable
    }

    fn ret() {
        v, _ = this.stk.pop()
        this.stk.clear()
        return v
    }

    fn call(name) {
        f = fntable[name]
        if f == undefined {
            panic("function not found: " + name)
        }
        arity, _ = this.stk.pop()
        args = this.stk.popArgs(arity)
        ret = f(args...)
        this.stk.push(ret)
    }
}

fntable = {
    "sin": sin,
    "cos": cos,
    "pow": pow,
    "max": max,
    "min": min,

    "$mul": fn(a, b) { return a*b },
    "$quo": fn(a, b) { return a/b },
    "$mod": fn(a, b) { return a%b },
    "$add": fn(a, b) { return a+b },
    "$sub": fn(a, b) { return a-b },
    "$neg": fn(a) { return -a },

    "$call": Calculator.call,
    "$pushFloat": Stack.push,
    "$ARITY": Stack.push,
}

main { // 使用main關鍵字將主程序括起來,是爲了避免其中用的局部變量比如 err 對其他函數造成影響

    calc = new Calculator
    engine, err = interpreter(calc, nil)
    if err != nil {
        fprintln(os.Stderr, err)
        return 1
    }

    scanner = bufio.NewScanner(os.Stdin)
    for scanner.Scan() {
        line = strings.Trim(scanner.Text(), " \t\r\n")
        if line != "" {
            err = engine.Eval(line)
            if err != nil {
                fprintln(os.Stderr, err)
            } else {
                printf("> %v\n\n", calc.ret())
            }
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章