how to convert "+=" to operator in ruby - ruby

I can convert "+" , "-" or "/" to operator use 2.send("-",3)
but it does not work with "+="
a = 2
a += 2 #=> 4
a = 2
a.send("+=", 4) #=> NoMethodError: undefined method `+=' for 2:Fixnum
I have tried to convert symbol first ; but not works too;
how to fix this?

2.send("-", 3) works, because - is a method and 2 responds to that method:
2.respond_to?('-') #=> true
= and += on the other hand are not methods:
2.respond_to?('=') #=> false
2.respond_to?('+=') #=> false
And even if = was a valid method1, then
a = 2
a.send("=", 4)
would be equivalent to:
2.send("=", 4)
or simply:
2 = 4
In other words: it would redefine 2 as 4, something Ruby doesn't allow you to do.
This is because variables (like a above) are not objects. a.send doesn't send a message to the variable a, but to the object, a is referring to (2 in the example).
The same applies to abbreviated assignment like +=:
a = 2
a += 2
is equivalent to:
a = 2
a = a + 2
You can just rewrite it as:
a = 2
a = a.send("+", 2)
The assignment is not part of the method invocation.
1 you can actually define a method = via define_method:
class Foo
define_method('=') { |other| puts "`=' called with #{other}" }
end
But it is just an ordinary method. In particular, it does not and can not alter the variable the object was assigned-to or the object's identity:
f = Foo.new
#=> #<Foo:0x007ff20c0eeda8>
f.send('=', 123)
# `=' called with 123
#=> nil
f
#=> #<Foo:0x007ff20c0eeda8>

Related

How do I call a ruby function named []?

I am new to Ruby, so please excuse this question if it is obvious.
I am working with a Module with a function signature that I don't understand. How would I call this function?
module Facter
...
def self.[](name)
collection.fact(name)
end
...
In my code I want to reference something that should be in collection.fact, in this Facter module. What syntax to I use to call this function?
Cheers
It works like this:
class MyModule
def self.[](arg)
puts arg
end
end
MyModule["Hello world"] # will print Hello world
Please see official docs:
https://ruby-doc.org/core/doc/syntax/methods_rdoc.html
Additionally, methods for element reference and assignment may be defined: [] and []= respectively. Both can take one or more arguments, and element reference can take none.
class C
def [](a, b)
puts a + b
end
def []=(a, b, c)
puts a * b + c
end
end
obj = C.new
obj[2, 3] # prints "5"
obj[2, 3] = 4 # prints "10"
So about example from docs
# From docs
obj[2, 3]
# It's the same as
obj.[](2, 3)
More interesting example
# From docs
obj[2, 3] = 4
# will print 10
# => 4
# It's the almost as
obj.[]=(2, 3, 4)
# will print 10
# => nil
As you see when you call as obj[2, 3] = 4 Ruby takes the value after = as the last argument of the []= method and return it as method result
And regardless of whether there is return in the method body. For example
class C
def []=(a, b, c)
puts "Before return"
return 12
puts "After return"
end
end
obj = C.new
obj[2, 3] = 4
# will print Before return
# => 4
obj.[]=(2, 3, 4)
# will print Before return
# => 12
It is desirable to define such method with more than one parameter. Technically, you can have only one, but the call will be like this obj[] = 1

Ruby "#<NoMethodError: undefined method `+' for nil:NilClass>"

I have the following code which looks for the letters "u"and "e" in a word and add 1 to the index so that it shows position in the word starting from 1 rather than 0, and then have it combined into an array.
def vowel_indices(word)
x = (word.index("u") + 1)
y = (word.index("e") + 1)
print = [x,y]
end
I am getting the following error message when running:
#<NoMethodError: undefined method `+' for nil:NilClass>
What is nil in this? From what I can see my variables are assigned correctly.
"What is nil in this?"
As # Cary Swoveland and #Lahiru already said, if a word is passed in that doesn't have a 'u' or 'e' that exception will be raised:
001 > def vowel_indices(word)
002?> x = (word.index("u") + 1) #word.index("u") returns nil if the word arg passed in doesn't have a 'u'
003?> y = (word.index("e") + 1)
004?> print = [x,y] #I've never seen the 'print =' syntax...any reason you're not just returning [x,y] here?
005?> end
=> :vowel_indices
006 > vowel_indices("cruel")
=> [3, 4]
007 > vowel_indices("cat")
NoMethodError: undefined method `+' for nil:NilClass
from (irb):2:in `vowel_indices' # This tells you exactly where the exception is coming from
from (irb):7
from /Users/kenenitz/.rvm/rubies/ruby-2.2.0/bin/irb:11:in `<main>'
Two quick & dirty ways to handle this would be to either add a conditional to check for the presence of each letter, or you can rescue NoMethodError:
#conditional check (preferred as it is more precise)
def vowel_indices(word)
u_index = word.index("u")
e_index = word.index("e")
x = u_index ? u_index + 1 : nil #verify that u_index is not nil before calling :+ method
y = e_index ? e_index + 1 : nil # same for e_index
[x,y]
end
#rescuing NoMethodError, not preferred in this case but including as a possible solution just so that you're familiar w/ this approach
def vowel_indices(word)
begin
x = word.index("u") + 1
y = word.index("e") + 1
rescue NoMethodError
x = nil
y = nil
end
[x,y]
end
With either solution I provide, if a word is missing a 'u' or 'e', the return value would contain nil which would most likely require some sort of special handling elsewhere in your program:
vowel_indices("cat")
=> [nil, nil]
vowel_indices("cut")
=> [2, nil]
It's because for some values assigned as word may not contain a u or e.
If you are using Rails, then you can overcome this by modifying your code like this:
def vowel_indices(word)
x = (word.try(:index, "u").try(:+, 1)
y = (word.try(:index, "e").try(:+, 1)
print = [x,y]
end
In this way, you can prevent the method trying to call :+ method for if the class is Nil.
In instances which we are not sure about the input, it's better to use try.
word = "Hello" # example
def vowel_indices(word)
x = (word.index("u") + 1) # x = (nil + 1) *There is no "u" in "Hello"
y = (word.index("e") + 1) # y = (1 + 1) *Output as expected based on index
print = [x,y]
end
p word.index("u") # will return nil if you need to check the return value

Is there a way to implicitly evaluate variables in ruby?

In PHP, I can do this:
$a = 1;
$c = 'a';
$$c = 2;
//now $a == 2
Is there any equivalent in ruby? By which I mean, any simple way to have it dereference a variable during execution like this? I'd rather not use eval, because it looks messy - I've already determined that eval can't be called as a method of a string.
It is possible but it's a bit more complicated., and you actually have two possibilities:
Kernel#local_variables
Returns the names of the current local variables.
fred = 1
for i in 1..10
# ...
end
local_variables #=> [:fred, :i]
Binding#local_variable_get/set
Returns a value of local variable symbol.
def foo
a = 1
binding.local_variable_get(:a) #=> 1
binding.local_variable_get(:b) #=> NameError
end
This method is short version of the following code.
binding.eval("#{symbol}")
if you just need this you can do
a = 1
c = 'a'
eval("#{c} = 2")
a == 2 # => true
... but this is moron way to do this
if you need this for instance variables
class Foo
attr_reader :a
def initialize
#a = 1
end
end
foo = Foo.new
foo.instance_variable_get(:a) #=> 1
foo.a #=> 1
foo.instance_variable_set(:"#a", 2)
foo.a #=> 2
...you can also eval instance like this:
# ...
foo.instance_eval do
#a = 'b'
end
foo.a # => 'b'

Why isn't a variable defined by attr_accessor available in a method?

class A
attr_accessor :rank
def change_rank
rank = rank + 1
end
end
a = A.new
a.rank = 5
p a.rank
a.change_rank
p a.rank
produces an error for rank = rank + 1 (undefined method + for nil:Nilclass). Shouldn't an implicit call to the "rank" method return the value of the instance variable #rank? For some reason, this code works if I change the 4th line to:
self.rank = self.rank + 1
Why does an explicit call to the rank method works while an implicit one doesn't?
def change_rank
rank = rank + 1
end
In this context name rank does not resolve to one of methods generated by attr_accessor. It is seen by ruby roughly as this:
def change_rank
rank = nil
rank = rank + 1
end
When ruby sees an assignment to a "naked" name, it creates a local variable with this name. It will shadow the outer method. And since local vars are initialized with nil, it explains the error you're getting.
You can do this to avoid the error (be explicit about what you're modifying):
def change_rank
self.rank += 1
end
Update:
Here's more code that illustrates this
defined?(x) # => nil # name x is unidentified yet
defined?(x = x) # => "assignment" # local var x is created by assignment
defined?(x) # => "local-variable" # now local var x exists
x # => nil # and its value is nil
I rewrite your code as following. You can see how Ruby treats rank= and rank with different ways.
class A
attr_accessor :rank
def change_rank
self.rank = rank + 1
end
end
a = A.new
a.rank = 5
p a.rank
a.change_rank
p a.rank
execution result:
5
6

Is it possible to have class.property = x return something other than x?

Let's say I have a Ruby class:
class MyClass
def self.property
return "someVal"
end
def self.property=(newVal)
# do something to set "property"
success = true
return success # success is a boolean
end
end
If I try and do MyClass.property=x, the return value of the whole statement is always x. It is a convention in a lot of C-based/inspired languages to return a boolean "success" value - is it possible to do this for a setter using the "equals syntax" in Ruby?
Furthermore - if this isn't possible, why not? Is there any conceivable downside to allowing an "equals setter" operation return a value?
One downside is that you would break the chained assignment semantics:
$ irb
irb(main):001:0> x = y = 3
=> 3
irb(main):002:0> p x
3
=> nil
irb(main):003:0> p y
3
=> nil
irb(main):004:0>
Consider:
x = MyClass.property = 3
Then x would take true if this worked as you had expected (right-associativity). That could be a surprise for people using your interface and used to the typical semantics.
You also got me thinking about parallel assignment, eg:
x, y = 1, 2
Apparently the return value from that expression is implementation specific... I guess I won't be chaining parallel assignments :)
Nice question!
Like Martin says, this would break assignment chaining.
The way ruby assignment methods are defined to work expands MyClass.property = 3 to the equivalent of (lambda { |v| MyClass.send('property=', v); v })[3] (not really, but this shows how chaining works). The return value of the assignment is always the value assigned.
If you want to see the result of your MyClass#property= method, then use #send:
irb> o = Object.new
=> #<Object:0x15270>
irb> def o.x=(y)
irb> #x = y+1
irb> puts "y = #{y}, #x = ##x"
irb> true
irb> end
=> nil
irb> def o.x
irb> puts "#x = ##x"
irb> #x
irb> end
=> nil
irb> o.x = 4
y = 4, #x = 5
=> 4
irb> o.x
#x = 5
=> 5
irb> o.send('x=', 3)
y = 3, #x = 4
=> true
However, the ruby way to do this is with exceptions - if something goes wrong during
the assignment, raise an exception. Then all invokers must handle it if something goes
wrong, unlike a return value, which can be easily ignored:
# continued from above...
irb> def o.x=(y)
irb> unless y.respond_to? :> and (y > 0 rescue false)
irb> raise ArgumentError, 'new value must be > 0', caller
irb> end
irb> #x = y + 1
irb> puts "y = #{y}, #x = ##x"
irb> end
=> nil
irb> o.x = 4
y = 4, #x = 5
=> 4
irb> o.x = 0
ArgumentError: new value must be > 0
from (irb):12
from :0
irb> o.x = "3"
ArgumentError: new value must be > 0
from (irb):13
from :0
irb> o.x
#x = 5
=> 5
I'm not a Ruby expert but I'd say no for that case I'm afraid. A property setter is solely there to set the value of a private field, not to have any side effects like returning result codes.
If you want that functionality then forget the setter and write a new method called TrySetProperty or something which tries to set the property and returns a boolean.

Resources