2

Take this class as an example:

 class MyClass
  attr_accessor :values, :uniq_values

  def initialize(value)
    self.uniq_values = ['default_value']
    self.values = ['default_value']
    copy_value(value)
    add_value(value)
  end

  def copy_value(value)
    uniq_values |= [value]
  end

  def add_value(value)
    values << value
  end

  def run
    puts "uniq_values: #{uniq_values}"
    puts "values: #{values}"
  end
end

obj = MyClass.new('poop')
obj.run

# Expect 'uniq_values' and 'values' to be the same
# OUTPUT:
#  uniq_values: ["default_value"]
#  values: ["default_value", "poop"]

I can get the desired output by using self.uniq_values |= [value], however I would expect that it would be necessary with the << operator as well. Can anyone explain the difference?

Jason Denney
  • 3,191
  • 1
  • 18
  • 14

2 Answers2

4

It's different.

values << value is method calling, which calls the method of :<< of Array.

While uniq_values |= value is just the short cut of uniq_values = uniq_values | value, here uniq_values will be parsed as the local variable.

Per the documentation:

"The local variable is created when the parser encounters the assignment, not when the assignment occurs"

and

"When using method assignment you must always have a receiver. If you do not have a receiver Ruby assumes you are assigning to a local variable"

Jason Denney
  • 3,191
  • 1
  • 18
  • 14
xdazz
  • 158,678
  • 38
  • 247
  • 274
  • ...that doesn't exist so will have to be set. `|` with a non-false will produce `true` to be written, nothing about arrays or strings. Totally unexpected. – D-side Sep 09 '14 at 10:25
  • Thanks. I'm still confused as doing a `puts` of uniq_values before the assignment gives `['default_value']`, which would indicate it's not getting parsed as a local variable? However, doing a `puts` of uniq_values after the assignment gives `true`, which would mean it is, given D-side's comment. – Jason Denney Sep 09 '14 at 11:16
  • @JasonDenney the assignment creates a new local variable `uniq_values`, shadowing the getter method with the same name. `uniq_values` refers to your method before the assignment and to the local variable afterwards. – Stefan Sep 09 '14 at 11:43
  • Thanks @stefan. It's definitely shadowing, I think my question is why a new local variable gets created when assigning, say for a more direct comparison if I did `values = values << value` vs the getter method actually getting called with `values << value`. – Jason Denney Sep 09 '14 at 15:27
  • @JasonDenney `Array#<<` modifies and returns the receiver (i.e. the very same array object) whereas `Array#|` returns a new array. – Stefan Sep 09 '14 at 15:34
  • @Stefan Thanks for your patience. My question has been refined to why a new local variable gets created. `values << value` modifies @values, however `values = values << value` attempts to modify a new local variable 'values' which is nil and gives `undefined method <<' for nil:NilClass`. What's weird to me is that it's not just because it's in an assignment, I can do `foo = values << value` and @values gets modified. It's some special case when a variable is assigned to itself. – Jason Denney Sep 09 '14 at 16:04
  • 1
    @JasonDenney that can be quite confusing indeed, Ruby handles assignments at the parser stage. From the [documentation](http://www.ruby-doc.org/core-2.1.2/doc/syntax/assignment_rdoc.html#label-Local+Variables+and+Methods): *"The local variable is created when the parser encounters the assignment, not when the assignment occurs"*. You'd have to use `values = self.values << value` or `values = values() << value` to force a method call. – Stefan Sep 09 '14 at 16:20
1

In your code uniq_values (local variable) and self.uniq_values (instance variable) are not the same thing.

Access your attr-stuff as instance variable with @.

class MyClass
  attr_accessor :values, :uniq_values

  def initialize(value)
    @uniq_values = ['default_value']
    @values = ['default_value']
    copy_value(value)
    add_value(value)
 end

  def copy_value(value)
    @uniq_values |= value # crash
    # or |= [value] # embed value in array
    # or |= Hash.new value # don't embed Arrays
    # or def copy_value(*values) # splat
    uniq_values = "poor me, here I am forgotten with the next 'end' keyword"
    # uniq_values != @uniq_values, but
    # self.uniq_values == @uniq_values
  end

  def add_value(value)
    @values << value
  end

  def run
    puts "uniq_values: #{@uniq_values}"
    puts "values: #{@values}"
  end
end

obj = MyClass.new('poop')
obj.run
# Crash!

Now, it crashes. |= is not the per se the array union operator ( Ruby |= assignment operator ) and for it to work, you have to convert value to an array first (or embed it in). There is nice ruby syntax for it i think (search for splat operator and edit my answer :) ).

Anyway the take-home message for you is to use the @, especially in combination with attr_accessor and the like .

edit

While my code fixes the surface issue, I somewhat missed the point of this question. (Why is uniq_values a local variable, while values is not), which has been explained in the other answer and comments. Lovely find!

Community
  • 1
  • 1
Felix
  • 4,510
  • 2
  • 31
  • 46
  • Yes, I meant to embed it in an array, that was a typo, thanks! Work colleagues have argued against using the @ syntax so that way the getter/setter methods can be overridden. – Jason Denney Sep 09 '14 at 14:36
  • thats a weird argument, afaiu you could still override it (and would likely still produce pretty weird and difficult to maintain code) - but then I do not know your scenario (smells a bit) and it was not the original question anyway. So, use the self to reference the instance variable (scoping), otherwise you create a new local variable uniq_values. – Felix Sep 09 '14 at 15:08
  • 1
    Holy crap, I totally missed the point of the question, sorry. Its a great find. Anyway, it results from the fear of not being able to override getters and setters :) . I for one would stick with the `@`s. I find it makes the code more comprehensible, too. – Felix Sep 09 '14 at 15:14