DEV Community

loading...
Cover image for Ruby has no class methods

Ruby has no class methods

edisonywh profile image Edison Yap Originally published at Medium ・5 min read

Wait…. Whaaaaaaaaaaaaaaaaaaat?

Mind Blown.gif

I don’t know about you, but that was my exact reaction when I first heard Nadia Odunayo’s talk in RubyConf KL 2018 (mind you, one of the most captivating talk I’ve ever heard!), in which she showed that that Ruby has no class methods.

When I first started learning Ruby, I’ve been told that Ruby basically have two types of methods:

  • Class Methods
  • Instance Methods
class Foo
  def self.class_method
    puts "This is a class method of #{self}"
  end

  def an_instance_method
    puts "This is an instance method of #{self}"
  end
end

Foo.class_method #=> "This is a class method of Foo"
Foo.new.class_method #=> undefined method `class_method' for #<Foo:0x007f83960d2228> (NoMethodError)

Foo.an_instance_method #=> undefined method `an_instance_method' for Foo:Class (NoMethodError)
Foo.new.an_instance_method #=> This is an instance method of #<Foo:0x007f80aa92d150>
Enter fullscreen mode Exit fullscreen mode

Well at first, it makes a lot of sense —

A class is an object, and when you call .new on a class, you create a new instance of that object.

Hence, instance methods are only available on instances of that class.

Okay so then.. what am I talking about when I said Ruby has no class methods?

Parents. Grandparents. Ancestors..!

First thing we need to understand is something called Ancestor Chains in Ruby.

Ancestors Chains is vital to understanding how Ruby works; the entire method calling chain works because of the chain, but here I’ll attempt at an overly simplified explanation.

In Ruby, everything inherits Object , and every Object inherits from a BasicObject.

Let’s do a quick check!

2.3.1 :001 > Foo.ancestors
 => [Foo, Object, Kernel, BasicObject]
2.3.1 :002 > Foo.superclass #superclass allow us to see what class the receiver inherited from. AKA, the parent of current class.
 => Object

2.3.1 :003 > Object.superclass
 => BasicObject

(Kernel is a Module, you could quickly verify it by doing Kernel.class)
Enter fullscreen mode Exit fullscreen mode

When you call a method, it calls the method on the current class; if the current class does not have that method, it goes up the ancestor chain until it finds one.

Not clear yet? I’ve gotchu! Cue weird metaphor

Imagine you’re just a kid, and that annoying Little Johnny who is trying to outsmart you asks you sheepishly: “Do you know what .planets are in our solar system? I bet you don’t!”.

Well you actually don’t know the answer, but you wouldn’t let Little Johnny get his victory, and so you make this your life goal to find out, and you ask your parents: “What are the .planets in our solar system?”. And well, your parents don’t know that either, so they go on and ask their parents, who happen to know the answer.

Now it’s your turn to smirk sheepishly back at Little Johnny and tell him what the answer is.

So now whenever someone asks you what are the .planets in our solar system, you have the answer, from your Grandparents.

How is this relevant? Well, we talked about how method lookup work, and in my last post I talked about reusing methods (Module) with include, extend and prepend. Now let’s look at their respective ancestor chains when you include, extend and prepend a module.

Awesome = Module.new

class IncludeModule
  include Awesome
end
IncludeModule.ancestors #=> [IncludeModule, Awesome, Object, Kernel, BasicObject]

class ExtendModule
  extend Awesome
end
ExtendModule #=> [ExtendModule, Object, Kernel, BasicObject]
# WHERE IS THE MODULE?!

class PrependModule
  prepend Awesome
end
PrependModule.ancestors #=> [Awesome, PrependModule, Object, Kernel, BasicObject]
Enter fullscreen mode Exit fullscreen mode

You’ll realize something strange:

  • When you include, the module is being inserted after the current class, which means it’ll be the first thing you hit when you can’t find a method on current class.
  • prepend is the opposite of include; it inserts before, so technically, you still wouldn’t be able to call it.
  • You don’t see the module on the ancestor chain when you extend a module

Hold on, we know for a fact that when we extend a module, we get access to class methods, and we also know that method calling happen via ancestor chains, but.. how can extend work if the module is not on the ancestor chain?

The reason is because of something in Ruby called Singleton Class (sometimes referred to as Metaclass, Anonymous Class or Eigenclass)

Whenever you initialize a class in Ruby, two objects are generated — Foo itself, and Foo’s Singleton Class. To prove this, in her talk Nadia showed a great way to inspect the current count of classes using ObjectSpace. ObjectSpace is basically a way for you to interact with all currently living Ruby objects in memory.

ObjectSpace.count_objects[:T_CLASS] #=> 920
Foo = Class.new
ObjectSpace.count_objects[:T_CLASS] #=> 922
Enter fullscreen mode Exit fullscreen mode

Now, before we understand what extend is doing, we need to first understand Singleton Class, so bear with me, get through the next section and we’ll then be able to understand the extend magic!

The one about Singleton Class

Singleton Class, like its alternative name, Anonymous Class suggests, is anonymous, thus does not show up in the ancestor chain(!!!) That doesn’t mean we can’t access the singleton class though, in fact, there’s a handy method called .singleton_class that we can use.

class Foo
end

Foo.singleton_class #=> #<Class:Foo>
Enter fullscreen mode Exit fullscreen mode

Let’s take a look at what is the .class of the class you just created.

class Foo
end

Foo.class #=> Class
Enter fullscreen mode Exit fullscreen mode

As you may have noticed earlier, there's also an alternative way to initialize a class as below:

Foo = Class.new
Enter fullscreen mode Exit fullscreen mode

Looks really familiar now doesn’t it? Foo is an instance of Class.

Why is this important? Because this means that the normal Class Methods that you have come to think you’re executing on Foo is in fact, an instance method on Class. — It works because Foo is an instance of Class!

We now know singleton classes aren’t visible on ancestor chains. Remember what else wasn’t visible on the chain? The module when you do an extend!

We can check the ancestor chain of the singleton class itself.

HiddenModule = Module.new
class Foo
  extend HiddenModule
end

Foo.singleton_class.ancestors #=> [#<Class:Foo>, HiddenModule, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]

# Well HiddenModule is not so hidden anymore!
Enter fullscreen mode Exit fullscreen mode

We can also prove that what we come to know as class_method, is actually just an instance method on the singleton class.

module Mystery
  def resolved?
      true
  end
end

class Foo
  extend Mystery
end

Foo.resolved? #=> true
Foo.singleton_class.instance_methods.grep(/resolved?/) #=> [:resolved?]
Foo.method(:resolved?) #=> #<Method: Class(Mystery)#resolved?>
Enter fullscreen mode Exit fullscreen mode

So now we know what extend actually does behind the scene is basically an include on the singleton class itself. Let’s prove it!

class Bar
  Bar.singleton_class.include Mystery
end

Bar.resolved? #=> true
Bar.singleton_class.instance_methods.grep(/resolved?/) #=> [:resolved?]
Bar.method(:resolved?) #=> #<Method: Class(Mystery)#resolved?>
Enter fullscreen mode Exit fullscreen mode

Still works!

But yeah, writing the full form requires a staggering three more words, that’s a lot of work, so we’ve got extend to save us that keystroke instead!

Author’s Note

I’ll be pumping out a few more articles, meanwhile do check-out my last post on included, extended and prepended hooks!

Phew, that’s it for my second blogpost. Looking forward to learning and sharing more!

Discussion (10)

pic
Editor guide
Collapse
yorodm profile image
Yoandy Rodriguez Martinez

So, you've effectively proven that.

  1. Ruby does have class methods, cause you know, metaclasses are not the same thing as classes.
  2. Metaclasses are cool (which it's known since Smalltalk times).
  3. MOP rules!
Collapse
edisonywh profile image
Edison Yap Author

Haha yeah, I admit the title is a bit clickbaity :p but it's more so that for my own personal understanding, class method wasn't what I thought it was.

Yeah in Nadia's talk she talked briefly about Smalltalk too! There's just so much more to learn, it's exciting!

Can explain what MOP is?

Collapse
yorodm profile image
Yoandy Rodriguez Martinez

Meta Object Protocol, oh man you're in for a treat!!!! Check this book

Thread Thread
gosukiwi_20 profile image
Federico Ramirez

Interesting, I was playing with writing an OO programming language in Common Lisp, looks like this might be a good architecture for the object system.

Collapse
chenge profile image
chenge

Good to learn, thanks. Where can we watch the video you said?

Collapse
edisonywh profile image
Edison Yap Author • Edited

Hey! I attended the RubyConf live, and I don't think they recorded anything, so frankly I'm not quite sure if a video exists.

Maybe you could start by looking at Nadia's Twitter or something?

EDIT: Hey man I took some time to dig and found it! nadiaodunayo.com/speaking/the-case...

Collapse
chenge profile image
chenge

I found it, thank you. Nice video.

nadiaodunayo.com/speaking/the-case...

Collapse
rhymes profile image
rhymes

Great explanation Edison!

Collapse
edisonywh profile image
Edison Yap Author

Thanks! Means a lot to someone who's pretty new to this blogging thing haha

Collapse
lucasprag profile image
Lucas Arantes

haha I found this post very interesting. Great work!