Monday, January 30, 2012

Exercise 49: Making Sentences: Learn Ruby the Hard Way: Practicum

For the record, here's the point where people get moderately bent at programming books when they can't seem to figure something out. Again, this is my problem, not Zed or Rob's, but it does point out what often comes to be a challenge for many.


When you get stuck on a concept, oftentimes, there really isn't a way to move forward until you figure it out. While I don't want to get all the way through the book and decide I'm stranded on one point, two exercises call for me to have my act together on this functionality (which makes perfect sense, because in life, you have to make the system work before you can add new features.


Thus, I'm reviewing this chapter with a significant handicap; I'm still stuck from Exercise 48.

So if we have managed to get the lexicon scanner to work, we should see output that looks something like this:



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 >


With this output, we should be able to now take the token pairs and make an actual sentence, using a Sentence class.

Sentences can be structured simply by combining the following tokens (for English, anyway; different languages will have different rules):

Subject Verb Object

So the primary goal of the sentence class is to turn the lists of structs above into a Sentence object with a subject, verb, and object.

Match And Peek

To do this we need four tools:

- A way to loop through the list of structs.

- A way to "match" different types of structs that we expect in our Subject Verb Object setup.
- A way to "peek" at a potential struct so we can make some decisions.
- A way to "skip" things we do not care about, like stop words.
- We use the peek function to say look at the next element in our struct array, and then match to take one off and work with it.

So here is the first peek function:

def peek(word_list)
  begin
    word_list.first.token
  rescue
    nil
  end
end

This is the match function:

def match(word_list, expecting)
  begin
    word = word_list.shift


    if word.token == expecting
      word
    else
      nil
    end
  rescue
     nil
  end
end

This is the skip function:

def skip(word_list, word_type)
  while peek(word_list) == word_type
    match(word_list, word_type)
  end
end


The Sentence Grammar

To build our Sentence objects from the struct array, we can do the following:

- Identify the next word with peek.

- If that word fits the grammar, call a function to handle that part of the grammar

- If it doesn't, raise an error (see below).

When we're all done, we should have a Sentence object to work with in our game.

So this time, instead of being given the test and trying to figure out the code, this time, we get the code and we then figure out how to write the test to meet the requirements.

Here's the code for parsing simple sentences using the ex48 Lexicon class:



The sections below are advice given by Zed and Rob. It's printed in italic to show it's their words verbatim.

A Word On Modules

This code uses something in Ruby called a "module" named Parser. A module (created with module Parser) is a way to package up the functions so that they don't conflict with other parts of Ruby. In Ruby 1.9 there was a change to the testing system that created a skip method which conflicted with the Parser.skip method. The solution was to do what you see here and wrap all the functions in this module.


You use a module by simply calling functions on it with the . operator, similar to an object you've made. In this case if you wanted to call the parse_verb() function you'd write Parser.parse_verb(). You'll see a demonstration of this when I give you a sample unit test.


What You Should Test

For Exercise 49 is write a complete test that confirms everything in this code is working. That includes making exceptions happen by giving it bad sentences. Here is a starter sample so you can see how you would call a function in a module:

require 'test/unit'
require_relative '../lib/ex49'


class ParserTests &lt; Test::Unit::TestCase


    def test_parse_verb()
        # WARNING: THIS FAILS ON PURPOSE SEE THE BOOK
        Parser.parse_verb([false])
    end


end

You can see I make the basic test class, then create a test_parse_verb to test out the Parser.parse_verb function. I don't want to do the work for you, so I've made this fail on purpose. This shows you how to use the Parser module and call functions on it, and you should work on making this test actually test all the code.


Check for an exception by using the function assert_raise from the Test::Unit documentation. Learn how to use this so you can write a test that is expected to fail, which is very important in testing. Learn about this function (and others) by reading the Test::Unit documentation.


When you are done, you should know how this bit of code works, and how to write a test for other people's code even if they do not want you to. Trust me, it's a very handy skill to have.

Extra Credit

-  Change the parse_ methods and try to put them into a class rather than be just methods. Which design do you like better?
- Make the parser more error resistant so that you can avoid annoying your users if they type words your lexicon doesn't understand.
- Improve the grammar by handling more things like numbers.
- Think about how you might use this Sentence class in your game to do more fun things with a user's input.

No comments: