Ruby class methods - ruby

Can some one please help me with this problem.
I try this code and i keep getting a method error as seen in the bottom
class Song
##play = 0; #class variable must be define before use
def initialize(name, artist, duration)
#name = name
#artist = artist
#duration = duration
#plays = 0
end
def play
#plays += 1 #same as #plays = #plays + 1
##play += 1
"This song: ##plays plays. Total ###play plays"
end
end
class SongList
Max_Time = 5*60
def self.is_too_long(song)
return song.duration > Max_Time
end
end
song1 = Song.new("Bicyclops", "Fleck", 260)
p song1.duration
p SongList.is_too_long(song1)
ruby_ex03.rb:49:in <main>': undefined method duration' for #Song:0x000000000605f338 (NoMethodError)

In Ruby, instance variables and methods are distinct things, and there is no way* to access instance variables from outside the class they belong to. If you want to make your instance variables accessible, you need to define accessors in your class
def duration
#duration
end
Fortunately, this pattern is so common that Ruby provides a shortcut for us. Simply put this in your class body
attr_reader :duration
*Technically, we can use reflection to access instance variables from anywhere, but conceptually you should think of them as inaccessible. Those techniques are advanced and useful only in special circumstances.

Related

Ruby - How to access instance variables from classes with "self" methods?

Sorry that I have no clue how to title this, I'm having a hard time looking this up because I don't know how to say this. Anyway...
Let's say I have a class that looks like this for example:
class Run
def self.starting
print "starting..."
end
def self.finished
print "Finished!"
end
end
All of the methods in Run have self before them, meaning that I don't have to do run = Run.new and I can just do Run.starting. Now let's say that I wanted to add some instance variables...
class Run
attr_accessor :starting, :finished
def self.starting
print "starting..."
#starting = true
#finished = false
end
def self.finished
print "finished!"
#starting = false
#finished = true
end
end
What if I wanted to access those instance variables from outside the class? I know that something like print "#{Run.finished}" or print "#{Run.starting}" won't do anything. Can I do that without run = Run.new? Or should I just remove self and then use run = Run.new? (Sorry if this question is a mess.)
All of the methods in Run have self before them, meaning that I don't have to do run = Run.new and I can just do Run.starting
There's much more to it than this. In your case you're calling class methods. If you did runner = Runner.new - then you'd be calling instance methods (those are defined without self.
In general, if you need "the thing" to hold some kind of state (like #running = true) then you'd rather want to instantiate an object, and call those methods.
Now, #whatever are instance variables, and you don't have the access to them in class methods.
class Run
attr_reader :running
def start
#running = true
end
def stop
#running = false
end
end
runner = Run.new
runner.running # nil
runner.start
runner.running # true
runner.stop
runner.running # false
I'd recommend you doing some tutorial or basic level book on rails programming, find a chapter about objects and classes. Do some exercises.
In Ruby instance variables are just lexical variables scoped to an instance of a class. Since they are scoped to the instance they always act like a private variable.
If you want to provide access to an instance variable from the outside you create setter and getter methods. Thats what attr_accessor does.
class Person
attr_accessor :name
def initialize(name:)
#name = name
end
def hello
"Hello my name is #{#name}"
end
end
john = Person.new(name: 'John')
john.name = "John Smith"
puts john.hello # "Hello my name is John Smith"
puts john.name # "John Smith"
Methods defined with def self.foo are class methods which are also referred to as singleton methods. You can't access variables belonging to an instance from inside a class method since the recipient when calling the method is the class itself and not an instance of the class.
Ruby also has class variables which are shared by a class and its subclasses:
class Person
##count = 0
def initialize
self.class.count += 1
end
def self.count
##count
end
def self.count=(value)
##count = value
end
end
class Student < Person
end
Person.new
Student.new
puts Person.count # 2 - wtf!
And class instance variables that are not shared with subclasses:
class Person
#count = 0 # sets an instance variable in the eigenclass
def initialize
self.class.count += 1
end
def self.count
#count
end
def self.count=(value)
#count = value
end
end
class Student < Person
#count = 0 # sets its own class instance variable
end
Person.new
Student.new
puts Person.count # 1
Class variables are not used as often and usually hold references to things like database connections or configuration which is shared by all instances of a class.
You can't access instance variables from outside the instance. That is the whole point of instance variables.
The only thing you can access from outside the instance are (public) methods.
However, you can create a public method that returns the instance variable. Such a method is called an attribute reader in Ruby, other languages may call it a getter. In Ruby, an attribute reader is typically named the same as the instance variable, but in your case that is not possible since there are already methods with the names starting and finished. Therefore, we have to find some other names for the attribute readers:
class Run
def self.starting?
#starting
end
def self.finished?
#finished
end
end
Since this is a common operation, there are helper methods which generate those methods for you, for example Module#attr_reader. However, they also assume that the name of the attribute reader method is the same as the name of the instance variable, so if you were to use this helper method, it would overwrite the methods you have already written!
class << Run
attr_reader :starting, :finished
end
When you do this, you will get warnings (you always have warning turned on when developing, do you?) telling you that you have overwritten your existing methods:
run.rb:19: warning: method redefined; discarding old starting
run.rb:2: warning: previous definition of starting was here
run.rb:19: warning: method redefined; discarding old finished
run.rb:5: warning: previous definition of finished was here

any way capture the name of a constant on declaration?

I have a class. Let's call it SomeClass:
class SomeClass
end
Instead of defining instances of this class the normal way, I would like to define them all using a constant:
MyConstant = SomeClass.new
I want to be able to capture the name of the constant that some class was set too, in much the same way that standard ruby classes do with the .class method.
MyConstant.name #-> "MyConstant"
I want to be able to do this to render better error messages from all instances of some class, like so:
class SomeClass
def display_error_message
"Error, some class #{self.name} has a problem"
end
end
MyConstant.display_error_message
#-> "Error, some class MyConstant has a problem"
Any way to accomplish this?
EDIT
Here's an example to clarify what I'm shooting for.
(Enum is the name of the class I'm creating, which is meant to act similar to Swifts 'Enum' type. Basically it sets a predefined list of options (:pepperoni, :sausage, :mushroom) with a raw_value ("Pepperoni", "Sausage", "Mushroom".) Obviously in this example a hash or a simple algorithm for converting a symbol to an UpperCamel case string could work, but in reality the enum class will do a lot more, but this example shows the gist of it.
class Pizza
attr_reader :topping
Toppings = Enum.new do
option(:pepperoni).set("Pepperoni")
option(:sausage).set("Sausage")
option(:mushrooms).set("Mushrooms")
end
def set_topping(symbol)
#topping = Toppings[symbol]
end
end
pizza = Pizza.new
### Happy Case
pizza.set_topping(:pepperoni)
### Sad Case (Error message shown below is what I'm trying to figure out)
pizza.set_topping(:spinach)
#-> Error. enum Toppings has no option spinach
A variable is just a way of referring to an object, and the variable's name is irrelevant. If you say X = Y and Y happens to be a class, then the Y class already has the name "Y", so you can't change that.
As far as Ruby is concerned X and Y are indistinguishable.
If you want to alter the name you can make a subclass even if that subclass doesn't do anything different:
X = Class.new(Y)
X.name
# => "X"
Z = X
Z.name
# => "X"
That way preserves the name properly but only in the context of the initialization. I think Ruby does something sneaky and if a new class is being assigned to a constant it assigns a name, but for ordinary variables it does not:
x = Class.new(Y)
x.name
# => nil
So this is a special case.
The key here is that there's a huge difference between a subclass, which does impact the name, and a variable reference, which doesn't.
There's some other strange stuff going on here as it seems like the class somehow "knows" when it's being assigned to something and if that something is a constant it steals the constant's name for itself:
z = Class.new
z.name
# => nil
Z = z
z.name
# => "Z"
As they say in programming: "Wat?"
Your Enum class could look something like this:
class Enum
def initialize(name, &blk)
#defined_options = {}
#name = name.freeze
instance_eval(&blk)
#defined_options.freeze
end
def [](key)
if #defined_options.key? key
#defined_options[key].value
else
unfound_option = Option.new(#name, key)
raise "Option #{unfound_option} not found."
end
end
def to_s
"#{#name}"
end
def inspect
keys = #defined_options.keys.join(',')
"#<#{self}::{#{keys}}>"
end
class Option
attr_reader :value
def initialize(enum_name, key)
#value_initialized = false
#enum_name = enum_name
#key = key
end
def set(value)
if #value_initialized
raise "Value for #{self} can't be set to #{value} " +
"because it is already initialized to #{#value}"
else
#value_initialized = true
#value = value.freeze
end
end
def to_s
"#{#enum_name}::#{#key}"
end
def inspect
"#<#{self}>"
end
end
private
def option(sym)
unless #defined_options.key? sym
option = Option.new(#name, sym)
#defined_options[sym] = option
end
#defined_options[sym]
end
end
Now you can almost keep the syntax you have in your question, and do the following:
class Pizza
attr_reader :topping
# I had to add the name in the initializer for better error reporting.
Toppings = Enum.new('Toppings') do
option(:pepperoni).set("Pepperoni")
option(:sausage).set("Sausage")
option(:mushrooms).set("Mushrooms")
end
def set_topping(symbol)
#topping = Toppings[symbol]
end
end
pizza = Pizza.new
### Happy Case
pizza.set_topping(:pepperoni)
#=> "Pepperoni"
### Sad Case (Error message shown below is what I'm trying to figure out)
pizza.set_topping(:spinach)
#=> RuntimeError: Option Toppings::spinach not found.
This might give you an idea how to solve this issue. This is just a rough sketch of the class and could probably be tweaked to your needs.

save instances in global variable

I need to save all instances of an object in a global variable, so I can access that instances from another object. There is no need to pass them like parameteres.
In my solution I have a mixin with a method that puts the instance in a variable, also I used an open class technique to include that mixin in Object, so other objects use that method (and not only ONE class).
class Object
include Favourite
end
module Favourite
def favourite_it
#if the variable its not initialized:
#favourites.class == Array.class ? #favourites.push(self) :
#favourites = [].push(self)
end
def get_favourites
#favourites
end
end
#this class is only an example
class Dog
def initialize age
#age = age
end
end
class Dog_T
#all instances of this class will be saved in the variable
def initialize age
#age = age
favourite_it
end
end
class Handler
def do_something
#here I need to access the variable with all the instances of favourites to do something to them
end
end
And here is a simple test
handler = Handler.new
d1 = Dog_T.new(10)
d2 = Dog_T.new(12)
all_f = Handler.get_favourites
expect(all_f[0].age).to eq 10
expect(all_f[1].age).to eq 12
d3 = Dog_T.new(15)
all_f = Handler.get_favourites
expect(all_f[3].age).to eq 15
I tried to do this, but only each instance save itself in a different list (it makes sense because I'm not using global variables yet).
How can I do to have only one list, add the instances when are created and have the ability to empty and manipulate that list from Handler?
Ruby supports using a class variable in a Module. You also need a reader method for the Dog_T object to be able to access the instance variable. Since Favourite doesn't know anything about the object, you will want to use respond_to? to guard against calling an non-existent method in the list. For example, if there were a Dog_R class that did not have a method age but did add itself, you would get a runtime error blindly calling the age method on members of the Favourite array.
module Favourite
##favourites = [] # you can use a class variable in module
def self.favourite_it(obj) # singleton method of the class
##favourites.push(obj)
end
def self.get_favourites # singleton method of the class, see below for usage example
##favourites
end
end
class Object
include Favourite
end
class Dog
def initialize age
#age = age
end
end
class Dog_T
attr_reader :age # you need a reader to able to access it
def initialize age
#age = age
Favourite.favourite_it(self)
end
end
d1 = Dog_T.new(10)
d2 = Dog_T.new(12)
all_f = Favourite.get_favourites
all_f.each do |obj|
puts "#{obj.class}: #{obj.age if obj.respond_to?(:age)}"
end
puts '-' * 20
d3 = Dog_T.new(15)
all_f = Favourite.get_favourites
all_f.each do |obj|
puts "#{obj.class}: #{obj.age if obj.respond_to?(:age)}"
end
The output of this program is:
Dog_T: 10
Dog_T: 12
--------------------
Dog_T: 10
Dog_T: 12
Dog_T: 15

Can a class and its parent contain member variable of the same name?

I made the following examples to demostrate. If I use name in both Song and KarokeSong class, I get an error.
class Song
def initialize(name, artist, duration)
#name = name
#artist = artist
#duration = duration
end
def to_s
"Song: #{#name}--#{#artist} (#{#duration})"
end
end
class KarokeSong < Song
def initalize(name, kname, artist, duration, lyrics)
super(name, artist, duration)
#name = kname
#lyrics = lyrics
end
def to_s
"KS: #{#name}--#{#artist} (#{#duration}) [#{#lyrics}] **#{#name}**"
end
end
songA = Song.new("Bicyclops", "Fleck", 260)
puts songA.to_s
songB = KarokeSong.new("John", "Casey", "The Band", 400, "And now, the...")
puts songB.to_s
Here is the error that I've received when trying to run the file.
stanley#ubuntu:~/Github/webdev_class/ruby$ ruby inheritance.rb
Song: Bicyclops--Fleck (260)
inheritance.rb:28:in `initialize': wrong number of arguments(5 for 3) (ArgumentError)
from inheritance.rb:28:in `new'
from inheritance.rb:28:in `<main>'
I'm guessing using the name name twice is not allowed if there's an inheritance relationship. I'm not sure if it has to do with the fact that Ruby doesn't support multiple-inheritance or V-pointers. Thanks in advance for your suggestions of what's happening.
I think the problem is that KaraokeSong isn't inheriting from Song.
Change the KaraokeSong class definition to class KaraokeSong < Song
As an answer to the question in the title: instance variables are bound to the object, not to the class. #name will always refer to the same variable within a given object, no matter which class the method that accessed it belongs to.
super name # assigns name to #name
#name = kname # overwrites name with kname

How do I call a custom method for an object?

I have a class with several methods:
class Test
def initialize (age, height)
#age = age
#height = height
end
def older
#age = #age + 2
end
def shorter
#height = #height - 5
end
end
man = Test.new(40, 170)
man.older
man.shorter
[...]
I want to pass onto object man a custom method, that is, I want to write something like man.variablemethod and set .variablemethod to either .older or .shorter, depending on some other factors. How can I do that?
I figured out that I can call "if condition then man.older", but I do not want to use if, especially when I have twenty different methods to choose from.
Sounds like you need send:
man.send(method_name)
You can pass a string or symbol representing the name of the method you wish to call, even pass additional arguments too:
def man.older_by_increment(years)
#age += years
end
man.send(:older_by_increment, 7)
This also allows you to call private and protected methods from anywhere:
class Man
# ...
private
def weight
#weight
end
end
Man.new.weight # => private method `weight' called for #<Man:0x10bc956d8> (NoMethodError)
Man.new.send(:weight) # => #weight

Resources