Search code examples
rubyclassmoduletypeerrordynamic-typing

Ruby inheritance and typing


I am having trouble with some fundamental concepts in Ruby, specifically the interchangeability of a subclass for the superclass.

According to the Ruby documentation on classes, "Class" inherits from "Module". https://ruby-doc.org/core-2.5.3/Class.html

class MyClassTest end
MyClassTest.is_a? Module # => true

However, when trying to use the module keyword to reopen a class defined with the keyword class, you get a TypeError that the class is not a module.

class MyClassTest end
module MyClassTest end # => TypeError: MyClassTest is not a module

This SO question has some excellent discussion surrounding subclasses vs subtypes, but I think it has lead me to more questions: Why can't classes be used as modules?

Generally, since Ruby is dynamically typed, I am confused by the existence of TypeErrors.

Specifically, in this case, I am extra confused as to how Ruby inheritance can result in a TypeError where the subclass cannot be substituted for the superclass. In my mind, subclassing is equivalent to subtyping in Ruby since the subclass will inherit the interface (methods and public attributes) of the superclass.

My current guess is that TypeError's are raised by the core Ruby library when certain assertions fail, and these TypeErrors don't necessarily have anything to do with Ruby's dynamic typing system, which is to say that typing is not a first-class concept in Ruby. The linked SO question raises excellent points about the diamond problem with multiple class inheritance, so it makes sense that Ruby would prevent the interchangeable usage of modules and classes when using the module or class keyword. Still, it feels like there are inconsistencies in my understanding of Ruby.

How can a "Class" input result in a TypeError when a "Module" object is expected?


Solution

  • Basic assertions are

    • Class is a Class (Class.is_a?(Class) #=> true)
    • Class is a Module (Class.is_a?(Module) #=> true)
    • An instance of the class Class is a Class (Class.new.is_a?(Class) #=> true)
    • An instance of the class Class is a Module (Class.new.is_a?(Module) #=> true)
    • Module is a Class (Module.is_a?(Class) #=> true)
    • By virtue Module is a Module (Module.is_a?(Module) #=> true)
    • An instance of the class Module is a Module (Module.new.is_a?(Module) #=> true)
    • However An instance of the class Module is not a Class (Module.new.is_a?(Class) #=> false)
    • an instance of the class Class is an instance of Class but not and instance of the class Module (Class.new.instance_of?(Module) #=> false)

    module is a declaration for an instance of the class Module just as class is a declaration for an instance of the class Class.

    If this were a method it might look like

    def module(name,&block)
      raise TypeError if defined?(name) && !const_get(name).instance_of?(Module)
      obj = defined?(name) ? const_get(name) : const_set(name, Module.new)
      obj.instance_eval(&block)
    end
    

    TypeError exists to prevent ambiguity.

    As in your case by using class MyClassTest you have created an instance of the class Class and that instance is called MyTestClass.

    If you were also allowed to use module MyTestClass, in the same global context, then during usage I would be unaware if when calling MyClassTest I would be calling the Class or the Module.

    The basic (very basic) difference is a Class can be instantiated (have instances) however a Module cannot.

    For instance

    Class.new.new #creates an instance of the anonymous class created by Class.new
    Module.new.new # results in NoMethodError