The Dynamic Def
Here’s a quiz for you. Without actually trying it, consider the following Ruby code. What would you expect it to do?
def foo
def bar
end
end
Here’s a hint: it’s syntactically (and semantically) valid. It parses, and runs. Given that, what does it do? What would happen if you were to call bar
? What about foo
? In what scope does that bar
method exist? When does it actually get defined–at parse time, or run time?
Perhaps you already know exactly what it’s going to do, but feel free to try it anyway. You’ll see it goes something like this:
def foo
def bar
end
end
bar #-> NameError!
foo #-> defines the `bar' method
bar #-> invokes the `bar' method
In other words, the method bar
does not exist, until the method foo
is invoked. At that point, the bar
method is defined, and becomes available.
However!
Looking at the above example, you might think that the method bar
becomes a method local to the specific object instance, but sadly this isn’t so. The thing about def
is that it tends to create methods at the class level. Just so here. Consider:
class Example
def foo
def bar
end
end
end
Example.new.foo
Example.new.bar #<-- this works!!!
Note that the second time we instantiate Example
, the bar
method exists! This is because we invoked foo
on the first instance, which defined the bar
method for us everywhere. Now, if we really wanted instance-local methods, we could declare those methods inside the object’s singleton class:
class Example
def foo
class <<self
def bar
end
end
end
end
e1, e2 = Example.new, Example.new
e1.foo
e1.bar
e2.bar #<-- BOOM! `bar' not yet defined on `e2'!
Still, that’s a bit cumbersome. And besides, you may be asking yourself: what’s the point? Why even bother?
I’m so glad you asked. :)
Memoization
If you’ve spent any time at all looking at Ruby code, you’ve almost certainly encountered this idiom:
def some_expensive_computation
@some_expensive_computation ||= begin
# .. perform computation
end
end
This is one common way of accomplishing method memoization. You perform the actual work of the method once, and then save the result. The next time you invoke that method, the cached result is returned instead.
Well…it turns out that we can use the Dynamic Def to do memoization for us.
def some_expensive_computation
def some_expensive_computation
@some_expensive_computation
end
@some_expensive_computation = # ...
end
Note, though, that you’ll probably need to define the inner method inside the singleton class; otherwise, you’ve just added the memoized method to every instance of the class. (Which is probably not what you wanted.)
def some_expensive_computation
class <<self
def some_expensive_computation
@some_expensive_computation
end
# or, more concisely:
attr_reader :some_expensive_computation
end
@some_expensive_computation = # ...
end
If you need justification for this, try a simple benchmark script. It turns out that using a nested def
for memoization results in a memoized method that is about 30% faster…but honestly, that’s not saying much. Both techniques are pretty blazing fast. On my computer, 500k iterations of accessing the memoized method took either 0.05s, or 0.07s, depending on which technique was used.
Shrug.
So, what else is it good for?
Fibonacci!
Check this out:
class Fib
def next
def next
def next
(@a, @b = @b, @a+@b).last
end
@b = 1
end
@a = 0
end
end
Now, we can compute the Fibonacci sequence:
seq = Fib.new
100.times { puts seq.next }
#-> 0
#-> 1
#-> 1
#-> 2
#-> 3
#-> 5
#-> 8
#-> 13
#-> 21
#-> 34
# ...
Fibonacci, without recursion, or conditions! Of course, if we try to instantiate a new Fib
and run it again…
seq2 = Fib.new
seq2.next #-> BOOM! "undefined method `+' for nil:NilClass"
It dies…because our successive next
methods were defined on the class, rather than the object. Easily fixed, but a little cumbersome. So, alright. Perhaps this approach to Fibonacci is best left as a novelty…
What else?
State Machines
Consider a coin-operated turnstyle. It has two states: locked, and unlocked. When locked, if you push on it, it won’t turn. You have to put a coin into it, which adds a credit and puts it in the unlocked state. At that point, it will turn when pushed, subtracting a credit, and when the credits get back to zero, it returns to the locked state.
Lookit.
def start
@coins ||= 0
def state
:locked
end
def insert_coin
@coins += 1
def push
@coins -= 1
(@coins < 1) ? start : state
end
def state
:unlocked
end
state
end
def push
puts "Nope! Nice try, though."
state
end
state
end
Start the state machine by invoking the start
method. Here’s a sample transcript:
start #-> :locked
push #-> "Nope! Nice try, though."
insert_coin #-> :unlocked
push #-> :locked
push #-> "Nope! Nice try, though."
insert_coin #-> :unlocked
insert_coin #-> :unlocked
insert_coin #-> :unlocked
push #-> :unlocked
push #-> :unlocked
push #-> :locked
push #-> "Nope! Nice try, though."
# ...
That’s fun. But it’s just a tantalizing taste of my favorite use of state machines… interactive fiction.
Interactive Fiction
Here’s a fun exercise. Let’s write an interactive fiction game that you play entirely in irb!
Each scene (or “room”) will be method. Within that method, other methods will be defined that will implement actions to be taken within that room. Then, before moving to another room (by calling the other room’s function), we’ll undef
all the functions we added.
Here’s a simple example:
def start
def look
puts "Oh, look! You're in a sparkly room! You can go north."
end
def north
puts "Right, then. North you go!"
undef look
undef north
room_to_the_north
end
look
end
def room_to_the_north
def look
puts "Uh, huh. Look at that. Nothing here."
puts "You can go south, if you want to."
end
def south
puts "Southward!"
undef look
undef south
start
end
look
end
Now, let’s load that up in irb, and start the game by typing start
.
irb> start
Oh, look! You're in a sparkly room! You can go north.
=> nil
irb> look
Oh, look! You're in a sparkly room! You can go north.
=> nil
irb> north
Right, then. North you go!
Uh, huh. Look at that. Nothing here.
You can go south, if you want to.
=> nil
irb> south
Southward!
Oh, look! You're in a sparkly room! You can go north.
=> nil
# etc...
Not a bad start. We can make it more polished, though. Did you know that you can customize irb, to control things like the prompt, and how it displays return values?
IRB.conf[:PROMPT][:GAME] = {
:PROMPT_I => "%N> ", # "initial" prompt
:PROMPT_C => "..", # continued command
:PROMPT_S => "..", # continued string
:PROMPT_N => "..", # nested command
:RETURN => "" # formatting for return values
}
# `context' is an object provided by irb, for
# configuring irb.
context.prompt_mode = :GAME
This will simpilfy the IRB prompt, and hide return values. (That way we don’t get that distracting => nil
after every command.) The prompt will still say irb
, though…because of that %N
in the PROMPT_I
setting.
Let’s change that. Instead of saying irb
, let’s have it say the name of the room we’re in. It’s as easy as telling irb what the “irb name” is, every time we enter a room:
def start
context.irb_name = "sparkly room"
# ...
end
def room_to_the_north
context.irb_name = "north room"
# ...
end
The transcript starts to look quite a bit cleaner now:
irb> start
Oh, look! You're in a sparkly room! You can go north.
sparkly room> look
Oh, look! You're in a sparkly room! You can go north.
sparkly room> north
Right, then. North you go!
Uh, huh. Look at that. Nothing here.
You can go south, if you want to.
north room> south
Southward!
Oh, look! You're in a sparkly room! You can go north.
# etc...
Note that because this is just an irb prompt, we can do some really cool things. Our interactive fiction platform lets us evaluate arbitrary Ruby code! This means we can have commands that accept lambdas and blocks, or iterate over commands. Let’s say we want someone to give a man a fish, a hundred times:
def start
# ...
@fish ||= 0
def give(thing)
if thing == :fish
@fish += 1
if @fish == 100
puts "Thank you! I have 100 fish now!"
elsif @fish < 100
puts "Not there yet. I need #{100-@fish} more fish."
else
puts "Whoa! TOO MANY FISH!"
end
else
puts "Um, I'm not sure what a #{thing} is..."
end
end
# ...
end
The interaction might go something like this:
sparkly room> give :fish
Not there yet. I need 99 more fish.
sparkly room> give :fish
Not there yet. I need 98 more fish.
sparkly room> 98.times { give :fish }
Not there yet. I need 97 more fish.
Not there yet. I need 96 more fish.
Not there yet. I need 95 more fish.
# ...
Thank you! I have 100 fish now!
sparkly room>
One last improvement. It kind of kills the mood to have to use a symbol like that. Wouldn’t it be nice if we could just do give fish
and have it work? Well, we can. I’m not normally one to endorse abuse of method_missing
, but for a throw-away project like this, why not!
class <<self
def method_missing(sym, *args)
if args.empty?
sym
else
super
end
end
end
We declare method_missing
inside the singleton class for irb, because otherwise it will get added to the Object
class, and that wreaks all kinds of havoc. (You’ll find irb won’t be very happy with that.) Our new method_missing
just takes every method call with zero arguments and has it return its name as a symbol. Thus fish
returns :fish
, and so forth. Our interaction now looks much nicer:
sparkly room> give fish
Not there yet. I need 99 more fish.
Where else to take this? How about adding an inventory, and the ability to pick up and drop objects? How about adding some automation to the room transitions, so that we don’t have to manually undef the methods we added? I’ll leave all that as an exercise for you, Dear Reader, because this article is already nearly a novel.
However, because I can rarely leave well-enough alone, I’ll close by showing you a little project I’ve called “ifrb”–interactive fiction for irb. It’s also a gem, so you can just gem install ifrb
. Once installed, just run ifrb basil
to enjoy a simple introductory adventure featuring Basil Smockwhitener (wizard and gentleman) and his stalwart manservant Fabian.
Or, you know. Write your own adventure. Enjoy!