Why do increment_v1 and_v3 work, but increment_v2 and _v4 don't (v2 returns the correct value, but doesn't change the #counter, v4 fails with "NoMethodError (undefined method `+' for nil:NilClass)")
class MyClass
attr_accessor :counter
def initialize
#counter = 0
end
def increment_v1
#counter = counter + 1
end
def increment_v2
counter = #counter + 1
end
def increment_v3
#counter += 1
end
def increment_v4
counter += 1
end
end
I expect all of these methods to have the same outcome (increase the #counter value and return the increased number). It has the same error if I replace attr_accessor with attr_reader and attr_writer. I feel like I may be misunderstanding something about the attr_* methods.
Here is what it looks like in the console:
2.6.3 :026 > a = MyClass.new
=> #<MyClass:0x00000000018d7240 #counter=0>
2.6.3 :027 > a.increment_v1
=> 1
2.6.3 :028 > a
=> #<MyClass:0x00000000018d7240 #counter=1>
2.6.3 :029 > a.increment_v2
=> 2
2.6.3 :030 > a
=> #<MyClass:0x00000000018d7240 #counter=1>
2.6.3 :031 > a.increment_v3
=> 2
2.6.3 :032 > a
=> #<MyClass:0x00000000018d7240 #counter=2>
2.6.3 :033 > a.increment_v4
Traceback (most recent call last):
5: from /home/guin/.rvm/rubies/ruby-2.6.3/bin/irb:23:in `<main>'
4: from /home/guin/.rvm/rubies/ruby-2.6.3/bin/irb:23:in `load'
3: from /home/guin/.rvm/rubies/ruby-2.6.3/lib/ruby/gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
2: from (irb):33
1: from (irb):23:in `increment_v4'
NoMethodError (undefined method `+' for nil:NilClass)
Running a.counter += 1 from outside the class works as I expect. Do I have to specify self.counter += 1 when I am inside the class? Why? It even works if with self.counter = counter + 1. What is going on?
As you have shown, you can always access an attribute directly using the instance variable (#counter). However your issue here is relating to the getter/setter methods generated by attr_accessor.
The getter method does not require self unless you have a local variable with the same name. Setter methods are different. You always need to use self with setters.
For example:
def test_method
# directly set instance var. this will always work
#counter = 1
# define local variable with same name.
# this does not call the setter because you don't use self
counter = 0
puts counter
# prints 0
# The getter method is never called because you have a local variable
# with the same name.
puts self.counter
# prints 1
# you can force the getter to be called by using self
end
I think the idiomatic way to write your method would be:
def increment_v5
self.counter += 1
end
However you could also write it like this:
def increment_v6
self.counter = counter + 1
# \ calls getter
end
and there are many other ways to write it.
Related
Why can't I apply an operator to an instance variable using it's attr_accessor or attr_writer?. If I try to run this code:
class Numbers
attr_accessor :number1
def initialize
#number1 = 15
end
def subtract
number1 -= 1
p number1
end
end
num = Numbers.new
num.subtract
I get this error:
Traceback (most recent call last):
1: from ex.rb:15:in `<main>'
ex.rb:9:in `subtract': undefined method `-' for nil:NilClass (NoMethodError)
My question, how could I subtract a number from #number1 without having to explicitly putting the #, just calling the attribute.
It is not an instance variable here it is a local one:
def subtract
number1 -= 1
p number1
end
To call an instance variable without # use self.number1.
def subtract
self.number1 -= 1
p number1
end
I've redone the question and included the full code for both files. In the touch_in method I am trying to instantiate a Journey class in the variable called 'journey'.
require_relative 'journey'
class Oystercard
MAXIMUM_BALANCE = 90
MINIMUM_BALANCE = 1
MINIMUM_CHARGE = 1
def initialize
#balance = 0
#journeys = {}
end
def top_up(amount)
fail 'Maximum balance of #{maximum_balance} exceeded' if amount + balance > MAXIMUM_BALANCE
#balance += amount
end
def in_journey?
#in_journey
end
def touch_out(station)
deduct(MINIMUM_CHARGE)
#exit_station = station
#in_journey = false
#journeys.merge!(entry_station => exit_station)
end
def touch_in(station)
fail "Insufficient balance to touch in" if balance < MINIMUM_BALANCE
journey = Journey.new
#in_journey = true
#entry_station = station
end
attr_reader :journeys
attr_reader :balance
attr_reader :entry_station
attr_reader :exit_station
private
def deduct(amount)
#balance -= amount
end
end
The Journey file is as follows:
class Journey
PENALTY_FARE = 6
MINIMUM_CHARGE = 1
def initialize(station = "No entry station")
#previous_journeys = {}
end
def active?
#active
end
def begin(station = "No entry station")
#active = true
#fare = PENALTY_FARE
#entry_station = station
end
def finish(station = "No exit station")
#active = false
#fare = MINIMUM_CHARGE
#exit_station = station
#previous_journeys.merge!(entry_station => exit_station)
end
attr_reader :fare
attr_reader :previous_journeys
attr_reader :entry_station
attr_reader :exit_station
end
I think that the 'touch_in' method should create a 'journey' variable that I called methods on, such as 'finish(station)' or 'active?' etc. When I attempt to do this in IRB I am given the following error:
2.6.3 :007 > journey
Traceback (most recent call last):
4: from /Users/jamesmac/.rvm/rubies/ruby-2.6.3/bin/irb:23:in `<main>'
3: from /Users/jamesmac/.rvm/rubies/ruby-2.6.3/bin/irb:23:in `load'
2: from /Users/jamesmac/.rvm/rubies/ruby-2.6.3/lib/ruby/gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
1: from (irb):7
NameError (undefined local variable or method `journey' for main:Object)
I'm aware that much of the code above is sloppily written and there are probably other bits, beside the 'journey' issue, where it's just incorrect. Please let me know if this is the case, the more I'm told the better.
Apologies to anyone who attempted to help me on my first attempt, as I say I'm still getting used to SO and was trying to make the post easier to read.
class Journey
# ...
def initialize
puts "Journey initialized"
# ...
end
# ...
end
require_relative 'journey'
class Oystercard
def initialize
end
# ...
def touch_in(station)
journey = Journey.new
# ...
end
end
Oystercard.new.touch_in("station")
stack_question$ ruby oystercard.rb
Journey initialized
It works fine - are you having some issue with this that is beyond the scope of the question?
I was playing with the local,class variable and instance variable creation inside the class block as below. But I found something which I failed to explain myself. My confusion has been posted between the two codes below.
class Foo
def self.show
##X = 10 if true
p "hi",##X.object_id,x.object_id
end
end
#=> nil
Foo.show
#NameError: undefined local variable or method `x' for Foo:Class
# from (irb):4:in `show'
# from (irb):7
# from C:/Ruby193/bin/irb:12:in `<main>'
The above erros is expected. But in the below code I have assigned the class variable ##X to 10. But in the p statement I used instance variable #X.Why did the error not throw up like the above code ?
class Foo
def self.show
##X = 10 if true
p "hi",#X.object_id
end
end
#=> nil
Foo.show
"hi"
4
#=> ["hi", 4]
Because of everything is object and no explicit variable declaration is required in Ruby, you code
p #X.object_id
silently introduces an instance variable #X (#X.nil? == true). You can see this magic in irb:
~ irb
> p #x.object_id
# 8
# ⇒ 8
p parent.class #=> NilClass # ok.
p !!parent # => false # as expected.
p parent.object_id # => 17006820 # should be 4
p parent && parent.foo # => NoMethodError foo # should be nil-guarded
Where does this object come from?
Possibly something like this:
class BlankSlate
instance_methods.each do |m|
# Undefine all but a few methods. Various implementations leave different
# methods behind.
undef_method(m) unless m.to_s == "object_id"
end
end
class Foo < BlankSlate
def method_missing(*args)
delegate.send(*args)
end
def delegate
# This probably contains an error and returns nil accidentally.
nil
end
end
parent = Foo.new
p parent.class
#=> NilClass
p !!parent
#=> false
p parent.object_id
#=> 2157246780
p parent && parent.foo
#=> NoMethodError: undefined method `foo' for nil:NilClass
Creating BlankSlate or BasicObject is a common pattern (before it was added to core Ruby as of version 1.9). It serves to create objects that will do something special with any method they are sent, or heavily delegate their behaviour to a different class. The downside is that it may introduce strange behaviour like this.
isn't + an operator? why would it not be defined?
here's my code:
Class Song
##plays = 0
def initialize(name, artist, duration)
#name = name
#artist = artist
#duration = duration
#plays = 0
end
attr_reader :name, :artist, :duration,
attr_writer :name, :aritist, :duration
def play
#plays += 1
##plays += 1
"This Song: ##plays play(s). Total ###plays plays."
end
def to_s
"Song: ##name--##artist (##duration)"
end
end
First, this code doesn't even run: class on Line 1 needs to be spelled with a lowercase c, and you can't have a comma after the last item in a statement (your attr_reader line). I don't get a NoMethodError after fixing those and running Song.new or Song#play or Song#to_s.
Anyway, you will always get that NoMethodError when you try adding anything to a nil value:
>> nil + 1
NoMethodError: undefined method `+' for nil:NilClass
from (irb):1
>> nil + nil
NoMethodError: undefined method `+' for nil:NilClass
from (irb):2
>> # #foo is not defined, so it will default to nil
?> #foo + 2
NoMethodError: undefined method `+' for nil:NilClass
from (irb):4
So you might be trying to add something to an uninitialized instance variable... or it could be anything. You always need to post full, minimal code to duplicate an error if you want to be helped properly.
+ is defined on numbers (among other things). However, as the error message says, it is not defined on nil. This means you can't do nil + something and why would you?
That being said, you're actually not calling nil + something anywhere in the code you've shown (you're initializing both #plays and ##plays to 0, and you're not setting them to nil at any point). And as a matter of fact your code runs just fine once you remove the two syntax error (Class should be class and there should be no comma after :duration). So the error is not in the code you've shown.
maybe you should include ##plays = 0 in your initialize method?