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

L

Louis J Scoras

------=_Part_17982_9256152.1128372048201
Content-Type: text/plain; charset=ISO-8859-1
Content-Transfer-Encoding: quoted-printable
Content-Disposition: inline

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 ActiveRecord=
s
`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 b=
e
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=3Dnil)
portal =3D (opts[:through] || opts[:via] if opts.respond_to?:[]) || 'door'
class_variable_set:)@@exits, []) unless class_variables.include?:)@@
exits.to_s)
exits =3D 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 =3D self.class.exits
@description =3D 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 =3D> '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 =3D> '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 =3D Attic.new
a.describe

------=_Part_17982_9256152.1128372048201--
 
E

Edward Faulkner

--liOOAslEiF7prFVr
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline

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

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

--liOOAslEiF7prFVr
Content-Type: application/pgp-signature; name="signature.asc"
Content-Description: Digital signature
Content-Disposition: inline

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.1 (GNU/Linux)

iD8DBQFDQbSRnhUz11p9MSARAu6uAJ93tR4X5KKjXKA9TXIN0opHwozouQCgmYsu
PL0EQ3KtRkoxwOfTVkZZ6Fk=
=TOsh
-----END PGP SIGNATURE-----

--liOOAslEiF7prFVr--
 
S

Sean O'Halpin

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

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

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 =3D @location.exits[name]
if dest
@location =3D @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 =3D 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 a=
ll)
super
end

end

class Game
include Directions

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

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

# read game definition
instance_eval &block

end

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

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

def symbol(s)
s.to_s.to_sym
end

def start(room_identifier)
@location =3D @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 =3D=3D
location.identifier}.map{|key, thing| "There is a #{thing.description}
here."}
end

def main_loop
while input =3D 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 =3D> 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 =3D Game.new(name, &block)
g.look
g.main_loop
end

# Game definition

game "Ruby Adventure" do

directions :east, :west, :north, :south, :up, :down, :upstairs, :downstai=
rs

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 =3D> [:door, :garden],
:upstairs =3D> [: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 =3D> [: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 =3D> [: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 --
 
L

Louis J Scoras

------=_Part_439_20500209.1128386563313
Content-Type: text/plain; charset=ISO-8859-1
Content-Transfer-Encoding: quoted-printable
Content-Disposition: inline

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 =3D)

That helps a lot, thanks.
 
J

James Edward Gray II

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 . . .

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!
 
S

Sean O'Halpin

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

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

Regards,

Sean
 
S

Sean O'Halpin

Oops - that should have been:

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)

Sean
 
J

James Edward Gray II

$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?

James Edward Gray II
 
A

Adam Shelly

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 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 =3D [whiskey_bottle, frog, bucket, chain]
#...
end
#...
end

$g =3D Game.new
$valid_words =3D []

def method_missing symbol, *args
#if the game has not started, add the symbol to the list of valid to=
kens
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
 
L

Louis J Scoras

------=_Part_15709_8912280.1128458746832
Content-Type: text/plain; charset=ISO-8859-1
Content-Transfer-Encoding: quoted-printable
Content-Disposition: inline

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=
 
E

Edward Faulkner

--45Z9DzgjV8m4Oswq
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline
Content-Transfer-Encoding: quoted-printable

So what would be the best way to do this if the data needs to be read-wri= te?
=2E..
But what would you do if the classes in question weren't meant to be
singletons?=20

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:

http://whytheluckystiff.net/articles/seeingMetaclassesClearly.html
Trying to use module_eval to dynamically define a class method doesn't se= em
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 =3D 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

--45Z9DzgjV8m4Oswq
Content-Type: application/pgp-signature; name="signature.asc"
Content-Description: Digital signature
Content-Disposition: inline

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.1 (GNU/Linux)

iD8DBQFDQvE7nhUz11p9MSARAkmfAKDCzRPhLJ2E7D7Qr1Cql7wBvg7EFQCgzxzt
cENHN5h8qozkA+iVVQg2zV0=
=wf31
-----END PGP SIGNATURE-----

--45Z9DzgjV8m4Oswq--
 
M

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'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
 
D

Dave Burt

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
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Similar Threads


Members online

No members online now.

Forum statistics

Threads
473,769
Messages
2,569,578
Members
45,052
Latest member
LucyCarper

Latest Threads

Top