mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2025-08-10 09:02:25 +00:00
回到简体
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
## 7.9. 示例: 表達式求值
|
||||
## 7.9. 示例: 表达式求值
|
||||
|
||||
在本節中,我們會構建一個簡單算術表達式的求值器。我們將使用一個接口Expr來表示Go語言中任意的表達式。現在這個接口不需要有方法,但是我們後面會爲它增加一些。
|
||||
在本节中,我们会构建一个简单算术表达式的求值器。我们将使用一个接口Expr来表示Go语言中任意的表达式。现在这个接口不需要有方法,但是我们后面会为它增加一些。
|
||||
|
||||
```go
|
||||
// An Expr is an arithmetic expression.
|
||||
type Expr interface{}
|
||||
```
|
||||
|
||||
我們的表達式語言由浮點數符號(小數點);二元操作符+,-,\*, 和/;一元操作符-x和+x;調用pow(x,y),sin(x),和sqrt(x)的函數;例如x和pi的變量;當然也有括號和標準的優先級運算符。所有的值都是float64類型。這下面是一些表達式的例子:
|
||||
我们的表达式语言由浮点数符号(小数点);二元操作符+,-,\*, 和/;一元操作符-x和+x;调用pow(x,y),sin(x),和sqrt(x)的函数;例如x和pi的变量;当然也有括号和标准的优先级运算符。所有的值都是float64类型。这下面是一些表达式的例子:
|
||||
|
||||
```go
|
||||
sqrt(A / pi)
|
||||
@@ -15,7 +15,7 @@ pow(x, 3) + pow(y, 3)
|
||||
(F - 32) * 5 / 9
|
||||
```
|
||||
|
||||
下面的五個具體類型表示了具體的表達式類型。Var類型表示對一個變量的引用。(我們很快會知道爲什麽它可以被輸出。)literal類型表示一個浮點型常量。unary和binary類型表示有一到兩個運算對象的運算符表達式,這些操作數可以是任意的Expr類型。call類型表示對一個函數的調用;我們限製它的fn字段隻能是pow,sin或者sqrt。
|
||||
下面的五个具体类型表示了具体的表达式类型。Var类型表示对一个变量的引用。(我们很快会知道为什么它可以被输出。)literal类型表示一个浮点型常量。unary和binary类型表示有一到两个运算对象的运算符表达式,这些操作数可以是任意的Expr类型。call类型表示对一个函数的调用;我们限制它的fn字段只能是pow,sin或者sqrt。
|
||||
|
||||
<u><i>gopl.io/ch7/eval</i></u>
|
||||
```go
|
||||
@@ -44,13 +44,13 @@ type call struct {
|
||||
}
|
||||
```
|
||||
|
||||
爲了計算一個包含變量的表達式,我們需要一個environment變量將變量的名字映射成對應的值:
|
||||
为了计算一个包含变量的表达式,我们需要一个environment变量将变量的名字映射成对应的值:
|
||||
|
||||
```go
|
||||
type Env map[Var]float64
|
||||
```
|
||||
|
||||
我們也需要每個表示式去定義一個Eval方法,這個方法會根據給定的environment變量返迴表達式的值。因爲每個表達式都必須提供這個方法,我們將它加入到Expr接口中。這個包隻會對外公開Expr,Env,和Var類型。調用方不需要獲取其它的表達式類型就可以使用這個求值器。
|
||||
我们也需要每个表示式去定义一个Eval方法,这个方法会根据给定的environment变量返回表达式的值。因为每个表达式都必须提供这个方法,我们将它加入到Expr接口中。这个包只会对外公开Expr,Env,和Var类型。调用方不需要获取其它的表达式类型就可以使用这个求值器。
|
||||
|
||||
```go
|
||||
type Expr interface {
|
||||
@@ -59,7 +59,7 @@ type Expr interface {
|
||||
}
|
||||
```
|
||||
|
||||
下面給大家展示一個具體的Eval方法。Var類型的這個方法對一個environment變量進行査找,如果這個變量沒有在environment中定義過這個方法會返迴一個零值,literal類型的這個方法簡單的返迴它眞實的值。
|
||||
下面给大家展示一个具体的Eval方法。Var类型的这个方法对一个environment变量进行查找,如果这个变量没有在environment中定义过这个方法会返回一个零值,literal类型的这个方法简单的返回它真实的值。
|
||||
|
||||
```go
|
||||
func (v Var) Eval(env Env) float64 {
|
||||
@@ -71,7 +71,7 @@ func (l literal) Eval(_ Env) float64 {
|
||||
}
|
||||
```
|
||||
|
||||
unary和binary的Eval方法會遞歸的計算它的運算對象,然後將運算符op作用到它們上。我們不將被零或無窮數除作爲一個錯誤,因爲它們都會産生一個固定的結果無限。最後,call的這個方法會計算對於pow,sin,或者sqrt函數的參數值,然後調用對應在math包中的函數。
|
||||
unary和binary的Eval方法会递归的计算它的运算对象,然后将运算符op作用到它们上。我们不将被零或无穷数除作为一个错误,因为它们都会产生一个固定的结果无限。最后,call的这个方法会计算对于pow,sin,或者sqrt函数的参数值,然后调用对应在math包中的函数。
|
||||
|
||||
```go
|
||||
func (u unary) Eval(env Env) float64 {
|
||||
@@ -111,9 +111,9 @@ func (c call) Eval(env Env) float64 {
|
||||
}
|
||||
```
|
||||
|
||||
一些方法會失敗。例如,一個call表達式可能未知的函數或者錯誤的參數個數。用一個無效的運算符如!或者<去構建一個unary或者binary表達式也是可能會發生的(盡管下面提到的Parse函數不會這樣做)。這些錯誤會讓Eval方法panic。其它的錯誤,像計算一個沒有在environment變量中出現過的Var,隻會讓Eval方法返迴一個錯誤的結果。所有的這些錯誤都可以通過在計算前檢査Expr來發現。這是我們接下來要講的Check方法的工作,但是讓我們先測試Eval方法。
|
||||
一些方法会失败。例如,一个call表达式可能未知的函数或者错误的参数个数。用一个无效的运算符如!或者<去构建一个unary或者binary表达式也是可能会发生的(尽管下面提到的Parse函数不会这样做)。这些错误会让Eval方法panic。其它的错误,像计算一个没有在environment变量中出现过的Var,只会让Eval方法返回一个错误的结果。所有的这些错误都可以通过在计算前检查Expr来发现。这是我们接下来要讲的Check方法的工作,但是让我们先测试Eval方法。
|
||||
|
||||
下面的TestEval函數是對evaluator的一個測試。它使用了我們會在第11章講解的testing包,但是現在知道調用t.Errof會報告一個錯誤就足夠了。這個函數循環遍歷一個表格中的輸入,這個表格中定義了三個表達式和針對每個表達式不同的環境變量。第一個表達式根據給定圓的面積A計算它的半徑,第二個表達式通過兩個變量x和y計算兩個立方體的體積之和,第三個表達式將華氏溫度F轉換成攝氏度。
|
||||
下面的TestEval函数是对evaluator的一个测试。它使用了我们会在第11章讲解的testing包,但是现在知道调用t.Errof会报告一个错误就足够了。这个函数循环遍历一个表格中的输入,这个表格中定义了三个表达式和针对每个表达式不同的环境变量。第一个表达式根据给定圆的面积A计算它的半径,第二个表达式通过两个变量x和y计算两个立方体的体积之和,第三个表达式将华氏温度F转换成摄氏度。
|
||||
|
||||
```go
|
||||
func TestEval(t *testing.T) {
|
||||
@@ -151,15 +151,15 @@ func TestEval(t *testing.T) {
|
||||
}
|
||||
```
|
||||
|
||||
對於表格中的每一條記録,這個測試會解析它的表達式然後在環境變量中計算它,輸出結果。這里我們沒有空間來展示Parse函數,但是如果你使用go get下載這個包你就可以看到這個函數。
|
||||
对于表格中的每一条记录,这个测试会解析它的表达式然后在环境变量中计算它,输出结果。这里我们没有空间来展示Parse函数,但是如果你使用go get下载这个包你就可以看到这个函数。
|
||||
|
||||
go test(§11.1) 命令會運行一個包的測試用例:
|
||||
go test(§11.1) 命令会运行一个包的测试用例:
|
||||
|
||||
```
|
||||
$ go test -v gopl.io/ch7/eval
|
||||
```
|
||||
|
||||
這個-v標識可以讓我們看到測試用例打印的輸出;正常情況下像這個一樣成功的測試用例會阻止打印結果的輸出。這里是測試用例里fmt.Printf語句的輸出:
|
||||
这个-v标识可以让我们看到测试用例打印的输出;正常情况下像这个一样成功的测试用例会阻止打印结果的输出。这里是测试用例里fmt.Printf语句的输出:
|
||||
|
||||
```
|
||||
sqrt(A / pi)
|
||||
@@ -175,9 +175,9 @@ pow(x, 3) + pow(y, 3)
|
||||
map[F:212] => 100
|
||||
```
|
||||
|
||||
幸運的是目前爲止所有的輸入都是適合的格式,但是我們的運氣不可能一直都有。甚至在解釋型語言中,爲了靜態錯誤檢査語法是非常常見的;靜態錯誤就是不用運行程序就可以檢測出來的錯誤。通過將靜態檢査和動態的部分分開,我們可以快速的檢査錯誤併且對於多次檢査隻執行一次而不是每次表達式計算的時候都進行檢査。
|
||||
幸运的是目前为止所有的输入都是适合的格式,但是我们的运气不可能一直都有。甚至在解释型语言中,为了静态错误检查语法是非常常见的;静态错误就是不用运行程序就可以检测出来的错误。通过将静态检查和动态的部分分开,我们可以快速的检查错误并且对于多次检查只执行一次而不是每次表达式计算的时候都进行检查。
|
||||
|
||||
讓我們往Expr接口中增加另一個方法。Check方法在一個表達式語義樹檢査出靜態錯誤。我們馬上會説明它的vars參數。
|
||||
让我们往Expr接口中增加另一个方法。Check方法在一个表达式语义树检查出静态错误。我们马上会说明它的vars参数。
|
||||
|
||||
```go
|
||||
type Expr interface {
|
||||
@@ -187,7 +187,7 @@ type Expr interface {
|
||||
}
|
||||
```
|
||||
|
||||
具體的Check方法展示在下面。literal和Var類型的計算不可能失敗,所以這些類型的Check方法會返迴一個nil值。對於unary和binary的Check方法會首先檢査操作符是否有效,然後遞歸的檢査運算單元。相似地對於call的這個方法首先檢査調用的函數是否已知併且有沒有正確個數的參數,然後遞歸的檢査每一個參數。
|
||||
具体的Check方法展示在下面。literal和Var类型的计算不可能失败,所以这些类型的Check方法会返回一个nil值。对于unary和binary的Check方法会首先检查操作符是否有效,然后递归的检查运算单元。相似地对于call的这个方法首先检查调用的函数是否已知并且有没有正确个数的参数,然后递归的检查每一个参数。
|
||||
|
||||
```go
|
||||
func (v Var) Check(vars map[Var]bool) error {
|
||||
@@ -236,7 +236,7 @@ func (c call) Check(vars map[Var]bool) error {
|
||||
var numParams = map[string]int{"pow": 2, "sin": 1, "sqrt": 1}
|
||||
```
|
||||
|
||||
我們在兩個組中有選擇地列出有問題的輸入和它們得出的錯誤。Parse函數(這里沒有出現)會報出一個語法錯誤和Check函數會報出語義錯誤。
|
||||
我们在两个组中有选择地列出有问题的输入和它们得出的错误。Parse函数(这里没有出现)会报出一个语法错误和Check函数会报出语义错误。
|
||||
|
||||
```
|
||||
x % 2 unexpected '%'
|
||||
@@ -248,11 +248,11 @@ log(10) unknown function "log"
|
||||
sqrt(1, 2) call to sqrt has 2 args, want 1
|
||||
```
|
||||
|
||||
Check方法的參數是一個Var類型的集合,這個集合聚集從表達式中找到的變量名。爲了保證成功的計算,這些變量中的每一個都必須出現在環境變量中。從邏輯上講,這個集合就是調用Check方法返迴的結果,但是因爲這個方法是遞歸調用的,所以對於Check方法填充結果到一個作爲參數傳入的集合中會更加的方便。調用方在初始調用時必須提供一個空的集合。
|
||||
Check方法的参数是一个Var类型的集合,这个集合聚集从表达式中找到的变量名。为了保证成功的计算,这些变量中的每一个都必须出现在环境变量中。从逻辑上讲,这个集合就是调用Check方法返回的结果,但是因为这个方法是递归调用的,所以对于Check方法填充结果到一个作为参数传入的集合中会更加的方便。调用方在初始调用时必须提供一个空的集合。
|
||||
|
||||
在第3.2節中,我們繪製了一個在編譯器才確定的函數f(x,y)。現在我們可以解析,檢査和計算在字符串中的表達式,我們可以構建一個在運行時從客戶端接收表達式的web應用併且它會繪製這個函數的表示的麴面。我們可以使用集合vars來檢査表達式是否是一個隻有兩個變量,x和y的函數——實際上是3個,因爲我們爲了方便會提供半徑大小r。併且我們會在計算前使用Check方法拒絶有格式問題的表達式,這樣我們就不會在下面函數的40000個計算過程(100x100個柵格,每一個有4個角)重複這些檢査。
|
||||
在第3.2节中,我们绘制了一个在编译器才确定的函数f(x,y)。现在我们可以解析,检查和计算在字符串中的表达式,我们可以构建一个在运行时从客户端接收表达式的web应用并且它会绘制这个函数的表示的曲面。我们可以使用集合vars来检查表达式是否是一个只有两个变量,x和y的函数——实际上是3个,因为我们为了方便会提供半径大小r。并且我们会在计算前使用Check方法拒绝有格式问题的表达式,这样我们就不会在下面函数的40000个计算过程(100x100个栅格,每一个有4个角)重复这些检查。
|
||||
|
||||
這個ParseAndCheck函數混合了解析和檢査步驟的過程:
|
||||
这个ParseAndCheck函数混合了解析和检查步骤的过程:
|
||||
|
||||
<u><i>gopl.io/ch7/surface</i></u>
|
||||
```go
|
||||
@@ -279,7 +279,7 @@ func parseAndCheck(s string) (eval.Expr, error) {
|
||||
}
|
||||
```
|
||||
|
||||
爲了編寫這個web應用,所有我們需要做的就是下面這個plot函數,這個函數有和http.HandlerFunc相似的籤名:
|
||||
为了编写这个web应用,所有我们需要做的就是下面这个plot函数,这个函数有和http.HandlerFunc相似的签名:
|
||||
|
||||
```go
|
||||
func plot(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -299,12 +299,12 @@ func plot(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||

|
||||
|
||||
這個plot函數解析和檢査在HTTP請求中指定的表達式併且用它來創建一個兩個變量的匿名函數。這個匿名函數和來自原來surface-plotting程序中的固定函數f有相同的籤名,但是它計算一個用戶提供的表達式。環境變量中定義了x,y和半徑r。最後plot調用surface函數,它就是gopl.io/ch3/surface中的主要函數,脩改後它可以接受plot中的函數和輸出io.Writer作爲參數,而不是使用固定的函數f和os.Stdout。圖7.7中顯示了通過程序産生的3個麴面。
|
||||
这个plot函数解析和检查在HTTP请求中指定的表达式并且用它来创建一个两个变量的匿名函数。这个匿名函数和来自原来surface-plotting程序中的固定函数f有相同的签名,但是它计算一个用户提供的表达式。环境变量中定义了x,y和半径r。最后plot调用surface函数,它就是gopl.io/ch3/surface中的主要函数,修改后它可以接受plot中的函数和输出io.Writer作为参数,而不是使用固定的函数f和os.Stdout。图7.7中显示了通过程序产生的3个曲面。
|
||||
|
||||
**練習 7.13:** 爲Expr增加一個String方法來打印美觀的語法樹。當再一次解析的時候,檢査它的結果是否生成相同的語法樹。
|
||||
**练习 7.13:** 为Expr增加一个String方法来打印美观的语法树。当再一次解析的时候,检查它的结果是否生成相同的语法树。
|
||||
|
||||
**練習 7.14:** 定義一個新的滿足Expr接口的具體類型併且提供一個新的操作例如對它運算單元中的最小值的計算。因爲Parse函數不會創建這個新類型的實例,爲了使用它你可能需要直接構造一個語法樹(或者繼承parser接口)。
|
||||
**练习 7.14:** 定义一个新的满足Expr接口的具体类型并且提供一个新的操作例如对它运算单元中的最小值的计算。因为Parse函数不会创建这个新类型的实例,为了使用它你可能需要直接构造一个语法树(或者继承parser接口)。
|
||||
|
||||
**練習 7.15:** 編寫一個從標準輸入中讀取一個單一表達式的程序,用戶及時地提供對於任意變量的值,然後在結果環境變量中計算表達式的值。優雅的處理所有遇到的錯誤。
|
||||
**练习 7.15:** 编写一个从标准输入中读取一个单一表达式的程序,用户及时地提供对于任意变量的值,然后在结果环境变量中计算表达式的值。优雅的处理所有遇到的错误。
|
||||
|
||||
**練習 7.16:** 編寫一個基於web的計算器程序。
|
||||
**练习 7.16:** 编写一个基于web的计算器程序。
|
||||
|
Reference in New Issue
Block a user