views:

70

answers:

3

I'm working on a module that, among other things, will add some generic 'finder' type functionality to the class you mix it into. The problem: for reasons of convenience and aesthetics, I want to include some functionality outside the class, in the same scope as the class itself.

For example:

class User
  include MyMagicMixin
end

# Should automagically enable:

User.name('Bob')   # Returns first user named Bob
Users.name('Bob')  # Returns ALL users named Bob 
User(5)            # Returns the user with an ID of 5
Users              # Returns all users

I can do the functionality within these methods, no problem. And case 1 (User.name('Bob')) is easy. Cases 2–4, however, require being able to create new classes and methods outside User. The Module.included method gives me access to the class, but not to its containing scope. There is no simple "parent" type method that I can see on Class nor Module. (For namespace, I mean, not superclass nor nested modules.)

The best way I can think to do this is with some string parsing on the class's #name to break out its namespace, and then turn the string back into a constant. But that seems clumsy, and given that this is Ruby, I feel like there should be a more elegant way.

Does anyone have ideas? Or am I just being too clever for my own good?

+3  A: 

I'd lean toward being too clever.

Even if there was an elegant solution, it seems rather odd to be including a module inside a class which creates classes outside the class.

jrallison
Nitpick: Actually, it creates *methods* outside the class, but I agree with you.
Jörg W Mittag
It's actually classes _and_ methods. For what it's worth, I agree too. But I like the idea of this API, and when I thought about other ways of achieving it (e.g., a class factory) they felt even _more_ magical or less Rubyish. Despite what it may look like above, I'm actively suspicious of magic, and when it has to happen I like to confine it to one place.
SFEley
What about something a bit closer to something like the ActiveRecord api?User.find(:name => "Bob"), User.find(5), User.all?Seems less magical all around and is IMO more semantic.
jrallison
Heh. Because part of my inspiration for this project is to demonstrate a working rebuttal to ActiveRecord. ActiveRecord is crufty and the overloading of the `find` method is terrible. It does umpteen different things depending on its parameter strategy. And if you really think it's less magical, try to explain the class of the collection returned by User.all to a newbie. --What I want is to try something new, with new semantics that I hope to make clearer and simpler. I might fail, but I expect it to be an _interesting_ failure.
SFEley
(BTW, I voted your answer up. It didn't directly answer what I asked, but it's helped me rethink whether such a large backflip to avoid a higher-level declaration is necessary. I'll keep cogitating. Thank you.)
SFEley
I figured your answer may be something along those lines. :) I agree it's not perfect. Good luck!
jrallison
+1  A: 

This is a problem that comes up sometimes on the mailinglists. It's also a problem that comes up in Rails. The solution is, as you already suspected, basically Regexp munging.

However, there is a more fundamental problem: in Ruby, classes do not have a name! A class is just an object like any other. You can assign it to an instance variable, to a local variable, to a global variable, to a constant or even not assign it to anything at all. The Module#name method is basically just a convenience method that works like this: it looks through the list of defined constants until it finds one that points to the receiver. If it finds one, it returns the first one it can find, otherwise it returns nil.

So, there's two failure modes here:

a = Class.new
a.name # => nil
B = a
B.name # => "B"
A = B
A.name # => "B"
  • a class might not have a name at all
  • a class might have more than one name, but Module#name will only return the first one it finds

Now, if someone tries to call As to get a list of As, they will be pretty surprised to find that that method doesn't exist, but that they can call Bs instead to get the same result.

This does actually happen in reality. In MacRuby, for example String.name returns NSMutableString, Hash.name returns NSMutableDictionary and Object.name returns NSObject. The reason for this is that MacRuby integrates the Ruby runtime and the Objective-C runtime into one, and since the semantics of an Objective-C mutable string are identical to a Ruby string, the entire implementation of Ruby's string class is essentially a single line: String = NSMutableString. And since MacRuby sits on top of Objective-C, that means that Objective-C starts first, which means that NSMutableString gets inserted into the symbol table first, which means it gets found first by Module#name.

Jörg W Mittag
Thanks, Jörg, for confirming that there isn't a more direct way. As for the failure mode, I'm not too worried about it. For one thing, we'd almost always know the class being pointed at exists and has a name because it's the one the module was mixed into. (The use case for this is such that mixing it into an anonymous class would be rare, and probably pointless.) For another, I also have some explicit declaration methods to avoid the magic. In any case, though, good point. And thanks.
SFEley
+1  A: 

In your example, User is just a constant that points to a Class object. You can easily create another constant pointer when MyMagicMixin is included:

module MyMagicMixin
  class <<self
    def self.included(klass)
      base.extend MyMagicMixin::ClassMethods
      create_pluralized_alias(klass)
    end

    private

    def create_pluralized_alias(klass)
      fq_name = klass.to_s
      class_name = fq_name.demodulize
      including_module = fq_name.sub(Regexp.new("::#{class_name}$", ''))
      including_module = including_module.blank? ? Object : including_module.constantize
      including_module.const_set class_name.pluralize, klass
    end
  end

  module ClassMethods
    # cass methods here
  end
end

Of course this doesn't answer whether you should do such a thing.

James A. Rosen
Thank you! And you're right, of course. Your answer looks right, but it's the sort of thing that I look at and think, "Wow, that's sort of ugly." (Not your code, I mean the way it has to do things.) When Ruby code turns ugly, I do try to stop and rethink whether I'm on the right track. In this case I will probably take another approach. Thanks to all of you.
SFEley