从我们这个小游戏的词汇扫描器中,我们应该可以得到类似下面的列表(你的看起来可能格式会不太一样):
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 物件,而这个对象又包含主谓宾各个成员。
为了达到这个效果,你需要四样工具:
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)对象了。我们的处理流程如下:
peek
识别下一个单词。parse_subject
好了。raise
一个错误,接下来你会学到这方面的内容。演示这个过程最简单的方法是把程式码展示给你让你阅读,不过这节习题有个不一样的要求,前面是我给你测试程式码,你照着写出程式码来,而这次是我给你的程序,而你要为它写出测试程式码来。
以下就是我写的用来解析简单句子的程式码,它使用了 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
你已经简单学过关于异常的一些东西,但还没学过怎样抛出(raise)它们。这节的程式码示范了如何 raise。首先在最前面,你要定义好 ParserException
这个类,而它又是 Exception
的一种。另外要注意我们是怎样使用 raise
这个关键字来抛出异常的。
你的测试程式码应该也要测试到这些异常,这个我也会示范给你如何实现。
为《习题49》写一个完整的测试方案,确认程式码中所有的东西都能正常工作,其中异常的测试——输入一个错误的句子它会抛出一个异常来。
使用 assert_raises
这个函式来检查异常,在 Test::Unit 的文件里查看相关的内容,学着使用它写针对“执行失败”的测试,这也是测试很重要的一个方面。从文件中学会使用 assert_raises
,以及一些别的函式。
写完测试以后,你应该就明白了这段程式码的运作原理,而且也学会了如何为别人的程式码写测试程式码。相信我,这是一个非常有用的技能。
parse_
method,将它们放到一个类里边,而不仅仅是独立的方法函式。这两种设计你喜欢哪一种呢?