This commit is contained in:
chai2010
2016-01-18 09:53:55 +08:00
parent 324d2c8925
commit ae133636eb
4 changed files with 68 additions and 4 deletions

View File

@@ -1,16 +1,22 @@
## 7.9. 示例: 表達式求值
在本節中我們會構建一個簡單算術表達式的求值器。我們將使用一個接口Expr來表示Go語言中任意的表達式。現在這個接口不需要有方法但是我們後面會爲它增加一些。
```go
// An Expr is an arithmetic expression.
type Expr interface{}
```
我們的表達式語言由浮點數符號(小數點);二元操作符+-\* 和/;一元操作符-x和+x調用pow(x,y)sin(x)和sqrt(x)的函數例如x和pi的變量當然也有括號和標準的優先級運算符。所有的值都是float64類型。這下面是一些表達式的例子
```go
sqrt(A / pi)
pow(x, 3) + pow(y, 3)
(F - 32) * 5 / 9
```
下面的五個具體類型表示了具體的表達式類型。Var類型表示對一個變量的引用。我們很快會知道爲什麽它可以被輸出。literal類型表示一個浮點型常量。unary和binary類型表示有一到兩個運算對象的運算符表達式這些操作數可以是任意的Expr類型。call類型表示對一個函數的調用我們限製它的fn字段隻能是powsin或者sqrt。
```go
// gopl.io/ch7/eval
// A Var identifies a variable, e.g., x.
@@ -37,18 +43,24 @@ type call struct {
args []Expr
}
```
爲了計算一個包含變量的表達式我們需要一個environment變量將變量的名字映射成對應的值
```go
type Env map[Var]float64
```
我們也需要每個表示式去定義一個Eval方法這個方法會根據給定的environment變量返迴表達式的值。因爲每個表達式都必鬚提供這個方法我們將它加入到Expr接口中。這個包隻會對外公開ExprEnv和Var類型。調用方不需要獲取其它的表達式類型就可以使用這個求值器。
```go
type Expr interface {
// Eval returns the value of this Expr in the environment env.
Eval(env Env) float64
}
```
下面給大家展示一個具體的Eval方法。Var類型的這個方法對一個environment變量進行査找如果這個變量沒有在environment中定義過這個方法會返迴一個零值literal類型的這個方法簡單的返迴它眞實的值。
```go
func (v Var) Eval(env Env) float64 {
return env[v]
@@ -58,7 +70,9 @@ func (l literal) Eval(_ Env) float64 {
return float64(l)
}
```
unary和binary的Eval方法會遞歸的計算它的運算對象然後將運算符op作用到它們上。我們不將被零或無窮數除作爲一個錯誤因爲它們都會産生一個固定的結果無限。最後call的這個方法會計算對於powsin或者sqrt函數的參數值然後調用對應在math包中的函數。
```go
func (u unary) Eval(env Env) float64 {
switch u.op {
@@ -96,9 +110,11 @@ func (c call) Eval(env Env) float64 {
panic(fmt.Sprintf("unsupported function call: %s", c.fn))
}
```
一些方法會失敗。例如一個call表達式可能未知的函數或者錯誤的參數個數。用一個無效的運算符如!或者<去構建一個unary或者binary表達式也是可能會發生的盡管下面提到的Parse函數不會這樣做)。這些錯誤會讓Eval方法panic其它的錯誤像計算一個沒有在environment變量中出現過的Var隻會讓Eval方法返迴一個錯誤的結果所有的這些錯誤都可以通過在計算前檢査Expr來發現這是我們接下來要講的Check方法的工作但是讓我們先測試Eval方法
下面的TestEval函數是對evaluator的一個測試它使用了我們會在第11章講解的testing包但是現在知道調用t.Errof會報告一個錯誤就足夠了這個函數循環遍歷一個表格中的輸入這個表格中定義了三個表達式和針對每個表達式不同的環境變量第一個表達式根據給定圓的面積A計算它的半徑第二個表達式通過兩個變量x和y計算兩個立方體的體積之和第三個表達式將華氏溫度F轉換成攝氏度
```go
func TestEval(t *testing.T) {
tests := []struct {
@@ -134,13 +150,17 @@ func TestEval(t *testing.T) {
}
}
```
對於表格中的每一條記録這個測試會解析它的表達式然後在環境變量中計算它輸出結果這里我們沒有空間來展示Parse函數但是如果你使用go get下載這個包你就可以看到這個函數
go test11.1) 命令會運行一個包的測試用例
```
$ go test -v gopl.io/ch7/eval
```
這個-v標識可以讓我們看到測試用例打印的輸出正常情況下像這個一樣成功的測試用例會阻止打印結果的輸出這里是測試用例里fmt.Printf語句的輸出
```
sqrt(A / pi)
map[A:87616 pi:3.141592653589793] => 167
@@ -154,9 +174,11 @@ pow(x, 3) + pow(y, 3)
map[F:32] => 0
map[F:212] => 100
```
幸運的是目前爲止所有的輸入都是適合的格式但是我們的運氣不可能一直都有甚至在解釋型語言中爲了靜態錯誤檢査語法是非常常見的靜態錯誤就是不用運行程序就可以檢測出來的錯誤通過將靜態檢査和動態的部分分開我們可以快速的檢査錯誤併且對於多次檢査隻執行一次而不是每次表達式計算的時候都進行檢査
讓我們往Expr接口中增加另一個方法Check方法在一個表達式語義樹檢査出靜態錯誤我們馬上會説明它的vars參數
```go
type Expr interface {
Eval(env Env) float64
@@ -164,7 +186,9 @@ type Expr interface {
Check(vars map[Var]bool) error
}
```
具體的Check方法展示在下面literal和Var類型的計算不可能失敗所以這些類型的Check方法會返迴一個nil值對於unary和binary的Check方法會首先檢査操作符是否有效然後遞歸的檢査運算單元相似地對於call的這個方法首先檢査調用的函數是否已知併且有沒有正確個數的參數然後遞歸的檢査每一個參數
```go
func (v Var) Check(vars map[Var]bool) error {
vars[v] = true
@@ -211,7 +235,9 @@ func (c call) Check(vars map[Var]bool) error {
var numParams = map[string]int{"pow": 2, "sin": 1, "sqrt": 1}
```
我們在兩個組中有選擇地列出有問題的輸入和它們得出的錯誤Parse函數這里沒有出現會報出一個語法錯誤和Check函數會報出語義錯誤
```
x % 2 unexpected '%'
math.Pi unexpected '.'
@@ -221,11 +247,13 @@ math.Pi unexpected '.'
log(10) unknown function "log"
sqrt(1, 2) call to sqrt has 2 args, want 1
```
Check方法的參數是一個Var類型的集合這個集合聚集從表達式中找到的變量名爲了保證成功的計算這些變量中的每一個都必鬚出現在環境變量中從邏輯上講這個集合就是調用Check方法返迴的結果但是因爲這個方法是遞歸調用的所以對於Check方法填充結果到一個作爲參數傳入的集合中會更加的方便調用方在初始調用時必鬚提供一個空的集合
在第3.2節中我們繪製了一個在編譯器才確定的函數f(x,y)。現在我們可以解析檢査和計算在字符串中的表達式我們可以構建一個在運行時從客戶端接收表達式的web應用併且它會繪製這個函數的表示的麴面我們可以使用集合vars來檢査表達式是否是一個隻有兩個變量,x和y的函數——實際上是3個因爲我們爲了方便會提供半徑大小r併且我們會在計算前使用Check方法拒絶有格式問題的表達式這樣我們就不會在下面函數的40000個計算過程100x100個柵格每一個有4個角重複這些檢査
這個ParseAndCheck函數混合了解析和檢査步驟的過程
```go
// gopl.io/ch7/surface
import "gopl.io/ch7/eval"
@@ -250,7 +278,9 @@ func parseAndCheck(s string) (eval.Expr, error) {
return expr, nil
}
```
爲了編寫這個web應用所有我們需要做的就是下面這個plot函數這個函數有和http.HandlerFunc相似的籤名
```go
func plot(w http.ResponseWriter, r *http.Request) {
r.ParseForm()