Ruby Voodoo

Deep dives into random corners of my favorite programming language.

7

OCT
2008

DSL Block Styles

There's an argument that rages in the Ruby camps: to instance_eval() or not to instance_eval(). Most often this argument is triggered by DSL discussions where we tend to want code like:

configurable.config do
  width 100
  mode  :wrap
end

You can accomplish something like this by passing the block to instance_eval() and changing self to an object that defines the width() and mode() methods. Of course changing self is always dangerous. We may have already been inside an object and planning to use methods from that namespace:

class MyObject
  include Configurable       # to get the config() method shown above

  def initialize
    config do
      width calculate_width  # a problem:  may not work with instance_eval()
    end
  end

  private

  def calculate_width        # the method we want to use
    # ...
  end
end

In this example, if width() comes from a different configuration object, we're in trouble. The instance_eval() will shift the focus away from our MyObject instance and we will get a NoMethodError when we try to call calculate_width(). This may prevent us from being able to use Configurable in our code.

A common solution is to pass the object with the width() and mode() methods into the block. You can then make calls on that object and keep the same self. This could fix the above problem example:

config do |c|
  c.width calculate_width
end

I imagine most of us agree that's not quite as smooth, but it tends to get viewed as a necessary evil. It's just not safe to always use instance_eval(). I think there are some issues with that line of thinking though:

  • Sometimes instance_eval() is OK and we would prefer to have the prettier syntax when it is
  • Library authors are making this blanket decision for all the use cases
  • We have a super dynamic language here so we should be able to have it both ways

It turns out that we can accommodate both schools of thought rather easily. You can ask Ruby to bundle up any block into a Proc object and Proc objects have an arity() method that will tell you how many arguments they expect. We can use that to determine when to switch strategies:

class DSL
  def initialize(&dsl_code)     # creates the Proc
    if dsl_code.arity == 1      # the arity() check
      dsl_code[self]            # argument expected, pass the object
    else
      instance_eval(&dsl_code)  # no argument, use instance_eval()
    end
  end

  def do_something
    puts "Doing something..."
  end
end

DSL.new { |d| d.do_something }
# >> Doing something...
DSL.new { do_something }
# >> Doing something... 

Users of this code can now decide how they want it to work based on their needs as the examples show.

With a language like Ruby these limitations just become opportunities for showing off how dynamic our code can be. Don't be so quick to give in to necessary evils.

In: Ruby Voodoo | Tags: DSLs & Style | 7 Comments
Comments (7)
  1. Trans
    Trans October 7th, 2008 Reply Link

    The work around.

    def initialize
      this = self
      config do
        width this.calculate_width
      end
    end
    
    1. Reply (using GitHub Flavored Markdown)

      Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

      Ajax loader
    2. James Edward Gray II
      James Edward Gray II October 7th, 2008 Reply Link

      Yeah, but gross, right? :)

      1. Reply (using GitHub Flavored Markdown)

        Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

        Ajax loader
  2. AdamSanderson
    AdamSanderson October 7th, 2008 Reply Link

    You might check out _why's mixico: http://hackety.org/2008/10/06/mixingOurWayOutOfInstanceEval.html

    I haven't had a chance to play with it yet, but it seems to be another approach to the same problem.

    1. Reply (using GitHub Flavored Markdown)

      Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

      Ajax loader
  3. James Edward Gray II
    James Edward Gray II October 7th, 2008 Reply Link

    Jim Weirich also posted a pure Ruby MethodDirector implementation to Ruby Core for mixing the methods of multiple objects into one namespace.

    1. Reply (using GitHub Flavored Markdown)

      Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

      Ajax loader
  4. John Mair
    John Mair October 11th, 2008 Reply Link

    it's a nice idea, thanks :)

    1. Reply (using GitHub Flavored Markdown)

      Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

      Ajax loader
  5. Julio Fernández
    Julio Fernández March 31st, 2010 Reply Link

    OK
    It returns US-ASCII if all the bytes in the content is < 128
    but return ASCII-8BIT if I put any char >128 in the content.

    1. Reply (using GitHub Flavored Markdown)

      Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

      Ajax loader
    2. James Edward Gray II
      James Edward Gray II March 31st, 2010 Reply Link

      Right. At that point, you will need to call force_encoding(), as I was referring to earlier, to set the proper encoding for your data.

      1. Reply (using GitHub Flavored Markdown)

        Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

        Ajax loader
Leave a Comment (using GitHub Flavored Markdown)

Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

Ajax loader