learningtech
learningtech

Reputation: 33715

How to access global method from object?

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

Answers (5)

Cary Swoveland
Cary Swoveland

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

7stud
7stud

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

histocrat
histocrat

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

Guilherme
Guilherme

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

Surya
Surya

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

Related Questions