Using Ruby provides a lot of “I wonder if we could do that?” moments, little things I play with to satisfy my curiosity rather than any actual purpose. My most recent one came when thinking about how Merb creates file paths from strings:

class String
  def /(o)
    File.join(self, o.to_s)
  end
end

"files" / "are" / "here" # => "files/are/here"

I wonder if we could lose all those extra quotes? Let’s start by removing them and see what happens:

files/are/here

# undefined local variable or method ‘files’ for main:Object

Obviously each of the individual bits need to be made into strings. This could be done using method_missing, but that will hijack other uses of the method and potentially mask real failures. What we need is a scope to make it safe to use method_missing. I first saw a way of doing this in the hobo framework: we can instance_eval a block in the context of an object which just defines method_missing:

class PathConverter < BlankSlate
  def method_missing(method)
    method.to_s
  end
end

def path(&block)
  PathConverter.new.instance_eval(&block)
end

path { files/are/here } # => "files/are/here"

We inherit from BlankSlate to avoid any clashes with methods that already exist on object. All methods are converted into Strings, and then we can use the Merb String method. Now, how about names that start with capitals?

path { Users/ohthatjames/Documents } 
# => uninitialized constant Users

OK, now we have to capture const_missings as well. How about this?

class PathConverter
  def self.const_missing(const)
    const.to_s
  end
end
path { Users/ohthatjames/Documents } 
# => uninitialized constant Users

Hmmm. We’re in the context of PathConverter, why doesn’t this work? After a bit of googling, there’s an explanation:

All that instance_eval does is set self and execute the block. Constants don't depend on self for their scope; they use a kind of quasi-static scoping, and the Bar in your block is resolved (unsuccessfully) as Bar from the top level.

You'd need to add your const_missing method to Object, so as to cover the top level.

Right. This is not going to be pretty. We’re going to have to define const_missing on Object. But we can’t just completely override it, that’d break Rails and many other programs. A potential solution would be to dynamically redefine const_missing in the path method, then replace it with the old method when we’re done. However the simplest thing to do is to use a global variable:

module ConstantToString
  def const_missing(const)
    return const.to_s if $convert_to_strings
    super
  end
end
Object.extend ConstantToString

def path(&block)
  $convert_to_strings = true
  result = CodeConverter.new.instance_eval(&block)
  $convert_to_strings = false
  result
end

path { Users/ohthatjames/Documents } 
# => "Users/ohthatjames/Documents"

So we set and unset $convert_to_strings around the instance_eval and only convert the constant to a string if $convert_to_strings is true. I can’t say I like it, but it works. We also extend Object with a module rather than just redefining const_missing so that we can maintain any other changes made to const_missing.

How about absolute paths?

path { /Users/ohthatjames/Documents } 
# => unknown regexp options - hthatja

The Ruby parser seems to interpret this as a regular expression. Can we use this to our advantage?

path { ///Users/ohthatjames/Documents } 
# => NoMethodError: undefined method `/' for //:Regexp

This is valid Ruby, we just need a / method for Regexp:

class Regexp
  def /(o)
    ""/o
  end
end

path { ///Users/ohthatjames/Documents } 
# => "/Users/ohthatjames/Documents"

This works, but there are still some things I can’t find solutions for: we can’t use a Ruby keyword or a filename with spaces. We would still have to put quotes around them.

So, is it possible to remove the quotes? Yes, mostly. Is it at all practical? Definitely no. It’s a glorified string concatenator, achieving something that can be done in much easier, cleaner ways. The hoops we have to jump through for such a questionable endeavour make it hard to sell: overriding both method_missing and const_missing, using a global variable and defining a method on Regexp is just going to make life difficult for anyone wanting to extend the code or for those who find their own code clashing with it.

Still, it’s a good example of how the power of Ruby can provide solutions to things that at first seem impossible. But it’s an even better example of when to leave certain features of Ruby well enough alone!

Full code:

class String
  def /(other=nil)
    return self if other.nil?
    File.join self, other.to_s
  end
end

module ConstantToString
  def const_missing(const)
    return const.to_s if $convert_to_strings
    super
  end
end
Object.extend ConstantToString

class PathConverter
  def method_missing(method)
    method.to_s
  end
end

class Regexp
  def /(other)
    ""/other
  end
end

def path(&block)
  $convert_to_strings = true
  result = PathConverter.new.instance_eval(&block)
  $convert_to_strings = false
  result
end