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?
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:
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.
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:
Note that the second time we instantiate
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:
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. :)
If you’ve spent any time at all looking at Ruby code, you’ve almost certainly encountered this idiom:
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.
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.)
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.
So, what else is it good for?
Check this out:
Now, we can compute the Fibonacci sequence:
Fibonacci, without recursion, or conditions! Of course, if we try to instantiate a new
Fib and run it again…
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…
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.
Start the state machine by invoking the
start method. Here’s a sample transcript:
That’s fun. But it’s just a tantalizing taste of my favorite use of state machines… 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:
Now, let’s load that up in irb, and start the game by typing
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?
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
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:
The transcript starts to look quite a bit cleaner now:
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:
The interaction might go something like this:
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!
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, and so forth. Our interaction now looks much nicer:
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!