Why can't classes be used as modules?

倖福魔咒の 提交于 2020-06-12 06:06:43

问题


Module is the superclass of Class:

Class.superclass
# => Module

In OOP, this implies that an instance of Class can be used in every place where an instance of Module can be used.

Surprisingly, this is not the case with Class instances in Ruby:

class C end
c = C.new

module M end
# Let's do all the extend/include/prepend stuff with M!
c.extend M
C.include M
C.prepend M

# All worked fine until this line.
# Let's turn to classes now!

# First, get a class to work with.
class C_as_M end
C_as_M.class.superclass
# => Module # yes, C_as_M is an instance of a Module child
C_as_M.is_a? Module
# => true   # yes, it is still a Module

# And now let's do the same extend/include/prepend stuff with C_as_M!

c.extend C_as_M
# => TypeError: wrong argument type Class (expected Module)
C.include C_as_M
# => TypeError: wrong argument type Class (expected Module)
C.prepend C_as_M
# => TypeError: wrong argument type Class (expected Module)

What is the reason for the violation of this OOP principle? Why can't classes be used as modules?


回答1:


In OOP, this implies that an instance of Class can be used in every place where an instance of Module can be used.

You are confusing subtypes and subclasses, i.e. subtyping (which is about refinement of contracts) and inheritance (which is about differential code re-use).

In Ruby, inheritance creates a subclass, but not a subtype. (In fact, "type" is a concept that exists only in the programmer's head in Ruby.)

Class < Module is one example of a subclass that is not a subtype, and StringIO <: IO is an example of a subtype that is not a subclass.

What is the reason for the violation of this OOP principle?

It's not an OO principle. Subtyping and OO are completely orthogonal. There are OO languages without subtyping as well as non-OO languages with subtyping.


Note: it would actually be easy to collapse modules and classes into one. They could inherit from each other both the same way as classes inherit from classes as well as the way classes inherit from modules.




回答2:


I guess, meanwhile I've figured out why classes can't be used as modules in Ruby.
Or to be more specific: why classes can't be included/prepended.

It's the same reason as why Ruby does not support multiple inheritance:
to avoid ambiguity/complexity in the ancestors hierarchy.

After a short explanation of how multiple inheritance affects the ancestors hierarchy, I will explain why including/prepending classes would introduce multiple inheritance or/and similar complexities through the back door.


With single inheritance, the ancestors hierarchy of a given class is simply a chain.
The chain may be long or short, but it's always just a linear chain of ancestor classes:

File.ancestors
=> [File, IO, File::Constants, Enumerable, Object, Kernel, BasicObject]
Object.ancestors
=> [Object, Kernel, BasicObject]
BasicObject.ancestors
=> [BasicObject]

So looking up an instance variable or method is simple as that: look in the current class; if not found, go to the next ancestor and look there; if not found, go to the next ancestor and look there...


With multiple inheritance, the ancestors hierarchy can branch.
Hypothetically given

class A end
class B < A; end
class C < A; end

class D < B, C
end

produces the following ancestors graph of class D:

This increases complexity and ambiguity, it induces "the diamond problem":

  • Do instances of D share instance variables in class A inherited via B and inherited via C?
  • If they do not share them,
    we need an extended syntax to designate
    whether we want to access an instance variable in A via B or via C.

Ruby is designed to avoid this complexity. Hence, no multiple inheritance by design.


Including/prepending modules
is another mean of constructing/manipulating the ancestors hierarchy:

class MyBase < BasicObject; end
class C < MyBase; end
C.ancestors
=> [C, MyBase, BasicObject]

module IncludeMe end
C.include IncludeMe
C.ancestors
=> [C, IncludeMe, MyBase, BasicObject]

module PrependMe end
C.prepend PrependMe
C.ancestors
=> [PrependMe, C, IncludeMe, MyBase, BasicObject]

module Intermediate end
MyBase.include Intermediate
C.ancestors
=> [PrependMe, C, IncludeMe, MyBase, Intermediate, BasicObject]

Including/prepending modules only keeps the ancestors chain a simple chain.
No bad things happen.


Including/prepending classes

Now imagine IncludeMe, PrependMe, and Intermediate were not modules but classes.

To keep it simple, I will stick to one class only:

class PrependMe
  def to_s
    "Hello from prepended #{super}!"
  end  
end  

Keep in mind that PrependMe inherits from Object by default:

PrependMe.ancestors
# => [PrependMe, Object, Kernel, BasicObject]

Also, pay attention to that fact that in Ruby it's not possible to create baseless classes (BasicObject is the only baseless class):

class BaselessClass < nil # Try it!
end # You'll fail.
# => TypeError: superclass must be a Class (NilClass given)

So, each class X (except BasicObject) has an ancestors chain with at least two parts,
always ending with BasicObject:

X.ancestors
# => [X, ..., BasicObject]
# the shortest possible chain is [X, BasicObject]
# the usual chain is [X, ..., Object, Kernel, BasicObject]

So, what should the ancestors hierarchy of class C look like after C.prepend PrependMe?

C.ancestors
=> [PrependMe, Object, Kernel, C, MyBase, BasicObject] ?
=> [PrependMe, C, Object, Kernel, MyBase, BasicObject] ?
=> [PrependMe, C, MyBase, Object, Kernel, BasicObject] ?
=> [PrependMe, C, MyBase, BasicObject] ? # Object and Kernel omitted on purpose

Or should the ancestors hierarchy even branch at PrependMe into an own branch for Object? With all the effects of the diamond problem.

One could reasonably argue for each of these options.
The answer depends on what you want to see as the result of (c = C.new).to_s.

Obviously, including/prepending classes would introduce ambiguity and complexity similar to or even worse than these of multiple inheritance. And as in the case of multiple inheritance, it's a deliberate decision of Ruby to avoid this.

Apparently, this is the reason why including/appending classes is forbidden in Ruby.



来源:https://stackoverflow.com/questions/53449705/why-cant-classes-be-used-as-modules

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!