Reputation: 33715
I have this code:
def login(user,pass)
end
class Bob
def login(pass)
login('bob',pass) #ERROR#
end
end
login('hello','world')
bob = Bob.new
bob.login('world')
When I try to execute the code from the command line, I get an wrong number of Arguments error on the line I commented as #ERROR#
. I'm guessing this is because I'm not successfully accessing the global login()
function instead? How do I reference it?
Upvotes: 0
Views: 1763
Reputation: 110725
Let's look at:
def login(user, pass)
puts "#{user}'s #{pass}"
end
class Bob
def login(pass)
greeting
login('Bob',pass) #ERROR#
end
def greeting
puts "hi"
end
end
When we run:
bob = Bob.new
bob.login('world')
we get:
hi
ArgumentError: wrong number of arguments (2 for 1)
and you know why the exception was raised.
We execute methods by sending them, together with any arguments, to a receiver. Initially, we send the method login
with argument 'world'
to the receiver bob
. But wait, in login
, no receiver is specified. Receivers are either explicit (e.g., outside the class, bob.greeting
) or unspecified, in which case they are assumed to be self
. Here self
is bob
, so greeting
in the method login
is equivalent to self.greeting
within the method or to bob.greeting
outside the class.
After greeting
is executed by login
, we want to execute the method login
that is outside the class. We therefore must use an explicit receiver. But what is it's class? (We know it has one!) After loading this code, try this in IRB:
method(:login).owner #=> Object
We ran this at the "top-level" where:
self #=> main
self.class #=> Object
It therefore can be invoked anywhere in our program. The only complication is when we are in a class that has an instance method of the same name.
OK, so login
outside of class Bob
is a method of class Object
. Is it a class method or an instance method?
Object.methods.include?(:login) #=> false
Object.instance_methods.include?(:login) #=> false
Neither! Hmmm. Then it must be a private method:
Object.private_methods.include?(:login) #=> true
Object.private_instance_methods.include?(:login) #=> true
Yes, in fact, it's both a private class method and a private instance method (of the class Object
). That's a bit confusing, but the answer as to why it is both and why it is private lies with Ruby's object model, and that cannot be explained in a few words, so that must wait for another day.
We can use the method Object#send
to invoke private methods, so that's what we will do. Let's use the private class method, so the receiver will be Object
:
def login(user,pass)
puts "#{user}'s #{pass}"
end
class Bob
def login(pass)
greeting
Object.send(:login, "Bob", pass)
end
def greeting
puts "hi"
end
end
bob = Bob.new
bob.login('world')
# hi
# Bob's world
Hurray!
Extra credit: Since login
is both a (private) class method and instance method, we should be able to insert new
in the operative line:
Object.new.send(:login, "Bob", pass)
and get the same result. Do we? I'll let you find out if you are interested.
Upvotes: 1
Reputation: 48609
I'm really new to ruby, so this is an entry level question.
The short answer is: the login() instance method in class Bob hides the toplevel login() method. The easy solution is: change the name of one of the methods.
Here are some things you should try to learn:
1) In ruby, every method is called with an object on the left hand side, e.g.
some_obj.login
The object on the left hand side is called the receiver.
2) If you don't explicitly specify a receiver, e.g.
login('bob',pass) #No receiver is specified on the left hand side
...ruby uses a variable called self on the left hand side, e.g.:
self.login('bob', pass)
3) Inside a method defined inside a class, e.g.:
class Bob
def login(pass)
#IN HERE
end
end
...self is equal to the object that called the method. In your case, you have this code:
bob = Bob.new
bob.login('world')
So bob is the object that is calling the login() instance method, and therefore you have this:
class Bob
def login(pass)
#IN HERE, self is equal to bob
end
end
Therefore, ruby does this:
class Bob
def login(pass)
#login('bob', pass) =>This line gets converted to this:
self.login('bob',pass) #ERROR#
#IN HERE, self is equal to bob
#So ruby executes this:
#bob.login('bob', pass) #ERROR: too many arguments#
end
end
One solution to your problem, like Guilherme Carlos suggested, is to use a module--but you can do that in a simpler way:
module MyAuthenticationMethods
def login(user, pass)
puts "user: #{user}, pass: #{pass}"
end
end
class Bob
def login(pass)
MyAuthenticationMethods::login('bob',pass)
end
end
However, generally you put a module in its own file and then require
it. The reason a module solves your problem is because a module name starts with a capital letter, which means it's a constant--and you can access a constant from anywhere in your code.
4) All def's attach themselves to the current class. The current class is determined by the value of the self variable: if self is a class, then the current class is just the value of self, but when self isn't a class, then the current class is self's class. Okay, let's see those principles in action:
class Bob
puts self #=>Bob
def login(pass)
...
end
end
Because self is a class, the current class is equal to self, and the def attaches itself to the Bob class.
What happens at the top level?
puts self #=> main
def login(user,pass)
end
Experienced rubyists are familiar with main
; it is the object that ruby assigns to self at the top level, i.e. outside any class or method definitions--what you are calling global. The important point is that main
is not a class. As a result, the top level login() def attaches itself to main's class, which is:
puts self #=>main
puts self.class #=>Object
def login(user,pass)
end
Brian Driscoll mentioned that ruby doesn't have a global scope--but that doesn't realy matter anyway because a def creates a new scope which closes off the outer scope, so nothing that exists outside the def is visible inside the def (except constants).
What you are trying to do is often done in ruby with what are called blocks. Blocks allow you to pass a second method to the first method, and then inside the first method you can call the second method. Here is an example:
class Bob
def login(pass)
yield('bob', pass) #yield calls the block with the specified arguments
end
end
bob = Bob.new
bob.login('my password') do |username, pword|
puts username, pword
end
The block in that code is this part:
do |username, pword|
puts username, pword
end
...which looks sort of like a method definition--but without a name. That is the standin for your top level login() method. Ruby automatically passes the block to the method specified before the block:
This method!
|
V
bob.login('my password')
And inside the login() method, you call the block using the word yield
--it's as if yield
were the name of the method.
Note that it is actually ruby's sytnax, i.e. writing a block after a method call, that causes a second method to be passed to the first method, and in the first method you can call the passed in method by simply writing yield(arg1, arg2, etc.)
.
Upvotes: 0
Reputation: 2381
You can use super
for this. Methods defined at the top level magically become private methods on all Objects.
class Bob
def login(pass)
super('Bob', pass)
end
end
Upvotes: 1
Reputation: 1103
In Ruby we have an hierarchy where class methods are called first than global scope methods.
Those global-scoped methods belongs to Object
class but they are declared as private.
To access them directly you can use send method, but it's not recommended
Object.send(:login, param1, param2)
A better way to solve this problem is using Modules.
Create a Module:
login.rb
module Login
def login(user, pass)
end
end
And include it in you class:
bob.rb
require 'login'
include Login
class Bob
login(pass)
Login::login('bob', pass)
end
end
bob = Bob.new
bob.login('test')
Upvotes: 0
Reputation: 16012
There maybe a better way if you can explain what exactly you're trying to do. But, if you must to do it anyway then:
def login(user, pass)
puts 'global login'
puts "user: #{user}, pass: #{pass}"
end
class Bob
def login(pass)
self.class.send(:login, 'bob',pass) #ERROR#
end
end
login('hello','world')
bob = Bob.new
bob.login('world')
#=> global login
#=> user: hello, pass: world
#=> global login
#=> user: bob, pass: world
Upvotes: 0