1

I am using Ruby on Rails v3.2.2. In a module I am trying to "dynamically" open a class so to add to it a Ruby on Rails "scope method" that makes use of a local variable, this way:

module MyModule
  extend ActiveSupport::Concern

  included do
    # Note: The `CLASS_NAME` is not the class where `MyModule` is included. That
    # is, for instance, if the including class of `MyModule` is `Article` then
    # the `CLASS_NAME` is `User`.
    CLASS_NAME           = self.get_class_name.constantize # => User
    counter_cache_column = self.get_counter_cache          # => "counter_count"

    class CLASS_NAME
      def self.order_by_counter
        order("#{counter_cache_column} DESC")
      end
    end
  end
end

If I run the above code, I get the following error:

NameError
undefined local variable or method `counter_cache_column' for #<Class:0x0000010775c558>

It happens because the counter_cache_column in not called in the context of the module. How should I properly state the order_by_counter scope method?


Bonus: What do you advice about the above "so dynamic" implementation?

Backo
  • 18,291
  • 27
  • 103
  • 170
  • I would like to improve the composition of your question, but I am having troubles myself to understand its meaning. What is a "scope method"? What do you mean by "dynamically" opening a class? Do you want to say that you know how to open a class "not dynamically"? And why are you surprised that a local variable declared outside the 'class CLASS_NAME' statement is not available inside? – Boris Stitnicky Oct 16 '12 at 12:05
  • Further to @Boris' comment, what do you mean by "opening" a class (dynamically or otherwise). We all believe we know what that means but to my knowledge Ruby does not define the term. If we have defined a class `C`, we can agree that writing `class C` "opens" the class, but what about `C.define_method(:cat) do ...end`. Does that "open" the class? I personally avoid the use of the term. – Cary Swoveland Apr 02 '19 at 17:48

4 Answers4

3

The included block provided by ActiveSupport::Concern is evaluated within the scope of the including class. In other words, you've "reopened" the class within this block. If the including class inherits from ActiveRecord::Base, you can use any AR class macros, e.g. scope, has_many, attr_accessible, etc.:

module MyModule
  extend ActiveSupport::Concern

  included do
    scope :order_by_counter, order("#{self.get_counter_cache} DESC")
  end

end

This assumes that 'get_counter_cache` is already defined as a class method in the including classes (though this isn't clear from the code you've shown).

rossta
  • 11,394
  • 1
  • 43
  • 47
  • The `CLASS_NAME` is *not* the class where `MyModule` is included. That is, for instance, if the including class of `MyModule` is `Article` then the `CLASS_NAME` is `User`. Sorry if I was not clear. – Backo Oct 16 '12 at 12:37
  • 1
    Yes, that wasn't clear initially. I would strongly recommend against this approach; you're basically [creating a side effect](http://blogs.msdn.com/b/alfredth/archive/2007/03/08/programming-proverbs-8.aspx) (manipulating a second object outside of the scope while modifying the first). This sort of thing is easy to forget and can be very difficult to track down in the future when bugs start to surface. – rossta Oct 16 '12 at 13:10
  • You are right but I think that in my case "the game *is* worth the candle" ("it *is* worth the trouble/the effort"). Documentation can do the rescue in such cases? – Backo Oct 16 '12 at 16:23
  • I can't make that judgement for you. But, when I find myself trying to write something that requires explanation, I usually find that there's another way, preferring to err on the side of [POLS](http://en.wikipedia.org/wiki/Principle_of_least_astonishment). – rossta Oct 16 '12 at 16:57
  • I don't understand how the POLS fits in "creating a side effect". Can you providing an explanation that better relates to my case? – Backo Oct 16 '12 at 18:42
  • POLS is relative to one's audience, which in this case, includes other members of your dev team and yourself. As a developer on a typical Ruby team, I'd expect that including a module in a class would modify only the including class. I'd therefore probably be surprised to find a module that also modifies a second class. Your example produces side effects that are unexpected compared with common usages of the Ruby module pattern. Comments might help or perhaps your team chooses to adopt different conventions where this sort of thing isn't so surprising. As for me, I'd look for another way. – rossta Oct 16 '12 at 19:08
  • OK, but in my case I think it is hard to "look for another way" since statements that make possible to "build / implement the internals" of the `get_class_name` method (reminder: `get_class_name.constantize # => User`) *depend and are strictly related to the class* including `MyModule` (that is, for example, parameters as-like `:User` are stated in the class including `MyModule`). So, I was in trouble on deciding on how to proceed... and finally I ended up to use the approach as code-described in the question. If you have any ideas for this, I am pleased to hear that. – Backo Oct 17 '12 at 07:28
  • Another approach could be to add a [complementary module to User instead](https://gist.github.com/3904856) – rossta Oct 17 '12 at 10:29
  • Thank you for your effort, but the refactoring process that led me to the code-approach present in the question is just because I am switching from the "complementary module" (as in your example) to a *"central place"* (the `MyModule` module) where to "add" / "provide" / "keep" all related features. – Backo Oct 19 '12 at 00:49
1

counter_cache_column is a local variable. Local variable are local to the scope they are defined in (that's why they are called local variables).

In this case, the scope is the block passed to included.

The class definition and the method definition create a new empty scope. Only blocks create nested scopes, so, you need to use a block to defined your method. Thankfully, there is a way to do so: by passing a block to define_method:

module MyModule
  extend ActiveSupport::Concern

  included do
    klass                = get_class_name.constantize # => User
    counter_cache_column = get_counter_cache          # => "counter_count"

    klass.define_singleton_method(:order_by_counter) {
      order("#{counter_cache_column} DESC")
    }
  end
end

I made some other style improvements:

  • self is the implicit receiver in Ruby, there is no need to specify it
  • CLASS_NAME is misleading: it doesn't contain the name of the class, it contains the class itself
  • also, I don't see why it would need to be a constant
Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
  • Thank you. I also opened another question that could be related to this one: [How to alias a class method within a module?](http://stackoverflow.com/questions/12917680/how-to-alias-a-class-method-within-a-module). – Backo Oct 16 '12 at 16:11
0

Local variables are not passed on to reopened classes.

module MyModule
  extend ActiveSupport::Concern

  included do
    counter_cache_column = self.get_counter_cache # => "counter_count"

    class_eval <<-RUBY, __FILE__, __LINE__+1
      def self.order_by_counter               # def self.order_by_counter
        order("#{counter_cache_column} DESC") #   order("counter_count DESC")
      end                                     # end
    RUBY
  end
end
simonmenke
  • 2,819
  • 19
  • 28
-1

There are many quick and dirty ways of achieving what you want. For example, if you want the symbol 'counter_cache_column' to mean something outside its scope, you could declare it as a method rather than a local variable:

included do
  CLASS_NAME           = self.get_class_name.constantize # => User
  def counter_cache_column; get_counter_cache end        # => "counter_count"

  class CLASS_NAME
    def self.order_by_counter
      order("#{counter_cache_column} DESC")
    end
  end
end
Boris Stitnicky
  • 12,444
  • 5
  • 57
  • 74
  • 1
    please don't promote hackish/easy-to-break solutions – simonmenke Oct 16 '12 at 12:11
  • 1
    I am aware of that, but the question itself is quite shoddy. I actually didn't mean to answer it at all, I was only trying to uderstand and reword it, and ended up contributing my 5 cents :)) – Boris Stitnicky Oct 16 '12 at 12:12
  • I think his question is perfectly clear. Maybe you should read up on the more subtle Ruby features – simonmenke Oct 16 '12 at 12:15
  • Then tell me what is 'scope method'. The title itself is unclear. Also, you are on SO, so do not assume that the people here lack subtlety like your high school classmates. – Boris Stitnicky Oct 16 '12 at 12:16
  • a 'scope method' is what `scope :hello, where(...)` defines. `scope :order_by_counter, order("#{self.get_counter_cache} DESC")`is equivalent to the `class_eval <<-RUBY ... RUBY` portion of my answer – simonmenke Oct 16 '12 at 12:19
  • I am eating my words. You were right and I was wrong, being uninformed about #scope method. – Boris Stitnicky Oct 16 '12 at 12:22