The maze book for programmers!
PragProg Amazon BN.com

Algorithms, circle mazes, hex grids, masking, weaving, braiding, 3D and 4D grids, spheres, and more!

DRM-Free Ebook

The Buckblog

assorted ramblings by Jamis Buck

The Dynamic Def

17 October 2015 — Playing around with Ruby's def statement. Also, the author introduces a throw-away project for playing interactive fiction games in irb — 8-minute read

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!

ifrb – Interactive Fiction for Interactive Ruby