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.
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:
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:
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:
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.
Shrug.
So, what else is it good for?
Fibonacci!
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…
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.
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.
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 start
.
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 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:
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!
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:
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!