Alan Coromano
Alan Coromano

Reputation: 26038

Module variable doesn't keep its state

I have a module in Ruby containing one method and one variable:

module ApplicationHelper
  @var1 = MyModel::CONST1.each_with_index.map { |item, i| [item, i] }
  def method1 v1, v2, v3 = @var1[0]
    #....
    # access to @var1
  end
end

The reason I want @var1 to be a variable is that if it was a method it would be evaluated each I access to it within method1. It's not reasonable and thus, I want it to be evaluated only once.

However, when I call this method1 (it's a helper method) via ajax (ajax-based dialog which uses it), @var1 turns out to be nil. Likewise, during a first page loading it's filled out as I expect.

What do I do about it?

Upvotes: 0

Views: 104

Answers (1)

Aaron K
Aaron K

Reputation: 6971

Your context is not the same (see):

module ApplicationHelper
  p self # => ApplicationHelper

  def method1
    p self # => #<OtherClass:0x007fbe032cf6f0> an instance of an object
  end
end

Ruby doesn't work like Java in the sense that you aren't able to set object instance variables in the context of a module (or class) as you did. In your code, @var1 = MyModel::CONST1.each_with_index.map { |item, i| [item, i] } is defining an instance variable var1 on the actual module object ApplicationHelper, not on any instance.

In Ruby, when you only want to define a variable once, we use the ||= memoization trick:

module ApplicationHelper
  def method1
    @var1 ||= MyModel::CONST1.each_with_index.map { |item, i| [item, i] }
  end
end

This is a trick that leverages the fact that the first time you reference a variable it will be nil and nil is considered fasely in Ruby. So the above line is the shortcut for writing:

@var1 || @var1 = MyModel::CONST1.each_with_index.map { |item, i| [item, i] }

NOTE: Be aware that if @var1 could be the values nil or false this will re-evaluate it each time. However, in this case. Since you are using Enumerable#map you will get [], and not nil, if there are no items. So the usage will be as expected in this situation.

This has an issue with the edge case the first time you call method1 because you are using it to set a default value. If we instead use message passing instead of direct variable access, we can achieve what you are looking for:

module ApplicationHelper
  def method1(v1, v2, v3 = var1.first)
    #....
    # all further access to @var1 is done by passing the message var1
    tmp = var1.select{ ... }
  end

  def var1
    @var1 ||= MyModel::CONST1.each_with_index.map { |item, i| [item, i] }
  end
end

Upvotes: 2

Related Questions