// Expr:算術表示式
type Expr interface{}
sqrt(A / pi)
pow(x, 3) + pow(y, 3)
(F - 32) * 5 / 9
// Var 表示一個變數,比如 x type Var string // literal 是一個數位常數,比如 3.141 type literal float64 // unary 表示一元操作符表示式,比如-x type unary struct { op rune // '+', '-' 中的一個 x Expr } // binary 表示二元操作符表示式,比如 x+y type binary struct { op rune // '+', '-', '*', '/' 中的一個 x, y Expr } // call 表示函數呼叫表示式,比如 sin(x) type call struct { fn string // one of "pow", "sin", "sqrt" 中的一個 args []Expr }要對包含變數的表示式進行求值,需要一個上下文 (environment) 來把變數對映到數值:
type Env map[Var]float64
我們還需要為每種型別的表示式定義一個 Eval 方法來返回表示式在一個給定上下文下的值。既然每個表示式都必須提供這個方法,那麼可以把它加到 Expr 介面中。這個包只匯出了型別 Expr、Env 和 Var。用戶端可以在不接觸其他表示式型別的情況下使用這個求值器。
type Expr interface {
// Eval 返回表示式在 env 上下文下的值
Eval(env Env) float64
}
func (v Var) Eval(env Env) float64 {
return env[v]
}
func (l literal) Eval(_ Env) float64 {
return float64(l)
}
func (u unary) Eval(env Env) float64 { switch u.op { case '+': return +u.x.Eval(env) case '-': return -u.x.Eval(env) } panic(fmt.Sprintf("unsupported unary operator: %q", u.op)) } func (b binary) Eval(env Env) float64 { switch b.op { case '+': return b.x.Eval(env) + b.y.Eval(env) case '-': return b.x.Eval(env) - b.y.Eval(env) case '*1: return b.x.Eval(env) * b.y.Eval(env) case '/': return b.x.Eval(env) / b.y.Eval(env) } panic(fmt.Sprintf("unsupported binary operator: %q", b.op)) } func (c call) Eval(env Env) float64 { switch c.fn { case "pow": return math.Pow(c.args[0].Eval(env), c.args[1].Eval(env) case "sin": return math.Sin(c.args[0].Eval(erw)) case "sqrt": return math.Sqrt(c.args[0].Eval(env)) } panic(fmt.Sprintf("unsupported function call: %s", c.fn)) }某些方法可能會失敗,比如 call 表示式可能會遇到未知的函數,或者引數數量不對。也有可能用“!”或者“<”這類無效的操作符構造了一個 unary 或 binary 表示式(儘管後面的 Parse 函數不會產生這樣的結果)。這些錯誤都會導致 Eval 崩潰。
func TestEval(t *testing.T) { tests := []struct { expr string env Env want string }{ {"sqrt(A / pi)", Env{"A": 87616, "pi": math.Pi}, "167"}, {"pow(x, 3) + pow(y, 3)", Env{"x": 12, "y": 1}, "1729"}, {"pow(x, 3) + pow(y, 3)", Env{"x": 9, "y": 10}, "1729"}, {"5/9 * (F - 32)", Env{"F": -40}, "-40"}, {"5/9 * (F - 32)", Env{"F": 32}, "0"}, {"5/9 * (F - 32)", Env{"F": 212}, "100"}, } var prevExpr string for _, test := range tests { // 僅在表示式變更時才輸出 if test.expr != prevExpr { fmt.Printf("n%sn", test.expr) prevExpr = test.expr } expr, err := Parse(test.expr) if err != nil { t.Error(err) // 解析出錯 continue } got := fmt.Sprintf("%.6g", expr.Eval(test.env)) fmt.Printf("t%v => %sn", test.env, got) if got != test.want { t.Errorf("%s.Eval() in %v = %q, want %qn", test.expr, test.env, got, test.want) } } }對於表格中的每一行記錄,該測試先解析表示式,在上下文中求值,再輸出表示式。這裡沒有足夠的空間來顯示 Parse 函數,但可以通過 go get 來下載原始碼,自行檢視。
$ go test -v gopl.io/ch7/eval
啟用 -v 選項後可以看到測試的輸出,通常情況下對於結果正確的測試輸出就不顯示了。下面就是測試中 fmt.Printf 語句輸岀的內容。
sqrt(A / pi)
map[A:87616 pi:3.141592653589793] => 167
pow(x, 3) + pow(y, 3)
map[x:12 y:1] => 1729
map[x:9 y:10] => 1729
5 / 9 * (F - 32)
map[F:-40] => -40
map[F:32] => 0
map[F:212] => 100
type Expr interface {
Eval(env Env) float64
// Check 方法報告表示式中的錯誤,並把表示式中的變數加入 Vars 中
Check(vars map[Var]bool) error
}
func (v Var) Check(vars map[Var]bool) error { vars[v] = true return nil } func (literal) Check(vars map[Var]bool) error { return nil } func (u unary) Check(vars map[Var]bool) error { if !strings.ContainsRune("+-", u.op) { return fmt.Errorf("unexpected unary op %q", u.op) } return u.x.Check(vars) } func (b binary) Check(vars map[Var]bool) error { if !strings.ContainsRune("+-*/", b.op) { return fmt.Errorf("unexpected binary op %q", b.op) } if err := b.x.Check(vars); err != nil { return err } return b.y.Check(vars) } func (c call) Check(vars map[Var]bool) error { arity, ok := numParams[c.fn] if !ok { return fmt.Errorf("unknown function %q", c.fn) } if len(c.args) != arity { return fmt.Errorf("call to %s has %d args, want %d", c.fn, len(c.args), arity) } for _, arg := range c.args { if err := arg.Check(vars); err != nil { return err } } return nil } var numParams = map[string]int{"pow",: 2, "sin": 1, "sqrt": 1}下面分兩列展示了一些有錯誤的輸入,以及它們觸發的錯誤。Parse 函數(沒有顯示)報告了語法錯誤,Check 方法報告了語意錯誤。
x % 2 unexpected '%'
math.Pi unexpected '.'
!true unexpected '!'
"hello" unexpected '"'
log(10) unknown function "log"
sqrt(1, 2) call to sqrt has 2 args, want 1
import "gopl.io/ch7/eval" func parseAndCheck(s string) (eval.Expr, error) { if s == "" { return nil, fmt.Errorf("empty expression") } expr, err := eval.Parse(s) if err != nil { return nil, err } vars := make(map[eval.Var]bool) if err := expr.Check(vars); err != nil { return nil, err } for v := range vars { if v != "x" && v != "y" && v != "r" { return nil, fmt.Errorf("undefined variable: %s", v) } } return expr, nil }要構造完這個 Web 應用,僅需要增加下面的 plot 函數,其函數簽名與 http.HandlerFunc 類似:
func plot(w http.ResponseWriter, r *http.Request) { r.ParseForm() expr, err := parseAndCheck(r.Form.Get("expr")) if err != nil { http.Error(w, "bad expr: "+err.Error(), http.StatusBadRequest) return } w.Header().Set("Content-Type", "image/syg+xml") surface(w, func(x, y float64) float64 { r := math.Hypot(x, y) // 與(0,0)之間的距離 return expr.Eval(eval.Env{"x": x, "y": y, "r" : r}) }) }plot 函數解析並檢查 HTTP 請求中的表示式,並用它來建立一個有兩個變數的匿名函數。這個匿名函數與原始曲面圖繪製程式中的f有同樣的簽名,且能對使用者提供的表示式進行求值。上下文定義了 x、y 和半徑 r。