There's been quite a bit of discussion lately about how much Ruby is and isn't like Lisp. While I've known of Lisp for some time now, I have never really taken the time to play with it until just recently. I started with this great tutorial:
Of course, just reading it didn't tell me all about the differences between Ruby and Lisp, so I decided to translate it to Ruby. That taught me a lot, so I thought I would share.
This week's Ruby Quiz is to translate the game built in the above tutorial into the equivalent Ruby.
The tutorial is very interactive, so you should probably target irb as your platform. (I did.) Don't worry about the prose here, we're just translating the code from the tutorial.
I would say there are probably two extremes for how this can be done. You can basically choose a direct command-for-command translation or decide to keep the premise but translate the code to typical Ruby constructs. I think there's merit to both approaches. I took the direct translation route for instance, and I had to do a fair bit of thinking when I hit the section on Lisp's macros. On the flip side, I think it would be great too wrap all the data in Ruby's objects and use the tutorial to show off concepts like Ruby's open class system. So aim for whatever approach interests you.
Quiz Summary
I'm not going to show one entire solution this week, but instead try to hit some highlights from many. We received a variety of solutions with impressive insights and downright scary hacks. Let's take the tour.
Cleaning Up the Data
The tutorials data format, a nested group of lists, may be very Lispish, but it's not how we do things in Rubyland. Dominik Bathon did a subtle but effective translation that started with the definition of two simple Structs:
Location = Struct.new(:description, :paths)
Path = Struct.new(:description, :destination)
# ...
end
Mix in a few Hashes and already the data has a lot more structure:
def initialize
@objects = [:whiskey_bottle, :bucket, :frog, :chain]
@map = {
:living_room => Location.new(
"you are in the living-room of a wizard's house. " +
"there is a wizard snoring loudly on the couch.",
{ :west => Path.new("door", :garden),
:upstairs => Path.new("stairway", :attic) }
),
:garden => Location.new(
"you are in a beautiful garden. " +
"there is a well in front of you.",
{:east => Path.new("door", :living_room)}
),
:attic => Location.new(
"you are in the attic of the abandoned house. " +
"there is a giant welding torch in the corner.",
{:downstairs => Path.new("stairway", :living_room)}
)
}
@object_locations = {
:whiskey_bottle => :living_room,
:bucket => :living_room,
:chain => :garden,
:frog => :garden
}
@location = :living_room
end
# ...
end
The rest of Dominik's translation came out quite nice and it's not a lot of code. Do look it over.
One thing I would really want to see in a Ruby version of this tutorial is the use of Ruby's open class system to slowly build up a data solution. Kero's code was working on this:
def initialize(descr, *elsewhere)
@descr = descr
@elsewhere = elsewhere
end
end
# ...
class Area
attr_reader :descr
end
class Area
attr_reader :elsewhere
def Area::path(ary)
"there is a #{ary[1]} going #{ary[0]} from here."
end
end
class Area
def paths
elsewhere.collect {|path|
Area::path path
}
end
end
I think that would be a great way to slowly unfold the tutorial.
Kero also figured out the obvious way to eliminate the very first command in the tutorial, be sure to look that up.
Replacing the Macros
The interesting part of the tutorial in question is when it begins using macros to redefine the interface. In Lisp, that allows the tutorial to go from using code like:
(walk-direction 'west)
To:
(walk west)
Of course, Ruby doesn't have macros. (That's the discussion that inspired this quiz!) So, most of the people who solved it handled the interface with a different Ruby idiom. The solution we'll examine here doesn't replace all instances of Lisp macro usage. Different applications would require different Ruby idioms to deal with, but the moral (to me, anyway) is use the tools your language provides. Macros are one of the things that make Lisp act like Lisp. On the other hand, method_missing() is a Ruby tool:
def method_missing(method_id, *args)
if args.empty?
method_id
else
[method_id] + args
end
end
end
That's some code from Brian Schroeder's direct translation of the tutorial. The key insight at work here is how Ruby would see a line like:
The answer is:
Now we can understand Brian's method_missing() hack. If a method isn't defined, like west(), method_missing() will be called and Brian just has it return the method name, so other methods will get it as an argument. In other words, the above call sequence is simplified to:
The walk() method is defined and knows how to handle a :west parameter.
The second half of method missing does one more trick. To understand it, we need to look at a different example. Imagine the following call sequence from later in the game:
That will work as I've shown it, assuming weld() is a real method and knows what to do with a :chain and :bucket, because Ruby sees the call as:
Which we have already seen would get simplified to:
Brian went one step further though and eliminated the comma:
Ruby sees that as:
The last call resolves as we have already seen:
But chain() is also handled by method_missing() and now it has an argument. That's what the second part of method_missing() is for. It adds the method name to the argument list and returns it, which leaves us with:
As long as weld() knows how to handle the Array, you can do without the comma.
Brian uses a different set of Ruby tools, define_method() and instance_eval(), to replace the game action macro. I'm not going to show it here in the interests of space and time, but do take a peek at the code. It's fancy stuff.
A Warning
Use a global method_missing() hack like the above, only when you really know what you are doing. When we're just fooling with irb like this, it is pretty harmless, but it still tripped me up a few times. Many Ruby errors are hidden under the rug when you define a global catch-all like this. That can make it tough to bug hunt.
Some solutions restricted the method_missing() hack to irb only and/or reduced the amount of things method_missing() was allowed to handle. These are good cautionary measures to take, when using a hack like this.
Reversing the Problem
A couple of people tried bringing Lisp to Ruby, instead of Rubifying Lisp. Watch how irb is responding to Dave Burt's solution:
irb(main):001:0> require 'lisperati'
(YOU ARE IN THE LIVING_ROOM OF A WIZARDS HOUSE. THERE IS A WIZARD SNORING
LOUDLY ON THE COUCH. THERE IS A DOOR GOING WEST FROM HERE. THERE IS A
STAIRWAY GOING UPSTAIRS FROM HERE. YOU SEE A WHISKEY_BOTTLE ON THE FLOOR.
YOU SEE A BUCKET ON THE FLOOR.)
=> true
irb(main):002:0> pickup bucket
=> (YOU ARE NOW CARRYING THE BUCKET)
irb(main):003:0> walk 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 FROG ON THE FLOOR. YOU SEE A CHAIN ON
THE FLOOR.)
irb(main):004:0> inventory[]
=> (BUCKET)
I can't decide if that's unholy or not, but it sure is cool. Here's the code Lispifying the Arrays:
def inspect # (JUST FOR FUN, MAKE ARRAYS LOOK LIKE LISP LISTS)
'(' + map{|x| x.upcase }.join(" ") + ')'
end
end
One simple override on inspect() gives us Lisp style output. Yikes.
There's more Lisp goodness hiding in Dave's code, so be sure and give it a look.
Daniel Sheppard also took a very Lispish approach, building a Lisp interpreter and then feeding in the Lisp code directly from the web site:
lisp = Object.new
lisp.extend(Lisp)
lisp.extend(Lisp::StandardFunctions)
require 'open-uri'
require 'fix_proxy.rb'
open("http://www.lisperati.com/code.html") { |f|
input = f.readlines.join.gsub(/<[^>]*>/, "")
#puts input
lisp.lisp(input)
}
commands = [
[ "(pickup whiskey-bottle)",
"(YOU ARE NOW CARRYING THE WHISKEY-BOTTLE)" ]
]
open("http://www.lisperati.com/cheat.html") { |f|
f.each { |line|
line.chomp!
line.gsub!("<br>","")
if /^>(.*)/ === line
line = $1
line.gsub!("Walk", "walk") #bug in input
commands << [line, ""]
else
#bugs in input
line.gsub!("WIZARDS", "WIZARD'S")
line.gsub!("ATTIC OF THE WIZARD'S", "ATTIC OF THE ABANDONED")
commands[-1][1] << line
end
}
}
commands.each do |c|
puts c[0]
result = lisp.lisp(c[0])
result = result.to_lisp.upcase
unless result == c[1]
puts "Wrong!"
p result
p c[1]
break
end
end
Here you can see that openuri is used to load pages from the tutorial site, which are parsed for code and fed straight to the Lisp interpreter. I must admit that I never expected to see a solution like that!
I won't show the lisp.rb file here in the interests of time and space, but hopefully the above has you curious enough to take a peek on your own. You won't be sorry you did.
Domain Specific Languages (DSLs)
I'm told Jim Weirich is giving a talk on DSLs at RubyConf, and I believe he actually intends to use this very problem area to discuss them. Some of you have a head start on that now.
Both Jim Menard and Sean O'Halpin sent in the beginning of text adventure frameworks for Ruby. Their goal seemed to be to create reasonable syntax for using Ruby in the creation of such games and there are interesting aspects to each approach.
Let's look at a little bit of Sean's code first:
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
Interesting use of blocks and method calls there, isn't it? What's really neat is that under the hood this is a fully object oriented system. The method calls just simplify it for you. Have a look at the game() method implementation, for example:
g = Game.new(name, &block)
g.look
g.main_loop
end
I love this synthesis of Ruby objects with trivial interface code.
Going a step further, it should be possible to derive the name() attribute from the Symbol parameter to room() and thing(), shaving off some more redundancy.
As you can see, these method don't quite use the typical Ruby syntax. Why is it `name 'frog'` and not `name = 'frog'`, for example? The reason is that the blocks in this code are instance_eval()ed, to adjust self for the call. Unfortunately, because of the way Ruby syntax is interpreted, `name = 'frog'` would be assumed to be a local variable assignment instead of a method call. That forced Sean to use this more Perlish syntax.
To follow up on that, let's see how those attribute methods are implemented:
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
Looks like we need to see the magic has() method:
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
Notice that the defined attribute methods have different behavior depending on the presence of any arguments in their call. Omit the arguments and you're calling a getter. Add an argument to set the attribute instead.
For more typical Ruby idioms, we turn to Jim's code:
$world.player.names = ['me', 'myself']
$world.player.long_desc = 'You look down at yourself. Plugh.'
living_room = Room.new(:living_room) { | r |
r.short_desc = "The living room."
r.names = ['living room', 'parlor']
r.long_desc = "You are in the living-room of a wizard's house."
r.west :garden, "door"
r.up :attic, "stairway"
}
wizard = Decoration.new { | o |
o.location = living_room
o.short_desc = 'There is a wizard snoring loudly on the couch.'
o.names = %w(wizard)
o.long_desc = "The wizard's robe and beard are unkempt. He sleeps " +
"the sleep of the dead. OK, the sleep of the really, " +
"really sleepy."
}
# ...
whiskey_bottle = Thing.new { | o |
o.location = living_room
o.short_desc = "whiskey bottle"
o.names = ['whiskey bottle', 'whiskey', 'bottle']
o.long_desc = "A half-full bottle of Old Throat Ripper. The label " +
"claims it's \"the finest whiskey sold\" and warns " +
"that \"mulitple applications may be required for " +
"more than three layers of paint\"."
}
bucket = Container.new { | o |
o.location = living_room
o.short_desc = "bucket"
o.long_desc = "A wooden bucket, its bottom damp with a slimy sheen."
}
# ...
$chain_welded = false
$bucket_filled = false
class << $world
def have?(obj)
obj.location == player
end
# ...
end
# ================================================================
startroom living_room
play_game
In that code you can see Rooms being built, Decorations added, Things created and even custom methods added to the $world. If you have any experience with Interactive Fiction (IF--a fancy name for these text adventure game), this declarative style code is probably looking pretty familiar.
Jim went so far as to do a minimal port of TADS (Text ADventure System). You can see the Ruby version, RADS, pulled in on the first line.
The main difference you see here is the use of object constructors and that the blocks are passed the objects to configure, allowing the use of standard Ruby attribute methods.
Both solutions are very interesting and worth digging deeper into, when you have some time.
Wrap Up
Just because I didn't mention a solution does not mean it wasn't interesting, especially this week. A lot of code came in and there were great tidbits all around. If you want to learn the great Ruby Voodoo, start reading now!
Thanks so much to all who played with this problem or even just discussed variations on Ruby Talk. As always, you taught me a lot.
Tomorrow's Ruby Quiz: Automated ASCII Art...