從我們這個小遊戲的詞彙掃描器中,我們應該可以得到類似下面的列表(你的看起來可能格式會不太一樣):
~~~
ruby-1.9.2-p180 :003 > print Lexicon.scan("go north")
[#<struct Lexicon::Pair token=:verb, word="go">,
#<struct Lexicon::Pair token=:direction, word="north">] => nil
ruby-1.9.2-p180 :004 > print Lexicon.scan("kill the princess")
[#<struct Lexicon::Pair token=:verb, word="kill">,
#<struct Lexicon::Pair token=:stop, word="the">,
#<struct Lexicon::Pair token=:noun, word="princess">] => nil
ruby-1.9.2-p180 :005 > print Lexicon.scan("eat the bear")
[#<struct Lexicon::Pair token=:verb, word="eat">,
#<struct Lexicon::Pair token=:stop, word="the">,
#<struct Lexicon::Pair token=:noun, word="bear">] => nil
ruby-1.9.2-p180 :006 > print Lexicon.scan("open the door and smack the bear in the nose")
[#<struct Lexicon::Pair token=:error, word="open">,
#<struct Lexicon::Pair token=:stop, word="the">,
#<struct Lexicon::Pair token=:noun, word="door">,
#<struct Lexicon::Pair token=:error, word="and">,
#<struct Lexicon::Pair token=:error, word="smack">,
#<struct Lexicon::Pair token=:stop, word="the">,
#<struct Lexicon::Pair token=:noun, word="bear">,
#<struct Lexicon::Pair token=:stop, word="in">,
#<struct Lexicon::Pair token=:stop, word="the">,
#<struct Lexicon::Pair token=:error, word="nose">] => nil
ruby-1.9.2-p180 :007 >
~~~
現在讓我們把它轉化成遊戲可以使用的東西,也就是一個 Sentence 類。
如果你還記得學校學過的東西的話,一個句子是由這樣的結構組成的:
> 主語(Subject) + 謂語(動詞Verb) + 賓語(Object)
很顯然實際的句子可能會比這複雜,而你可能已經在英語的語法課上面被折騰得夠嗆了。我們的目的,是將上面的 struct 列表轉換為一個 Sentence 物件,而這個對象又包含主謂賓各個成員。
## 匹配(Match) And 窺視(Peek)
為了達到這個效果,你需要四樣工具:
1. 一個循環存取 struct 列表的方法,這挺簡單的。
2. 「匹配」我們的主謂賓設置中不同種類 struct 的方法。
3. 一個「窺視」潛在struct的方法,以便做決定時用到。
4. 「跳過(skip)」我們不在乎的內容的方法,例如形容詞、冠詞等沒有用處的詞彙。
5. 我們使用 peek 函式查看 struct 列表中的下一個成員,做匹配以後再對它做下一步動作。讓我們先看看這個 peek 函式:
~~~
def peek(word_list)
begin
word_list.first.token
rescue
nil
end
end
~~~
很簡單。再看看 match 函式:
~~~
def match(word_list, expecting)
begin
word = word_list.shift
if word.token == expecting
word
else
nil
end
rescue
nil
end
end
~~~
還是很簡單,最後我們看看 skip 函式:
~~~
def skip(word_list, word_type)
while peek(word_list) == word_type
match(word_list, word_type)
end
end
~~~
以你現在的水準,你應該可以看出它們的功能來。確認自己真的弄懂了它們。
## 句子的語法
有了工具,我們現在可以從 struct 列表來構建句子(Sentence)對象了。我們的處理流程如下:
1. 使用?`peek`?識別下一個單詞。
2. 如果這個單詞和我們的語法匹配,我們就調用一個函式來處理這部分語法。假設函式的名字叫?`parse_subject`?好了。
3. 如果語法不匹配,我們就?`raise`?一個錯誤,接下來你會學到這方面的內容。
4. 全部分析完以後,我們應該能得到一個 Sentence 物件,然後可以將其應用在我們的遊戲中。
演示這個過程最簡單的方法是把程式碼展示給你讓你閱讀,不過這節習題有個不一樣的要求,前面是我給你測試程式碼,你照著寫出程式碼來,而這次是我給你的程序,而你要為它寫出測試程式碼來。
以下就是我寫的用來解析簡單句子的程式碼,它使用了?`ex48`?這個 Lexicon class。
~~~
class ParserError < Exception
end
class Sentence
def initialize(subject, verb, object)
# remember we take Pair.new(:noun, "princess") structs and convert them
@subject = subject.word
@verb = verb.word
@object = object.word
end
end
def peek(word_list)
begin
word_list.first.token
rescue
nil
end
end
def match(word_list, expecting)
begin
word = word_list.shift
if word.token == expecting
word
else
nil
end
rescue
nil
end
end
def skip(word_list, token)
while peek(word_list) == token
match(word_list, token)
end
end
def parse_verb(word_list)
skip(word_list, :stop)
if peek(word_list) == :verb
return match(word_list, :verb)
else
raise ParserError.new("Expected a verb next.")
end
end
def parse_object(word_list)
skip(word_list, :stop)
next_word = peek(word_list)
if next_word == :noun
return match(word_list, :noun)
end
if next_word == :direction
return match(word_list, :direction)
else
raise ParserError.new("Expected a noun or direction next.")
end
end
def parse_subject(word_list, subj)
verb = parse_verb(word_list)
obj = parse_object(word_list)
return Sentence.new(subj, verb, obj)
end
def parse_sentence(word_list)
skip(word_list, :stop)
start = peek(word_list)
if start == :noun
subj = match(word_list, :noun)
return parse_subject(word_list, subj)
elsif start == :verb
# assume the subject is the player then
return parse_subject(word_list, Pair.new(:noun, "player"))
else
raise ParserError.new("Must start with subject, object, or verb not: #{start}")
end
end
~~~
## 關於異常(Exception)
你已經簡單學過關於異常的一些東西,但還沒學過怎樣拋出(raise)它們。這節的程式碼示範了如何 raise。首先在最前面,你要定義好?`ParserException`這個類,而它又是?`Exception`?的一種。另外要注意我們是怎樣使用?`raise`這個關鍵字來拋出異常的。
你的測試程式碼應該也要測試到這些異常,這個我也會示範給你如何實現。
## 你應該測試的東西
為《習題49》寫一個完整的測試方案,確認程式碼中所有的東西都能正常工作,其中異常的測試——輸入一個錯誤的句子它會拋出一個異常來。
使用?`assert_raises`?這個函式來檢查異常,在 Test::Unit 的文件裡查看相關的內容,學著使用它寫針對「執行失敗」的測試,這也是測試很重要的一個方面。從文件中學會使用?`assert_raises`,以及一些別的函式。
寫完測試以後,你應該就明白了這段程式碼的運作原理,而且也學會了如何為別人的程式碼寫測試程式碼。相信我,這是一個非常有用的技能。
## 加分習題
1. 修改?`parse_`?method,將它們放到一個類裡邊,而不僅僅是獨立的方法函式。這兩種設計你喜歡哪一種呢?
2. 提高parser 對於錯誤輸入的抵禦能力,這樣即使使用者輸入了你預定義語彙之外的詞語,你的程式碼也能正常運行下去。
3. 改進語法,讓它可以處理更多的東西,例如數字。
4. 想想在遊戲裡你的 Sentence 類可以對使用者輸入做哪些有趣的事情。
- 笨方法更簡單
- 習題 0: 準備工作
- 習題 1: 第一個程式
- 習題 2: 注釋和井號
- 習題 3: 數字和數學計算
- 習題 4: 變數(variable)和命名
- 習題 5: 更多的變數和印出
- 習題 6: 字串(string)和文字
- 習題 7: 更多印出
- 習題 8: 印出,印出
- 習題 9: 印出,印出,印出
- 習題 10: 那是什么?
- 習題 11: 提問
- 習題 12: 模組 (Module)
- 習題 13: 參數、解包、參數
- 習題 14: 提示和傳遞
- 習題 15: 讀取檔案
- 習題 16: 讀寫檔案
- 習題 17: 更多的檔案操作
- 習題 18: 命名、變數、程式碼、函式
- 習題 19: 函式和變數
- 習題 20: 函式和檔案
- 習題 21: 函式可以傳回東西
- 習題 22: 到現在你學到了哪些東西?
- 習題 23: 閱讀一些程式碼
- 習題 24: 更多練習
- 習題 25: 更多更多的練習
- 習題 26: 恭喜你,現在來考試了!
- 習題 27: 記住邏輯關系
- 習題 28: 布林(Boolean)表示式練習
- 習題 29: 如果(if)
- 習題 30: Else 和 If
- 習題 31: 做出決定
- 習題 32: 回圈和陣列
- 習題 33: While 回圈
- 習題 34: 存取陣列里的元素
- 習題 35: 分支 (Branches) 和函式 (Functions)
- 習題 36: 設計和測試
- 習題 37: 復習各種符號
- 習題 38: 閱讀程式碼
- 習題 39: 陣列的操作
- 習題 40: Hash, 可愛的 Hash
- 習題 41: 來自 Percal 25 號行星的哥頓人(Gothons)
- 習題 42: 物以類聚
- 習題 43: 你來制作一個游戲
- 習題 44: 評估你的游戲
- 習題 45: 物件、類和從屬關系
- 習題 46: 一個專案骨架
- 習題 47: 自動化測試
- 習題 48: 更進階的使用者輸入
- 習題 49: 創造句子
- 習題 50: 你的第一個網站
- 習題 51: 從瀏覽器中取得輸入
- 習題 52: 創造你的網頁游戲
- 下一步
- 一個老程式設計師的建議