[Solution]: Lisp partial solution - meta-programming help

Hi all;

I started to give the lisp quiz a go. From the begining, I resolved to try
for a solution such that the map can be set up in a declarative manner.

I wanted it to look something like the attr_ class methods and ActiveRecords
`associations`; however, setting up something like this seems to be more
work than I thought it would be. Following is some working code, but it's
pretty ugly. Setting the class variable in this manner just feels very
hackish, yet it's the only way I could seem to get this to work.

The problem was this: how to write the methods so that they don't have to be
explicitly overwritten in the subclasses, and yet they can reference these
concrete classes state?

Anyone with a bit more ruby experiance have any refactoring ideas?

···

--
Lou

module LocationClassMethods
def exit_to(place,direction,opts=nil)
portal = (opts[:through] || opts[:via] if opts.respond_to?:[]) || 'door'
class_variable_set(:@@exits, []) unless class_variables.include?(:@@
exits.to_s)
exits = class_variable_get(:@@exits)
exits << Exit.new(direction,portal)
end

def description(desc)
class_variable_set(:@@description, desc)
end

def exits
class_variable_get(:@@exits)
end

def get_description
class_variable_get(:@@description)
end

end

class Location
def self.inherited(sub)
sub.extend(LocationClassMethods)
end

def initialize
@exits = self.class.exits
@description = self.class.get_description
end

def describe
puts @description
describe_exits
end

def describe_exits
@exits.each { |e| puts e.describe }
end
end

class Attic < Location
exit_to :living_room, :down, :via => 'staircase'

description 'You are in the attic of the wizard\'s house. There is a giant
welding torch in the corner.'
end

class Garden < Location
exit_to :living_room, :west

description 'You are standing in a beautiful garden. There is a well in
front of you.'
end

class LivingRoom < Location
exit_to :attic, :up, :via => 'staircase'
exit_to :garden, :east

description 'You are in the living-room of a wizard\'s house. There is a
wizard snoring loudly on the couch.'
end

a = Attic.new
a.describe

Here's a sketch. It doesn't have the full feature set (portals, etc),
but that should be easy to add. Instead of messing around with class
variables, we simply define methods that return the values we want.
The data is stored implicitly within blocks.

class Location
  def self.description(d)
    # Even though we're using a constant method name, we use
    # "define_method" instead of "def" in order to form a closure over
    # the local variable "d".
    define_method(:describe) do
      d
    end
  end

  def self.exit_to(direction,location)
    # @exits is a member of the class object itself, not the instances
    @exits ||= {}
    @exits[direction] = location
    e = @exits
    define_method(:exits) do
      e
    end
  end
end

class A < Location
  exit_to :north, :b
  exit_to :east, :garden
  description "You're at point A. It's very boring."
end

class B < Location
  exit_to :south, :a
  description "Point B is even more boring than Point A."
end

regards,
Ed

···

On Tue, Oct 04, 2005 at 05:40:51AM +0900, Louis J Scoras wrote:

Anyone with a bit more ruby experiance have any refactoring ideas?

Maybe I read the original lesson wrong, but I understood the point of
that article to be how to use Lisp macros (which he renames 'spel's so
as not to confuse people). I'm new to Ruby, but it seems that Ruby
doesn't offer such a mechanism. Most solutions here seem to use
method_missing to handle the new features, but that really isn't the
same as a macro.

Does anyone know if macros will be introduced into Ruby in 1.9 or 2.0?
Even something like Dylan's macros would be great.
http://www.gwydiondylan.org/books/dpg/db_329.html
The latest description of Ruby 2.0 I know of doesn't mention macros at
all. http://www.rubygarden.org/ruby?Rite Do people here think macros
are not useful?

-mike

I have a solution that works nicely in irb, but I'd like some help making
it more friendly. It works well in that using method_missing I have set it
up so that unquoted uncoloned barewords in commaless lists just work:

$ irb -r LispGame
You are in the living room of a wizard's house. There is a wizard snoring loudly on the couch.
There is a stairway going upstairs from here.
There is a door going west from here.
You see a whiskeyBottle on the floor.
You see a bucket on the floor.
irb(main):001:0> get bucket
You are now carrying the bucket.
=> nil
irb(main):002:0> go west
You are in a beautiful garden. There is a well in front of you.
There is a door going east from here.
You see a chain on the floor.
You see a frog on the floor.
=> nil
irb(main):003:0> get chain
You are now carrying the chain.
=> nil
irb(main):004:0> go east
You are in the living room of a wizard's house. There is a wizard snoring loudly on the couch.
There is a stairway going upstairs from here.
There is a door going west from here.
You see a whiskeyBottle on the floor.
=> nil
irb(main):005:0> go upstairs
You are in the attic of the wizard's house. There is a giant welding torch in the corner.
There is a stairway going downstairs from here.
=> nil
irb(main):006:0> weld chain to bucket
(irb):6: warning: parenthesize argument(s) for future version
(irb):6: warning: parenthesize argument(s) for future version
The chain is now securely welded to the bucket.
=> nil
irb(main):007:0>

But the problem with this approach is that I can't display a helpful message
when the player enters an invalid command:

irb(main):007:0> help
=> :help
irb(main):008:0>

I'm trying to come up with a way to have my cake and eat it, too. Is there
any way to hook into irb and have some code executed every time the prompt
is displayed, like $PROMPT_COMMAND in bash? That would let me work around
this . . .

Along the same lines, as I am unlikely to pursue this any further,
here is my half-baked doodle towards a game DSL :slight_smile:

You can move about (using 'east', 'west', 'upstairs', etc.) but nothing else.

Regards,
Sean

-- CODE --
module Attributes
  def has(*names)
    self.class_eval {
      names.each do |name|
        define_method(name) {|*args|
          if args.size > 0
            instance_variable_set("@#{name}", *args)
          else
            instance_variable_get("@#{name}")
          end
          }
      end
      }
  end

end

module Directions
  def directions(*directions)
    directions.each do |name|
      self.class.class_eval {
        define_method(name) {
          dest = @location.exits[name]
          if dest
            @location = @rooms[dest[1]]
            look
          else
            puts "You can't move in that direction"
          end
        }
      }
    end
  end
end

class GameObject
  extend Attributes
  has :identifier, :name, :description
  def initialize(identifier, &block)
    @identifier = identifier
    instance_eval &block
  end
end

class Thing < GameObject
  has :location
end

class Room < GameObject
  has :exits

  def initialize(identifier, &block)
    # put defaults before super - they will be overridden in block (if at all)
    super
  end

end

class Game
  include Directions

  attr_accessor :name, :rooms, :location, :things

  def initialize(name, &block)
    @name = name
    @rooms = {}
    @things = {}

    # read game definition
    instance_eval &block

  end

  def room(identifier, &block)
    @rooms[symbol(identifier)] = Room.new(symbol(identifier), &block)
  end

  def thing(identifier, &block)
    @things[symbol(identifier)] = Thing.new(symbol(identifier), &block)
  end

  def symbol(s)
    s.to_s.to_sym
  end

  def start(room_identifier)
    @location = @rooms[room_identifier]
  end

  def describe_path(direction, path)
    "There is a #{path} going #{direction}."
  end

  def describe_exits(location)
    location.exits.map {|direction, (path, destination)|
      describe_path(direction, path)
    }
  end

  def describe_floor(location)
    @things.select{|key, thing| thing.location ==
location.identifier}.map{|key, thing| "There is a #{thing.description}
here."}
  end

  def main_loop
    while input = gets
      input.chomp!
      case input
      when 'exit', 'quit'
        break
      when 'help'
        puts "Sorry pal! You're on your own here :)"
      else
        begin
          instance_eval input
        rescue Exception => e
          puts e.to_s
          puts "Eh?"
        end
      end
    end
  end

  # commands

  def look
    puts location.description
    puts describe_exits(location)
    puts describe_floor(location)
  end

  def quit
    break
  end

end

def game(name, &block)
  g = Game.new(name, &block)
  g.look
  g.main_loop
end

# Game definition

game "Ruby Adventure" do

  directions :east, :west, :north, :south, :up, :down, :upstairs, :downstairs

  room :living_room do
    name 'Living Room'
    description "You are in the living-room of a wizard's house. There
is a wizard snoring loudly on the couch."
    exits :west => [:door, :garden],
          :upstairs => [:stairway, :attic]
  end

  room :garden do
    name 'Garden'
    description "You are in a beautiful garden. There is a well in
front of you."
    exits :east => [:door, :living_room]
  end

  room :attic do
    name "Attic"
    description "You are in the attic of the wizard's house. There is
a giant welding torch in the corner."
    exits :downstairs => [:stairway, :living_room]
  end

  thing :whiskey_bottle do
    name 'whiskey bottle'
    description 'half-empty whiskey bottle'
    location :living_room
  end

  thing :bucket do
    name 'bucket'
    description 'rusty bucket'
    location :living_room
  end

  thing :chain do
    name 'chain'
    description 'sturdy iron chain'
    location :garden
  end

  thing :frog do
    name 'frog'
    description 'green frog'
    location :garden
  end

  start :living_room

end
-- END --

Mike Salisbury...

Maybe I read the original lesson wrong, but I understood the point of
that article to be how to use Lisp macros (which he renames 'spel's so
as not to confuse people).

I don't think so; it looks to me like a general intro, with macros included
to get a feeling for what LISP can do.

I'm new to Ruby, but it seems that Ruby
doesn't offer such a mechanism. Most solutions here seem to use
method_missing to handle the new features, but that really isn't the
same as a macro.

That's right. eval is as close as Ruby gets to macros, although ParseTree
may give similar capability down the track (now even?).

Does anyone know if macros will be introduced into Ruby in 1.9 or 2.0?
Even something like Dylan's macros would be great.
http://www.gwydiondylan.org/books/dpg/db_329.html
The latest description of Ruby 2.0 I know of doesn't mention macros at
all. http://www.rubygarden.org/ruby?Rite Do people here think macros
are not useful?

I daresay macros aren't a part of the "Ruby Way," and I'm pretty sure you
won't see them in Ruby 2.0. This doesn't mean they can't be implemented as a
library.

Cheers,
Dave

Okay cool. So closures are the way to go. That's the way I would have done
it in perl, but I didn't know if all the OO magic was prefered way in Ruby.
Plus, I was looking at the Pickaxe reference for the Module Class
documentation =)

That helps a lot, thanks.

···

--
Lou

This was quite problematic when I was building my own version. I tried to get around it with my $stringify global, but that requires you to spell out all allowed words.

I'm very it interested in the answers to your irb questions, though, so we will both await the responses from the gurus...

James Edward Gray II

P.S. Please do share your code at some point... Looks great!

···

On Oct 3, 2005, at 6:01 PM, Mark J.Reed wrote:

I'm trying to come up with a way to have my cake and eat it, too. Is there
any way to hook into irb and have some code executed every time the prompt
is displayed, like $PROMPT_COMMAND in bash? That would let me work around
this . . .

James Edward Gray II <james@grayproductions.net> writes:

···

On Oct 3, 2005, at 6:01 PM, Mark J.Reed wrote:
P.S. Please do share your code at some point... Looks great!

Sure. Here's what I have so far. As I said, it follows the Lisp
pretty closely; I started to do a real Rubyish O-O solution, but all
the constructor calls and map building seemed inelegant by comparison.
So I figured I'd do this way first.

------

$objects = [ :whiskeyBottle, :bucket, :chain, :frog ]

$map =
{
    :livingRoom =>
        [ "You are in the living room of a wizard's house." +
          " There is a wizard snoring loudly on the couch.",
          { :west => [ :door, :garden ] ,
            :upstairs => [ :stairway, :attic ] }
        ],

    :garden =>
        [ "You are in a beautiful garden. There is a well in front of you.",
          { :east => [ :door, :livingRoom ] }
        ],
    :attic =>
        [ "You are in the attic of the wizard's house." +
          " There is a giant welding torch in the corner.",
          { :downstairs => [ :stairway, :livingRoom ] }
        ]
}

$locations =
{
    :whiskeyBottle => :livingRoom,
    :bucket => :livingRoom,
    :chain => :garden,
    :frog => :garden
}

$location = :livingRoom

def description(location)
    $map[location][0]
end

def exits(location)
   $map[location][1]
end

def contents(location)
    $objects.find_all do
        >obj>
        $locations[obj] == location
    end
end

def look(location=$location)
    puts description(location)
    exits(location).each do
        >k, v|
       puts "There is a #{v[0]} going #{k} from here"
    end
    contents(location).each do
        >obj>
        puts "You see a #{obj} on the floor."
    end
    return
end

def go(direction)
    if $map[$location][1].include?(direction) then
        $location = $map[$location][1][direction][1]
        look
    else
        puts "You can't go that way."
    end
end

def get(object)
    if $locations[object] == $location then
        $locations[object] = :body
        puts "You are now carrying the #{object}"
    else
        puts "You cannot get that."
    end
end

def inventory
    contents(:body)
end

def have?(object)
    inventory.include? object
end

$chain_welded = false

def weld(*args)
    args.flatten!
    if args.length == 3 && args[1] == :to then
        args.delete_at(1)
    end
    if $chain_welded || args.length != 2 || $location != :attic ||
        !args.include?(:chain) || !args.include?(:bucket) then
        puts "You can't weld like that."
    else
        $chain_welded = true
        puts "The chain is now securely welded to the bucket."
    end
end

def method_missing(*s)
    return *s.flatten
end
look

Here's one way to hook into irb's eval loop:

module IRB
  class Context
    def evaluate(line, line_no)
      value = @workspace.evaluate(self, line, irb_path, line_no)
      puts "value = #{value}" # do something with returned value here
      value
    end
  end
end

Regards,

Sean

I solved it by letting method_missing fill in the allowed words itself.
The trick is that all the allowed words are used in Game#initialize,
so after the global Game object is constructed, any additional unknown
words must be invalid commands.

class Game
   def initialize
      @objects = [whiskey_bottle, frog, bucket, chain]
  #...
  end
  #...
end

$g = Game.new
$valid_words = []

def method_missing symbol, *args
       #if the game has not started, add the symbol to the list of valid tokens
       if !$g
               $valid_words << symbol
               symbol.to_s
       #if the game has started, see if it is a token
       elsif $words.include? symbol
               symbol.to_s
       #otherwise, it is an unknown command.
       else
               puts "> #{symbol} #{args.join ' '}" if !$Interactive
               puts "Sorry, I don't understand #{symbol}"
       end
end

-Adam

···

On 10/3/05, James Edward Gray II <james@grayproductions.net> wrote:

This was quite problematic when I was building my own version. I
tried to get around it with my $stringify global, but that requires
you to spell out all allowed words.

So what would be the best way to do this if the data needs to be read-write?

In the context of the game---and the code above---I guess we could try to
create two methods (getter/setter) that close over the same shared variable.
But what would you do if the classes in question weren't meant to be
singletons?

Trying to use module_eval to dynamically define a class method doesn't seem
to work, because it's not available in the constructor.

Oops - that should have been:

module IRB
  class Context
    def evaluate(line, line_no)
      value = @workspace.evaluate(self, line, irb_path, line_no)
      puts "value = #{value}"
      set_last_value(value)
    end
  end
end

(Note set_last_value)

Sean

I had to double-check that to convince myself it works. I thought I remembered reading more than once that the block arguments had to be on the same line as the start of the block.

Has that been changed, or is my memory fuzzy?

James Edward Gray II

···

On Oct 3, 2005, at 8:11 PM, Mark J.Reed wrote:

    $objects.find_all do
        >obj>

oops:

       elsif $words.include? symbol

should be:
         elsif $valid_words.include? symbol

So what would be the best way to do this if the data needs to be read-write?

...

But what would you do if the classes in question weren't meant to be
singletons?

In that case the data must ultimately be stored in the individual
instances. You can copy it there at #initialize time. Check out
_why's Creature class at the bottom of this page:

Trying to use module_eval to dynamically define a class method doesn't seem
to work, because it's not available in the constructor.

You can dynamically define class methods like this:

class Metaprogrammable
  def self.add_feature(f)
    meta = class << self; self; end
    meta.instance_eval {
      define_method(f) do
        "This is class method #{f}"
      end
    }
  end
end

class Foo < Metaprogrammable
  add_feature :bogus
end

Remember that a Class is also an Object. When you create class
methods, you're extending one particular instance of the class
"Class". So this:

class << MyClass
  def a_class_method
    ...
  end
end

is equivalent to this:

def MyClass.a_class_method
...
end

and also this:

class MyClass
  def self.a_class_method
    ...
  end
end

regards,
Ed

···

On Wed, Oct 05, 2005 at 05:45:49AM +0900, Louis J Scoras wrote:

Sean O'Halpin <sean.ohalpin@gmail.com> writes:

module IRB
class Context
   def evaluate(line, line_no)
     value =3D @workspace.evaluate(self, line, irb_path, line_no)
     puts "value =3D #{value}"
     set_last_value(value)
   end
end
end

(Note set_last_value)

Useful. Thanks!

James Edward Gray II <james@grayproductions.net> writes:

···

On Oct 3, 2005, at 8:11 PM, Mark J.Reed wrote:

    $objects.find_all do
        >obj>

I had to double-check that to convince myself it works. I thought I
remembered reading more than once that the block arguments had to be
on the same line as the start of the block.

Has that been changed, or is my memory fuzzy?

I've always done it this way, so it's worked at least since Ruby 1.6.