How to pass all local variables when calling a method - ruby

I am wondering if there is an easy way to pass all local variables when calling a method, instead of passing them one by one as parameters. I want the following:
class Greeting
def hello
message = 'HELLO THERE!'
language = 'English'
say_greeting
end
def konnichiwa
message = 'KONNICHIWA!'
language = 'Japanese'
say_greeting
end
private def say_greeting
puts message
end
end
Greeting.new.hello
to show HELLO THERE!. But it returns an error: NameError: undefined local variable or method 'message'.
I tried local_variables to get all local variables as an Array of symbols. But I can't seem to access the actual variables because there seemed to be no Ruby method like local_variable_get.
Background of the problem
In my Rails application, I have a controller having three update methods for different view files. All of them behave exactly the same (that they all will update a resource, show error if any, etc). Their only main difference are
template to be rendered when unsuccessful
redirect url when successful
flash success message
I only have three variables, so it really is indeed easy to just pass them as parameters, but I am just wondering if there is an elegant solution to this.

Local variables are there to do precisely not what you are trying to do: encapsulate reference within a method definition. And instance variables are there to do precisely what you are trying to: sharing information between different method calls on a single object. Your use of local variable goes against that.
Use instance variables, not local variables.
If you insist on referencing local variables between methods, then here is a way:
class Greeting
def hello
message = 'HELLO THERE!'
language = 'English'
say_greeting(binding)
end
def konnichiwa
message = 'KONNICHIWA!'
language = 'Japanese'
say_greeting(binding)
end
private def say_greeting b
puts b.local_variable_get(:message)
end
end
You can't make it without passing any arguments, but you can just pass a single binding argument, and refer all local variables from there.

Make the message and language as instance variables.
Then you can access them inside the private method.
class Greeting
def hello
#message = 'HELLO THERE!'
#language = 'English'
say_greeting
end
def konnichiwa
#message = 'KONNICHIWA!'
#language = 'Japanese'
say_greeting
end
private
def say_greeting
puts #message
end
end
puts Greeting.new.hello

Use instance variables. In your method say_greeting make sure you call the method hello first, otherwise the value of #message will be nil.
def hello
#message = "Hello there"
end
def say_greeting
hello #method call
puts message
end

Related

Why can't a class method have the same name as a non-class method?

I'm learning ruby, and noticed that I cannot create a class method called puts:
class Printer
def initialize(text="")
#text = text
end
def puts
puts #text
end
end
The error is:
`puts': wrong number of arguments (given 1, expected 0)
My expectation was that I could use the code like this:
p = Printer.new("hello")
p.puts
It's not just because puts is a built-in method, though. For instance, this code also gives a syntax error:
def my_puts(text)
puts text
end
class Printer
def initialize(text="")
#text = text
end
def my_puts
my_puts #name
end
end
tldr; within the scope of the instance, the puts resolves to self.puts (which then resolves to the locally defined method, and not Kernel#puts). This method overriding is a form of shadowing.
Ruby has an 'implicit self' which is the basis for this behavior and is also how the bare puts is resolved - it comes from Kernel, which is mixed into every object.
The Kernel module is included by class Object, so its methods [like Kernel#puts] are available in every Ruby object. These methods are called without a receiver and thus can be called in functional form [such as puts, except when they are overridden].
To call the original same-named method here, the super keyword can be used. However, this doesn't work in the case where X#another_method calls X#puts with arguments when it expects to be calling Kernel#puts. To address that case, see Calling method in parent class from subclass methods in Ruby (either use an alias or instance_method on the appropriate type).
class X
def puts
super "hello!"
end
end
X.new.puts
P.S. The second example should trivially fail, as my_puts clearly does not take any parameters, without any confusion of there being another "puts". Also, it's not a syntax error as it occurs at run-time after any language parsing.
To add to the previous answer (https://stackoverflow.com/a/62268877/13708583), one way to solve this is to create an alias of the original puts which you use in your new puts method.
class Printer
alias_method :original_puts, :puts
attr_reader :text
def initialize(text="")
#text = text
end
def puts
original_puts text
end
end
Printer.new("Hello World").puts
You might be confused from other (static) programming languages in which you can overwrite a method by creating different signatures.
For instance, this will only create one puts method in Ruby (in Java you would have two puts methods (disclaimer: not a Java expert).
def puts(value)
end
def puts
end
If you want to have another method with the same name but accepting different parameters, you need to use optional method parameters like this:
def value(value = "default value")
end

Binding method to instance

Is there a way to bind an existing method to an existing instance of an object if both the method and the instance are passed as symbols into a method that does that if the instance is not a symbol?
For example:
def some_method
#do something
end
some_instance = Klass.new(something)
def method_that_binds(:some_method, to: :some_instance)
#how do I do that?
end
Your requirements are a little unusual, but it is possible to do this mostly as you say:
class Person; end
harry = Person.new
barry = Person.new
def test
puts 'It works!'
end
define_method :method_that_binds do |a_method, to|
eval(to[:to].to_s).singleton_class.send(:define_method, a_method, &Object.new.method(a_method))
end
method_that_binds :test, to: :harry
harry.test
# It works! will be sent to STDOUT
barry.test
# undefined method 'test'
This doesn't actually use a named parameter, but accepts a hash with a to key, but you can see you can call it in the way you want. It also assumes that the methods you are defining are defined globally on Object.
The API you want doesn't easily work, because you have to know from which scope you want to access the local variable. It's not quite clear to me why you want to pass the name of the local variable instead of passing the content of the local variable … after all, the local variable is present at the call site.
Anyway, if you pass in the scope in addition to the name, this can be accomplished rather easily:
def some_method(*args)
puts args
puts "I can access some_instance's ivar: ##private_instance_var"
end
class Foo; def initialize; #private_instance_var = :foo end end
some_instance = Foo.new
def method_that_binds(meth, to:, within:, with: [])
self.class.instance_method(meth).bind(within.local_variable_get(to)).(*with)
end
method_that_binds(:some_method, to: :some_instance, within: binding, with: ['arg1', 'arg2'])
# arg1
# arg2
# I can access some_instance's ivar: foo
As you can see, I also added a way to pass arguments to the method. Without that extension, it becomes even simpler:
def method_that_binds(meth, to:, within:)
self.class.instance_method(meth).bind(within.local_variable_get(to)).()
end
But you have to pass the scope (Binding) into the method.
If you'd like to add a method just to some_instance i.e. it's not available on other instances of Klass then this can be done using define_singleton_method (documentation here.)
some_instance.define_singleton_method(:some_method, method(:some_method))
Here the first use of the symbol :some_method is the name you'd like the method to have on some_instance and the second use as a parameter to method is creating a Method object from your existing method.
If you'd like to use the same name as the existing method you could wrap this in your own method like:
def add_method(obj, name)
obj.define_singleton_method(name, method(name))
end
Let's say we have a class A with a method a and a local variable c.
class A
def a; 10 end
end
c = '5'
And we want to add the method A#a to c.
This is how it can be done
c.singleton_class.send :define_method, :b, &A.new.method(:a)
p c.b # => 10
Explanations.
One way to add a method to an object instance and not to its class is to define it in its singleton class (which every ruby object has).
We can get the c's singleton class by calling the corresponding method c.signleton_class.
Next we need to dynamically define a method in its class and this can usually be accomplished by using the define_method which takes a method name as its first argument (in our case :b) and a block. Now, converting the method into a block might look a bit tricky but the idea is relatively simple: we first transform the method into a Method instance by calling the Object#method and then by putting the & before A.new.method(:a) we tell the interpreter to call the to_proc method on our object (as our returned object is an instance of the Method, the Method#to_proc will be called) and after that the returned proc will be translated into a block that the define_method expects as its second argument.

why do I need the # for setting variable value

I'm a little confused about scope of variables, in ruby I wrote a test program:
class Test
attr_reader :tester
def initialize(data)
#tester = data
end
def getData
tester
end
end
puts Test.new(11).getData
now this works fine, the attr_reader, but my confusion is that since I've define attr_reader :tester then why can't I go tester = data rather then #tester = data, because when retrieving the data in getData I only have to write tester and not #tester
Using attr_reader is equivalent to
class Test
def initialize(data)
#tester = data
end
# attr_reader defines this method for you
def tester
#tester
end
def getData
tester
end
end
In your getData method using tester is equivalent to self.tester. If you use #tester you access the variable directly. When you use tester you access the variable via the getter method.
attr_reader means that should read: "The corresponding instance variable getter and setter
methods will be created for you." so that first we get data and then we set that data.
some_name = syntax without the explicit receiver is interpreted as a local variable assignment. In order to do an assignment to an instance variable, you have to explicitly set the receiver even if it is self. In this case, self.tester =.

question about parameter passing in Ruby

Comparing the following two code snippets:
class Logger
def self.add_logging(id_string)
define_method(:log) do |msg|
now = Time.now.strftime("%H:%M:%S")
STDERR.puts "#{now}-#{id_string}: #{self} (#{msg})"
end
end
end
class Song < Logger
add_logging "Tune"
end
song = Song.new
song.log("rock on")
class Logger
def self.add_logging(id_string)
def log(msg)
now = Time.now.strftime("%m")
puts "#{now}-#{id_string}: #{self}(#{msg})"
end
end
end
class Song < Logger
add_logging "Tune"
end
s = Song.new
s.log("can't smile with you")
#=> NameError: undefined local variable or method `id_string' for #<Song:0x000001018aad70>
I can't figure out why the second case gets the NameError error, and why the id_string can't be passed to it.
A def creates a new scope; a block does not. A new scope cuts off the visibility of the surrounding variables. ruby has two other 'new scope creators': class and module.
x = 10
3.times do |i|
puts i * x
end
def do_stuff
puts x * 10
end
do_stuff
--output:--
0
10
20
`do_stuff': undefined local variable or method `x'
id_string is local to method add_logging. In your latter implementation, log-method can not see it, hence the error. In the former implementation, you dynamically define log-method within add_logging.
In other words, local variable is visible within the scope it is defined in (in this case, a method). In that latter implementation, you have nested scopes (=a method declaration within a method), and inner scope can not access variables that are local to outer scope.
As suggested in answer by #stef, you might get around this my widening the scope of the variable. I would recommend keeping variable scopes as 'tight' as possible, and therefore prefer your first implementation.
Try this with a class variable?
class Logger
def self.add_logging(id_string)
##my_id = id_string
define_method(:log) do |msg|
now = Time.now.strftime("%H:%M:%S")
STDERR.puts "#{now}-#{##my_id}: #{self} (#{msg})"
end
end
end
Class variables should be avoided in ruby due to their problematic nature. The ruby way is to use 'class instance variables' instead.

Accessing variables using overloading brackets [] in Ruby

Hi i want to do the following. I simply want to overload the [] method in order to access the instance variables... I know, it doesn't make great sense at all, but i want to do this for some strange reason :P
It will be something like this...
class Wata
attr_accessor :nombre, :edad
def initialize(n,e)
#nombre = n
#edad = e
end
def [](iv)
self.iv
end
end
juan = Wata.new('juan',123)
puts juan['nombre']
But this throw the following error:
overload.rb:11:in `[]': undefined method 'iv' for # (NoMethodError)
How can i do that?
EDIT
I have found also this solution:
def [](iv)
eval("self."+iv)
end
Variables and messages live in a different namespace. In order to send the variable as a message, you'd need to define it as either:
def [](iv)
send iv
end
(if you want to get it through an accessor)
or
def [](iv)
instance_variable_get "##{iv}"
end
(if you want to access the ivar directly)
try instance_variable_get instead:
def [](iv)
instance_variable_get("##{iv}")
end

Resources