自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

從Java走進(jìn)Scala:構(gòu)建計(jì)算器 case類和模式匹配

開(kāi)發(fā) 后端
特定于領(lǐng)域的語(yǔ)言已經(jīng)成為一個(gè)熱門話題;很多函數(shù)性語(yǔ)言之所以受歡迎,主要是因?yàn)樗鼈兛梢杂糜跇?gòu)建特定于領(lǐng)域的語(yǔ)言。鑒于此,本文將著手構(gòu)建一個(gè)簡(jiǎn)單的計(jì)算器 DSL,以此來(lái)展示函數(shù)性語(yǔ)言的構(gòu)建 “外部” DSL 的強(qiáng)大功能。作者研究了 Scala 的一個(gè)新的特性:case類,并重新審視一個(gè)功能強(qiáng)大的特性:模式匹配。

上個(gè)月的文章發(fā)表后,我又收到了一些抱怨/評(píng)論,說(shuō)我迄今為止在本系列中所用的示例都沒(méi)涉及到什么實(shí)質(zhì)性的問(wèn)題。當(dāng)然在學(xué)習(xí)一個(gè)新語(yǔ)言的初期使用一些小例子是很合理的,而讀者想要看到一些更 “現(xiàn)實(shí)的” 示例,從而了解語(yǔ)言的深層領(lǐng)域和強(qiáng)大功能以及其優(yōu)勢(shì),這也是理所當(dāng)然的。因此,在這個(gè)月的文章中,我們來(lái)分兩部分練習(xí)構(gòu)建特定于領(lǐng)域的語(yǔ)言(DSL)— 本文以一個(gè)小的計(jì)算器語(yǔ)言為例。

特定于領(lǐng)域的語(yǔ)言

可能您無(wú)法(或沒(méi)有時(shí)間)承受來(lái)自于您的項(xiàng)目經(jīng)理給您的壓力,那么讓我直接了當(dāng)?shù)卣f(shuō)吧:特定于領(lǐng)域的語(yǔ)言無(wú)非就是嘗試(再一次)將一個(gè)應(yīng)用程序的功能放在它該屬于的地方 — 用戶的手中。

通過(guò)定義一個(gè)新的用戶可以理解并直接使用的文本語(yǔ)言,程序員成功擺脫了不停地處理 UI 請(qǐng)求和功能增強(qiáng)的麻煩,而且這樣還可以使用戶能夠自己創(chuàng)建腳本以及其他的工具,用來(lái)給他們所構(gòu)建的應(yīng)用程序創(chuàng)建新的行為。雖然這個(gè)例子可能有點(diǎn)冒險(xiǎn)(或許會(huì)惹來(lái)幾封抱怨的電子郵件),但我還是要說(shuō),DSL 的最成功的例子就是 Microsoft® Office Excel “語(yǔ)言”,用于表達(dá)電子表格單元格的各種計(jì)算和內(nèi)容。甚至有些人認(rèn)為 SQL 本身就是 DSL,但這次是一個(gè)旨在與關(guān)系數(shù)據(jù)庫(kù)相交互的語(yǔ)言(想象一下如果程序員要通過(guò)傳統(tǒng) API read()/write() 調(diào)用來(lái)從 Oracle 中獲取數(shù)據(jù)的話,那將會(huì)是什么樣子)。

這里構(gòu)建的 DSL 是一個(gè)簡(jiǎn)單的計(jì)算器語(yǔ)言,用于獲取并計(jì)算數(shù)學(xué)表達(dá)式。其實(shí),這里的目標(biāo)是要?jiǎng)?chuàng)建一個(gè)小型語(yǔ)言,這個(gè)語(yǔ)言能夠允許用戶來(lái)輸入相對(duì)簡(jiǎn)單的代數(shù)表達(dá)式,然后這個(gè)代碼來(lái)為它求值并產(chǎn)生結(jié)果。為了盡量簡(jiǎn)單明了,該語(yǔ)言不會(huì)支持很多功能完善的計(jì)算器所支持的特性,但我不也不想把它的用途限定在教學(xué)上 — 該語(yǔ)言一定要具備足夠的可擴(kuò)展性,以使讀者無(wú)需徹底改變?cè)撜Z(yǔ)言就能夠?qū)⑺米饕粋€(gè)功能更強(qiáng)大的語(yǔ)言的核心。這意味著該語(yǔ)言一定要可以被輕易地?cái)U(kuò)展,并要盡量保持封裝性,用起來(lái)不會(huì)有任何的阻礙。

換句話說(shuō),(最終的)目標(biāo)是要允許客戶機(jī)編寫代碼,以達(dá)到如下的目的:

清單 1. 計(jì)算器 DSL:目標(biāo)

  1. // This is Java using the Calculator  
  2. String s = "((5 * 10) + 7)";  
  3. double result = com.tedneward.calcdsl.Calculator.evaluate(s);  
  4. System.out.println("We got " + result); // Should be 57 

我們不會(huì)在一篇文章完成所有的論述,但是我們?cè)诒酒恼轮锌梢詫W(xué)習(xí)到一部分內(nèi)容,在下一篇文章完成全部?jī)?nèi)容。

從實(shí)現(xiàn)和設(shè)計(jì)的角度看,可以從構(gòu)建一個(gè)基于字符串的解析器來(lái)著手構(gòu)建某種可以 “挑選每個(gè)字符并動(dòng)態(tài)計(jì)算” 的解析器,這的確***誘惑力,但是這只適用于較簡(jiǎn)單的語(yǔ)言,而且其擴(kuò)展性不是很好。如果語(yǔ)言的目標(biāo)是實(shí)現(xiàn)簡(jiǎn)單的擴(kuò)展性,那么在深入研究實(shí)現(xiàn)之前,讓我們先花點(diǎn)時(shí)間想一想如何設(shè)計(jì)語(yǔ)言。

根據(jù)那些基本的編譯理論中最精華的部分,您可以得知一個(gè)語(yǔ)言處理器(包括解釋器和編譯器)的基本運(yùn)算至少由兩個(gè)階段組成:

解析器,用于獲取輸入的文本并將其轉(zhuǎn)換成 Abstract Syntax Tree(AST)。

代碼生成器(在編譯器的情況下),用于獲取 AST 并從中生成所需字節(jié)碼;或是求值器(在解釋器的情況下),用于獲取 AST 并計(jì)算它在 AST 里面所發(fā)現(xiàn)的內(nèi)容。
擁有 AST 就能夠在某種程度上優(yōu)化結(jié)果樹(shù),如果意識(shí)到這一點(diǎn)的話,那么上述區(qū)別的原因就變得更加顯而易見(jiàn)了;對(duì)于計(jì)算器,我們可能要仔細(xì)檢查表達(dá)式,找出可以截去表達(dá)式的整個(gè)片段的位置,諸如在乘法表達(dá)式中運(yùn)算數(shù)為 “0” 的位置(它表明無(wú)論其他運(yùn)算數(shù)是多少,運(yùn)算結(jié)果都會(huì)是 “0”)。

您要做的***件事是為計(jì)算器語(yǔ)言定義該 AST。幸運(yùn)的是,Scala 有 case 類:一種提供了豐富數(shù)據(jù)、使用了非常薄的封裝的類,它們所具有的一些特性使它們很適合構(gòu)建 AST。

關(guān)于 DSL 的更多信息

DSL 這個(gè)主題的涉及面很廣;它的豐富性和廣泛性不是本文的一個(gè)段落可以描述得了的。想要了解更多 DSL 信息的讀者可以查閱本文末尾列出的 Martin Fowler 的 “正在進(jìn)展中的圖書”;特別要注意關(guān)于 “內(nèi)部” 和 “外部” DSL 之間的討論。Scala 以其靈活的語(yǔ)法和強(qiáng)大的功能而成為***有力的構(gòu)建內(nèi)部和外部 DSL 的語(yǔ)言。更加具體的介紹,可參考51CTO之前發(fā)布的DSL領(lǐng)域特定語(yǔ)言初探一文。

#p#

case類

在深入到 AST 定義之前,讓我先簡(jiǎn)要概述一下什么是 case類。case類是使 scala 程序員得以使用某些假設(shè)的默認(rèn)值來(lái)創(chuàng)建一個(gè)類的一種便捷機(jī)制。例如,當(dāng)編寫如下內(nèi)容時(shí):

清單 2. 對(duì) person 使用 case類

  1. case class Person(first:String, last:String, age:Int)  
  2. {  
  3.  

Scala 編譯器不僅僅可以按照我們對(duì)它的期望生成預(yù)期的構(gòu)造函數(shù) — Scala 編譯器還可以生成常規(guī)意義上的 equals()、toString() 和 hashCode() 實(shí)現(xiàn)。事實(shí)上,這種 case類很普通(即它沒(méi)有其他的成員),因此 case 類聲明后面的大括號(hào)的內(nèi)容是可選的:

清單 3. 世界上最短的類清單

  1. case class Person(first:String, last:String, age:Int) 

這一點(diǎn)通過(guò)我們的老朋友 javap 很容易得以驗(yàn)證:

清單 4. 神圣的代碼生成器,Batman!

  1. C:\Projects\Exploration\Scala>javap Person  
  2. Compiled from "case.scala" 
  3. public class Person extends java.lang.Object implements scala.ScalaObject,scala.  
  4. Product,java.io.Serializable{  
  5.     public Person(java.lang.String, java.lang.String, int);  
  6.     public java.lang.Object productElement(int);  
  7.     public int productArity();  
  8.     public java.lang.String productPrefix();  
  9.     public boolean equals(java.lang.Object);  
  10.     public java.lang.String toString();  
  11.     public int hashCode();  
  12.     public int $tag();  
  13.     public int age();  
  14.     public java.lang.String last();  
  15.     public java.lang.String first();  

如您所見(jiàn),伴隨 case 類發(fā)生了很多傳統(tǒng)類通常不會(huì)引發(fā)的事情。這是因?yàn)?case 類是要與 Scala 的模式匹配(在 “集合類型” 中曾簡(jiǎn)短分析過(guò))結(jié)合使用的。

使用 case 類與使用傳統(tǒng)類有些不同,這是因?yàn)橥ǔK鼈兌疾皇峭ㄟ^(guò)傳統(tǒng)的 “new” 語(yǔ)法構(gòu)造而成的;事實(shí)上,它們通常是通過(guò)一種名稱與類相同的工廠方法來(lái)創(chuàng)建的:

清單 5. 沒(méi)有使用 new 語(yǔ)法?

  1. object App  
  2. {  
  3.   def main(args : Array[String]) : Unit =  
  4.   {  
  5.     val ted = Person("Ted""Neward"37)  
  6.   }  

case 類本身可能并不比傳統(tǒng)類有趣,或者有多么的與眾不同,但是在使用它們時(shí)會(huì)有一個(gè)很重要的差別。與引用等式相比,case 類生成的代碼更喜歡按位(bitwise)等式,因此下面的代碼對(duì) Java 程序員來(lái)說(shuō)有些有趣的驚喜:

清單 6. 這不是以前的類

  1. object App  
  2. {  
  3.   def main(args : Array[String]) : Unit =  
  4.   {  
  5.     val ted = Person("Ted""Neward"37)  
  6.     val ted2 = Person("Ted""Neward"37)  
  7.     val amanda = Person("Amanda""Laucher"27)  
  8.  
  9.     System.out.println("ted == amanda: " +  
  10.       (if (ted == amanda) "Yes" else "No"))  
  11.     System.out.println("ted == ted: " +  
  12.       (if (ted == ted) "Yes" else "No"))  
  13.     System.out.println("ted == ted2: " +  
  14.       (if (ted == ted2) "Yes" else "No"))  
  15.   }  
  16. }  
  17.  
  18. /*  
  19. C:\Projects\Exploration\Scala>scala App  
  20. ted == amanda: No  
  21. ted == ted: Yes  
  22. ted == ted2: Yes  
  23.  */ 

case 類的真正價(jià)值體現(xiàn)在模式匹配中,本系列的讀者可以回顧一下模式匹配(參見(jiàn) 本系列的第二篇文章,關(guān)于 Scala 中的各種控制構(gòu)造),模式匹配類似 Java 的 “switch/case”,只不過(guò)它的本領(lǐng)和功能更加強(qiáng)大。模式匹配不僅能夠檢查匹配構(gòu)造的值,從而執(zhí)行值匹配,還可以針對(duì)局部通配符(類似局部 “默認(rèn)值” 的東西)匹配值,case 還可以包括對(duì)測(cè)試匹配的保護(hù),來(lái)自匹配標(biāo)準(zhǔn)的值還可以綁定于局部變量,甚至符合匹配標(biāo)準(zhǔn)的類型本身也可以進(jìn)行匹配。

有了 case 類,模式匹配具備了更強(qiáng)大的功能,如清單 7 所示:

清單 7. 這也不是以前的 switch

  1. case class Person(first:String, last:String, age:Int);  
  2.  
  3. object App  
  4. {  
  5.   def main(args : Array[String]) : Unit =  
  6.   {  
  7.     val ted = Person("Ted""Neward"37)  
  8.     val amanda = Person("Amanda""Laucher"27)  
  9.  
  10.     System.out.println(process(ted))  
  11.     System.out.println(process(amanda))  
  12.   }  
  13.   def process(p : Person) =  
  14.   {  
  15.     "Processing " + p + " reveals that" +  
  16.     (p match  
  17.     {  
  18.       case Person(_, _, a) if a > 30 =>  
  19.         " they're certainly old." 
  20.       case Person(_, "Neward", _) =>  
  21.         " they come from good genes...." 
  22.       case Person(first, last, ageInYears) if ageInYears > 17 =>  
  23.         first + " " + last + " is " + ageInYears + " years old." 
  24.       case _ =>   
  25.         " I have no idea what to do with this person" 
  26.     })  
  27.   }  
  28. }  
  29.  
  30. /*  
  31. C:\Projects\Exploration\Scala>scala App  
  32. Processing Person(Ted,Neward,37) reveals that they're certainly old.  
  33. Processing Person(Amanda,Laucher,27) reveals that Amanda Laucher is 27 years old  
  34. .  
  35.  */ 

清單 7 中發(fā)生了很多操作。下面就讓我們先慢慢了解發(fā)生了什么,然后回到計(jì)算器,看看如何應(yīng)用它們。

首先,整個(gè) match 表達(dá)式被包裹在圓括號(hào)中:這并非模式匹配語(yǔ)法的要求,但之所以會(huì)這樣是因?yàn)槲野涯J狡ヅ浔磉_(dá)式的結(jié)果根據(jù)其前面的前綴串聯(lián)了起來(lái)(切記,函數(shù)性語(yǔ)言里面的任何東西都是一個(gè)表達(dá)式)。

其次,***個(gè) case 表達(dá)式里面有兩個(gè)通配符(帶下劃線的字符就是通配符),這意味著該匹配將會(huì)為符合匹配的 Person 中那兩個(gè)字段獲取任何值,但是它引入了一個(gè)局部變量 a,p.age 中的值會(huì)綁定在這個(gè)局部變量上。這個(gè) case 只有在同時(shí)提供的起保護(hù)作用的表達(dá)式(跟在它后邊的 if 表達(dá)式)成功時(shí)才會(huì)成功,但只有***個(gè) Person 會(huì)這樣,第二個(gè)就不會(huì)了。第二個(gè) case 表達(dá)式在 Person 的 firstName 部分使用了一個(gè)通配符,但在 lastName 部分使用常量字符串 Neward 來(lái)匹配,在 age 部分使用通配符來(lái)匹配。

由于***個(gè) Person 已經(jīng)通過(guò)前面的 case 匹配了,而且第二個(gè) Person 沒(méi)有姓 Neward,所以該匹配不會(huì)為任何一個(gè) Person 而被觸發(fā)(但是,Person("Michael", "Neward", 15) 會(huì)由于***個(gè) case 中的 guard 子句失敗而轉(zhuǎn)到第二個(gè) case)。

第三個(gè)示例展示了模式匹配的一個(gè)常見(jiàn)用途,有時(shí)稱之為提取,在這個(gè)提取過(guò)程中,匹配對(duì)象 p 中的值為了能夠在 case 塊內(nèi)使用而被提取到局部變量中(***個(gè)、***一個(gè)和 ageInYears)。***的 case 表達(dá)式是普通 case 的默認(rèn)值,它只有在其他 case 表達(dá)式均未成功的情況下才會(huì)被觸發(fā)。

簡(jiǎn)要了解了 case類和模式匹配之后,接下來(lái)讓我們回到創(chuàng)建計(jì)算器 AST 的任務(wù)上。

#p#

計(jì)算器 AST

首先,計(jì)算器的 AST 一定要有一個(gè)公用基類型,因?yàn)閿?shù)學(xué)表達(dá)式通常都由子表達(dá)式組成;通過(guò) “5 + (2 * 10)” 就可以很容易地看到這一點(diǎn),在這個(gè)例子中,子表達(dá)式 “(2 * 10)” 將會(huì)是 “+” 運(yùn)算的右側(cè)運(yùn)算數(shù)。

事實(shí)上,這個(gè)表達(dá)式提供了三種 AST 類型:

◆基表達(dá)式

◆承載常量值的 Number 類型

◆承載運(yùn)算和兩個(gè)運(yùn)算數(shù)的 BinaryOperator

想一下,算數(shù)中還允許將一元運(yùn)算符用作求負(fù)運(yùn)算符(減號(hào)),將值從正數(shù)轉(zhuǎn)換為負(fù)數(shù),因此我們可以引入下列基本 AST:

清單 8. 計(jì)算器 AST(src/calc.scala)

  1. package com.tedneward.calcdsl  
  2. {  
  3.   private[calcdsl] abstract class Expr  
  4.   private[calcdsl]  case class Number(value : Double) extends Expr  
  5.   private[calcdsl]  case class UnaryOp(operator : String, arg : Expr) extends Expr  
  6.   private[calcdsl]  case class BinaryOp(operator : String, left : Expr, right : Expr)  
  7.    extends Expr  

注意包聲明將所有這些內(nèi)容放在一個(gè)包(com.tedneward.calcdsl)中,以及每一個(gè)類前面的訪問(wèn)修飾符聲明表明該包可以由該包中的其他成員或子包訪問(wèn)。之所以要注意這個(gè)是因?yàn)樾枰獡碛幸幌盗锌梢詼y(cè)試這個(gè)代碼的 JUnit 測(cè)試;計(jì)算器的實(shí)際客戶機(jī)并不一定非要看到 AST。因此,要將單元測(cè)試編寫成 com.tedneward.calcdsl 的一個(gè)子包:

清單 9. 計(jì)算器測(cè)試(testsrc/calctest.scala)

  1. package com.tedneward.calcdsl.test  
  2. {  
  3.   class CalcTest  
  4.   {  
  5.     import org.junit._, Assert._  
  6.       
  7.     @Test def ASTTest =  
  8.     {  
  9.       val n1 = Number(5)  
  10.  
  11.       assertEquals(5, n1.value)  
  12.     }  
  13.       
  14.     @Test def equalityTest =  
  15.     {  
  16.       val binop = BinaryOp("+", Number(5), Number(10))  
  17.         
  18.       assertEquals(Number(5), binop.left)  
  19.       assertEquals(Number(10), binop.right)  
  20.       assertEquals("+", binop.operator)  
  21.     }  
  22.   }  
  23. }  

到目前為止還不錯(cuò)。我們已經(jīng)有了 AST。

再想一想,我們用了四行 Scala 代碼構(gòu)建了一個(gè)類型分層結(jié)構(gòu),表示一個(gè)具有任意深度的數(shù)學(xué)表達(dá)式集合(當(dāng)然這些數(shù)學(xué)表達(dá)式很簡(jiǎn)單,但仍然很有用)。與 Scala 能夠使對(duì)象編程更簡(jiǎn)單、更具表達(dá)力相比,這不算什么(不用擔(dān)心,真正強(qiáng)大的功能還在后面)。

接下來(lái),我們需要一個(gè)求值函數(shù),它將會(huì)獲取 AST,并求出它的數(shù)字值。有了模式匹配的強(qiáng)大功能,編寫這樣的函數(shù)簡(jiǎn)直輕而易舉:

清單 10. 計(jì)算器(src/calc.scala)

  1. package com.tedneward.calcdsl  
  2. {  
  3.   // ...  
  4.  
  5.   object Calc  
  6.   {  
  7.     def evaluate(e : Expr) : Double =  
  8.     {  
  9.       e match {  
  10.         case Number(x) => x  
  11.         case UnaryOp("-", x) => -(evaluate(x))  
  12.         case BinaryOp("+", x1, x2) => (evaluate(x1) + evaluate(x2))  
  13.         case BinaryOp("-", x1, x2) => (evaluate(x1) - evaluate(x2))  
  14.         case BinaryOp("*", x1, x2) => (evaluate(x1) * evaluate(x2))  
  15.         case BinaryOp("/", x1, x2) => (evaluate(x1) / evaluate(x2))  
  16.       }  
  17.     }  
  18.   }  
  19. }  

注意 evaluate() 返回了一個(gè) Double,它意味著模式匹配中的每一個(gè) case 都必須被求值成一個(gè) Double 值。這個(gè)并不難:數(shù)字僅僅返回它們的包含的值。但對(duì)于剩余的 case(有兩種運(yùn)算符),我們還必須在執(zhí)行必要運(yùn)算(求負(fù)、加法、減法等)前計(jì)算運(yùn)算數(shù)。正如常在函數(shù)性語(yǔ)言中所看到的,會(huì)使用到遞歸,所以我們只需要在執(zhí)行整體運(yùn)算前對(duì)每一個(gè)運(yùn)算數(shù)調(diào)用 evaluate() 就可以了。

大多數(shù)忠實(shí)于面向?qū)ο蟮木幊倘藛T會(huì)認(rèn)為在各種運(yùn)算符本身以外 執(zhí)行運(yùn)算的想法根本就是錯(cuò)誤的 — 這個(gè)想法顯然大大違背了封裝和多態(tài)性的原則。坦白說(shuō),這個(gè)甚至不值得討論;這很顯然違背 了封裝原則,至少在傳統(tǒng)意義上是這樣的。

在這里我們需要考慮的一個(gè)更大的問(wèn)題是:我們到底從哪里封裝代碼?要記住 AST 類在包外是不可見(jiàn)的,還有就是客戶機(jī)(最終)只會(huì)傳入它們想求值的表達(dá)式的一個(gè)字符串表示。只有單元測(cè)試在直接與 AST case 類合作。

#p#

但這并不是說(shuō)所有的封裝都沒(méi)有用了或過(guò)時(shí)了。事實(shí)上恰好相反:它試圖說(shuō)服我們?cè)趯?duì)象領(lǐng)域所熟悉的方法之外,還有很多其他的設(shè)計(jì)方法也很奏效。不要忘了 Scala 兼具對(duì)象和函數(shù)性;有時(shí)候 Expr 需要在自身及其子類上附加其他行為(例如,實(shí)現(xiàn)良好輸出的 toString 方法),在這種情況下可以很輕松地將這些方法添加到 Expr。函數(shù)性和面向?qū)ο蟮慕Y(jié)合提供了另一種選擇,無(wú)論是函數(shù)性編程人員還是對(duì)象編程人員,都不會(huì)忽略到另一半的設(shè)計(jì)方法,并且會(huì)考慮如何結(jié)合兩者來(lái)達(dá)到一些有趣的效果。

從設(shè)計(jì)的角度看,有些其他的選擇是有問(wèn)題的;例如,使用字符串來(lái)承載運(yùn)算符就有可能出現(xiàn)小的輸入錯(cuò)誤,最終會(huì)導(dǎo)致結(jié)果不正確。在生產(chǎn)代碼中,可能會(huì)使用(也許必須使用)枚舉而非字符串,使用字符串的話就意味著我們可能潛在地 “開(kāi)放” 了運(yùn)算符,允許調(diào)用出更復(fù)雜的函數(shù)(諸如 abs、sin、cos、tan 等)乃至用戶定義的函數(shù);這些函數(shù)是基于枚舉的方法很難支持的。

對(duì)所有設(shè)計(jì)和實(shí)現(xiàn)的來(lái)說(shuō),都不存在一個(gè)適當(dāng)?shù)臎Q策方法,只能承擔(dān)后果。后果自負(fù)。

但是這里可以使用一個(gè)有趣的小技巧。某些數(shù)學(xué)表達(dá)式可以簡(jiǎn)化,因而(潛在地)優(yōu)化了表達(dá)式的求值(因此展示了 AST 的有用性):

◆任何加上 “0” 的運(yùn)算數(shù)都可以被簡(jiǎn)化成非零運(yùn)算數(shù)。

◆任何乘以 “1” 的運(yùn)算數(shù)都可以被簡(jiǎn)化成非零運(yùn)算數(shù)。

◆任何乘以 “0” 的運(yùn)算數(shù)都可以被簡(jiǎn)化成零。

不止這些。因此我們引入了一個(gè)在求值前執(zhí)行的步驟,叫做 simplify(),使用它執(zhí)行這些具體的簡(jiǎn)化工作:

清單 11. 計(jì)算器(src/calc.scala)

  1. def simplify(e : Expr) : Expr =  
  2. {  
  3.   e match {  
  4.     // Double negation returns the original value  
  5.     case UnaryOp("-", UnaryOp("-", x)) => x  
  6.     // Positive returns the original value  
  7.     case UnaryOp("+", x) => x  
  8.     // Multiplying x by 1 returns the original value  
  9.     case BinaryOp("*", x, Number(1)) => x  
  10.     // Multiplying 1 by x returns the original value  
  11.     case BinaryOp("*", Number(1), x) => x  
  12.     // Multiplying x by 0 returns zero  
  13.     case BinaryOp("*", x, Number(0)) => Number(0)  
  14.     // Multiplying 0 by x returns zero  
  15.     case BinaryOp("*", Number(0), x) => Number(0)  
  16.     // Dividing x by 1 returns the original value  
  17.     case BinaryOp("/", x, Number(1)) => x  
  18.     // Adding x to 0 returns the original value  
  19.     case BinaryOp("+", x, Number(0)) => x  
  20.     // Adding 0 to x returns the original value  
  21.     case BinaryOp("+", Number(0), x) => x  
  22.     // Anything else cannot (yet) be simplified  
  23.     case _ => e  
  24.   }  

還是要注意如何使用模式匹配的常量匹配和變量綁定特性,從而使得編寫這些表達(dá)式可以易如反掌。對(duì) evaluate() 惟一一個(gè)更改的地方就是包含了在求值前先簡(jiǎn)化的調(diào)用:

清單 12. 計(jì)算器(src/calc.scala)

  1. def evaluate(e : Expr) : Double =  
  2. {  
  3.   simplify(e) match {  
  4.     case Number(x) => x  
  5.     case UnaryOp("-", x) => -(evaluate(x))  
  6.     case BinaryOp("+", x1, x2) => (evaluate(x1) + evaluate(x2))  
  7.     case BinaryOp("-", x1, x2) => (evaluate(x1) - evaluate(x2))  
  8.     case BinaryOp("*", x1, x2) => (evaluate(x1) * evaluate(x2))  
  9.     case BinaryOp("/", x1, x2) => (evaluate(x1) / evaluate(x2))  
  10.   }  

還可以再進(jìn)一步簡(jiǎn)化;注意一下:它是如何實(shí)現(xiàn)只簡(jiǎn)化樹(shù)的***層的?如果我們有一個(gè)包含 BinaryOp("*", Number(0), Number(5)) 和 Number(5) 的 BinaryOp 的話,那么內(nèi)部的 BinaryOp 就可以被簡(jiǎn)化成 Number(0),但外部的 BinaryOp 也會(huì)如此,這是因?yàn)榇藭r(shí)外部 BinaryOp 的其中一個(gè)運(yùn)算數(shù)是零。

我突然犯了作家的職業(yè)病了,所以我想將它留予讀者來(lái)定義。其實(shí)是想增加點(diǎn)趣味性罷了。如果讀者愿意將他們的實(shí)現(xiàn)發(fā)給我的話,我將會(huì)把它放在下一篇文章的代碼分析中。將會(huì)有兩個(gè)測(cè)試單元來(lái)測(cè)試這種情況,并會(huì)立刻失敗。您的任務(wù)(如果您選擇接受它的話)是使這些測(cè)試 — 以及其他任何測(cè)試,只要該測(cè)試采取了任意程度的 BinaryOp 和 UnaryOp 嵌套 — 通過(guò)。

結(jié)束語(yǔ)

顯然我還沒(méi)有說(shuō)完;還有分析的工作要做,但是計(jì)算器 AST 已經(jīng)成形。我們無(wú)需作出大的變動(dòng)就可以添加其他的運(yùn)算,運(yùn)行 AST 也無(wú)需大量的代碼(按照 Gang of Four 的 Visitor 模式),而且我們已經(jīng)有了一些執(zhí)行計(jì)算本身的工作代碼(如果客戶機(jī)愿意為我們構(gòu)建用于求值的代碼的話)。

更重要的是,您已經(jīng)看到了 case 類是如何與模式匹配合作,使得創(chuàng)建 AST 并對(duì)其求值變得輕而易舉。這是 Scala 代碼(以及大多數(shù)函數(shù)性語(yǔ)言)很常用的設(shè)計(jì),而且如果您準(zhǔn)備認(rèn)真地研究這個(gè)環(huán)境的話,這是您應(yīng)當(dāng)掌握的內(nèi)容之一。

【相關(guān)閱讀】

  1. Scala編程語(yǔ)言專題
  2. 面向Java開(kāi)發(fā)人員的Scala指南:包和訪問(wèn)修飾符
  3. 面向Java開(kāi)發(fā)人員的Scala指南:使用元組、數(shù)組和列表
  4. 面向Java開(kāi)發(fā)人員的Scala指南:當(dāng)繼承中的對(duì)象遇到函數(shù)
  5. 面向Java開(kāi)發(fā)人員的Scala指南:使用Scala版本的Java接口
責(zé)任編輯:yangsai 來(lái)源: IBMDW
相關(guān)推薦

2009-06-19 13:16:36

Scala計(jì)算器解析器組合子

2009-06-19 11:42:09

Scala計(jì)算器解析

2009-06-16 17:54:38

Scala類語(yǔ)法語(yǔ)義

2009-09-09 11:37:08

Scala的模式匹配

2009-09-28 11:01:39

從Java走進(jìn)Scal

2009-08-21 16:17:25

ScalaTwitter API

2009-06-17 13:57:25

Scala元組數(shù)組

2009-06-17 11:44:22

Scala控制結(jié)構(gòu)

2009-06-19 10:51:39

Scalapackage訪問(wèn)修飾符

2009-07-15 10:14:25

Scala并發(fā)性

2009-12-09 09:15:47

從Java走進(jìn)ScalTwitter API

2009-02-04 17:32:03

ibmdwJavaScala

2009-10-14 11:14:38

ScitterScalaTwitter

2009-07-08 15:35:18

Case類Scala

2009-06-16 17:09:17

Scala面向?qū)ο?/a>函數(shù)編程

2009-08-14 11:35:01

Scala Actor

2009-06-17 13:26:06

scala繼承模型

2011-09-16 14:13:15

Windows7計(jì)算器

2022-09-08 11:35:45

Python表達(dá)式函數(shù)

2022-09-09 00:25:48

Python工具安全
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)