Understanding Smalltalk classes and metaclasses

TL;DR

This tutorial explains the design of the Smalltalk object model, in particular the relationship between objects, classes and metaclasses.

Introduction

This tutorial is loosely based on Part 1 of the Blue Book.

The design of each object-oriented language differs slightly in how objects, classes and metaclasses are related to each other. Smalltalk's design can be explained in terms of the following seven points:

1. Every object is an instance of a class. 2. Every class eventually inherits from Object ProtoObject subclass: #Object instanceVariableNames: '' classVariableNames: 'DependentsFields' package: 'Kernel-Objects' . 3. Every class is an instance of a metaclass. 4. The metaclass hierarchy parallels the class hierarchy. 5. Every metaclass inherits from Class ClassDescription subclass: #Class instanceVariableNames: 'subclasses name classPool sharedPools environment category' classVariableNames: '' package: 'Kernel-Classes' and Behavior Object subclass: #Behavior instanceVariableNames: 'superclass methodDict format layout' classVariableNames: 'ClassProperties ObsoleteSubclasses' package: 'Kernel-Classes' . 6. Every metaclass is an instance of Metaclass ClassDescription subclass: #Metaclass instanceVariableNames: 'thisClass' classVariableNames: '' package: 'Kernel-Classes' . 7. The metaclass of Metaclass is an instance of Metaclass.

1. Every object is an instance of a class

In Smalltalk, everything is an object, and every object is an instance of a class.

true class == True
  
1 class == SmallInteger
  
nil class == UndefinedObject
  

The class of an object serves both as factory for instances and as a repository for common behavior. For instance, the methodDict of a class is a dictionary mapping messages to compiled methods.

True methodDict
  

2. Every class eventually inherits from Object

Classes in Smalltalk form a single inheritance hierarchy with Object ProtoObject subclass: #Object instanceVariableNames: '' classVariableNames: 'DependentsFields' package: 'Kernel-Objects' at the root.

Every class eventually inherits from Object.

true class inheritsFrom: Object
  
1 class inheritsFrom: Object
  

Aside: Actually there is another class, ProtoObject ProtoObject subclass: #ProtoObject instanceVariableNames: '' classVariableNames: '' package: 'Kernel-Objects'. ProtoObject superclass: nil , which is the superclass of Object. Mostly it can be ignored, but since Object already provides a large number of pre-defined methods, ProtoObject is sometimes useful to avoid having all this default behavior.

Object methodDict size
  

When an object receives a message, the method is looked up in the method dictionary of its class, and, if necessary, its superclasses, all the way up to Object (and PrototoObject).

The class of true has a method for the message #not.

true class selectors includes: #not
  

Although true understands #=, it is defined in Object, not in True or Boolean.

"true"
true class canUnderstand: #=
  
"not true"
true class selectors includes: #=
  
"not true"
Boolean selectors includes: #=
  
"true"
Object selectors includes: #=
  

If no method is found for a message, the default behavior is to send the message #doesNotUnderstand: with the reified message back to the original receiver.

"Won't be found, so will generate a #doesNotUnderstand: message"
true foo
  

3. Every class is an instance of a metaclass

Since everything is an object, it follows that classes are objects too. Since classes are objects, they too must be instances of classes. Such classes are known as metaclasses . Different object-oriented languages adopt different design decisions concerning whether or not metaclasses are supported, and, if they are, whether metaclasses can be explicitly defined or not.

In Smalltalk, there are no explicit metaclasses. Instead metaclasses are created implicitly when classes are created. The name of a Smalltalk metaclass of a class X is simply X class, and can be accessed by sending #class to X.

True class name = 'True class'
  

While a class serves as a repository of common behavior for its instances, a metaclass serves as the repository of behavior for its (unique) instance, namely the class.

Here we see the method dictionary of Boolean, defining methods understood by instances of Boolean (and its subclasses):

Boolean methodDict
  

In contrast, here is the method dictionary of Boolean class, holding methods understood by Boolean itself:

Boolean class methodDict
  

The GT Coder shows all class and instance methods together in one browser, however in the bottom right corner we can see whether a method belongs to the class or instance side.

For example, Boolean>>#isLiteral isLiteral ^ true is an instance method.

We can send #isLiteral to an instance of Boolean or its subclasses.

true isLiteral
  

On the other hand, Boolean>>#new new self error: 'You may not create any more Booleans - this is two-valued logic' is a class method.

We can send #new to Boolean itself or its subclasses (though this will generate an error, as intended).

True new
  

4. The metaclass hierarchy parallels the class hierarchy

This is a pragmatic design choice in the Smalltalk-80 system. By making the metaclass hierarchy parallel the class hierarchy, we ensure that every class also inherits its class-side methods from its metaclass.

NB: method lookup works exactly the same way for metaclasses as it does for classes.

5. Every metaclass inherits from Class and Behavior

Since rule 2 tells us that all classes eventually inherit from Object, we should infer that the same holds for metaclasses, which are also classes. So the metaclass hierarchy does not stop with Object class but rather with Object. In between, however, we have the special system classes Class, ClassDescription, and Behavior.

Behavior Object subclass: #Behavior instanceVariableNames: 'superclass methodDict format layout' classVariableNames: 'ClassProperties ObsoleteSubclasses' package: 'Kernel-Classes' provides the minimum state necessary for objects that have instances. It is also the basic interface to the compiler: creating a method dictionary, compiling a method, and accessing and modifying the class hierarchy.

ClassDescription Behavior subclass: #ClassDescription instanceVariableNames: 'organization commentSourcePointer' classVariableNames: '' package: 'Kernel-Classes' extends Behavior with instance variables, method categories, and change sets, amongst others.

Class ClassDescription subclass: #Class instanceVariableNames: 'subclasses name classPool sharedPools environment category' classVariableNames: '' package: 'Kernel-Classes' provides the common behavior of all (regular) classes.

6. Every metaclass is an instance of Metaclass

Metaclass ClassDescription subclass: #Metaclass instanceVariableNames: 'thisClass' classVariableNames: '' package: 'Kernel-Classes' serves as the shared repository of behavior for all metaclasses (just as Class is the shared repository of behavior for all normal classes).

true class class class = Metaclass
  
1 class class class = Metaclass
  
Smalltalk class class class = Metaclass
  

7. The metaclass of Metaclass is an instance of Metaclass

At last we can close the loop. Since everything is an object, Metaclass is also an object, and is an instance of Metaclass Metaclass class instanceVariableNames: '' . This, in turn, is itself a metaclass, and hence is an insatnce of Metaclass.

Metaclass class class = Metaclass
  

Now we can see how the parallel hierarchies are actually part of the same single inheritance hierarchy with Object ProtoObject subclass: #Object instanceVariableNames: '' classVariableNames: 'DependentsFields' package: 'Kernel-Objects' at the root.

What's next?