Class instance variables and inheritance
In this post let’s take a look at some metaprogramming, class instance variable, and inheritance. Let’s dive into some code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
module Echoer
def echoes(*phrases)
@known_phrases.concat(phrases)
@known_phrases.each do |phrase|
next if self.respond_to?(phrase.to_sym)
define_method("#{phrase}") { phrase.to_s }
end
end
def known_phrases
@known_phrases.map do |phrase|
phrase.to_s
end
end
def self.extended(mod)
mod.instance_variable_set(:@known_phrases, [])
mod.send :include, InstanceMethods
end
module InstanceMethods
def echo(phrase)
phrase.to_s
end
def known_phrases
self.class.known_phrases
end
end
end
class Person
extend Echoer
echoes :hello, :goodbye
end
Here we’ve defined a module Echoer and a class Person that extends it. To understand what this code does let’s try a few examples:
1
2
3
4
5
6
7
8
9
10
Person.known_phrases # => ["hello", "goodbye"]
p = Person.new
p.known_phrases # => ["hello", "goodbye"]
p.echo("a phrase") #=> "a phrase"
p.echo([1, 2, 3]) #=> "[1, 2, 3]"
p.hello # => "hello"
p.goodbye # => "goodbye"
We see that Echoer adds the following behavior to a class that extends it:
- the class gains the
echoesmethod which defines what instances of the class know how to ‘say’. - the class and instances of it may be queried about what phrases they know via the
known_phrasesmethod - instances of the class gain the
echomethod which returns a string version of its argument
Thus Persons know how to say “hello” and “goodbye”, as well as arbitrary phrases through echo. If we forget, we can also ask Person and its instances what phrases it knows.
Now, let’s try subclassing:
1
2
3
4
5
6
class Shopper < Person
end
s = Shopper.new
s.hello # => "hello"
s.goodbye # => "goodbye"
So far, so good. But wait, there’s more:
1
2
3
s.known_phrases # => NoMethodError: undefined method `map' for nil:NilClass
Shopper.known_phrases # => NoMethodError: undefined method `map' for nil:NilClass
Shopper.echoes :charge # => NoMethodError: undefined method `concat' for nil:NilClass
Shoppers know how to say “hello” and “goodbye”, but they don’t know that they know it! Also, we can’t add to what a Shopper can say. What’s going on?
When we defined Shopper, it inherited hello and goodbye from Person. It did not inherit @known_phrases. This is because @known_phrases is a class instance variable of Person. A class instance variable is an instance variable of a class object. It is only defined on that class.
Programmers who use the Echoer module may be surprised by the inheritance (non-)behavior of echoes and known_phrases, so let’s fix it.
We simply need to add the following to Echoer:
1
2
3
def inherited(subclass)
subclass.instance_variable_set(:@known_phrases, @known_phrases)
end
Here we use the inherited hook to customize what happens when a class that extends Echoer is subclassed. Interesting points are:
- the correct definition is
def inheritednotdef self.inherited. Usingdef self.inheritedwill not add aninheritedmethod to classes extendingEchoer. inheritedaccepts the subclass as an argument. We simply set the subclass’s@known_phrasesto the value of the superclass’s@known_phrases.- there’s no call to
super. Things seem to work fine without it. If anyone can point to an authoritative resource stating thatsupershould or should not be called ininherited, I’d really appreciate it.
Now inheritance works as one might expect:
1
2
3
4
5
6
Shopper.echoes :charge
Shopper.known_classes # => ["hello", "goodbye", "charge"]
s = Shopper.new
s.known_classes # => ["hello", "goodbye", "charge"]
s.charge # => "charge"
Full code is below. Leave a comment!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
module Echoer
def echoes(*phrases)
@known_phrases.concat(phrases)
@known_phrases.each do |phrase|
next if self.respond_to?(phrase.to_sym)
define_method("#{phrase}") { phrase.to_s }
end
end
def known_phrases
@known_phrases.map do |phrase|
phrase.to_s
end
end
def self.extended(mod)
mod.instance_variable_set(:@known_phrases, [])
mod.send :include, InstanceMethods
end
def inherited(subclass)
subclass.instance_variable_set(:@known_phrases, @known_phrases)
end
module InstanceMethods
def echo(phrase)
phrase.to_s
end
def known_phrases
self.class.known_phrases
end
end
end
class Person
extend Echoer
echoes :hello, :goodbye
end
class Shopper < Person
echoes :charge
end
The more that you read, the more things you will know. The more that you learn, the more places you’ll go.
– I Can Read With My Eyes Shut! by Dr. Seuss