Subclasses
Tags: crystal, todayilearned, smalltalk, beginners
Published: 2022-07-11 13:27:03

The other day I was writing some code examples in [Crystal](https://crystal-lang.org/) and I wanted to print the `subclasses` for a given `Class`. In Smalltalk we would've used the message [subclasses](https://www.gnu.org/software/smalltalk/manual-base/gst-base.html#Behavior_002daccessing-class-hierarchy) and so I tried:

```crystal
class A; end
class B < A; end

p A.subclasses
```

I was expecting:

```terminal
[B]
```

But the actual output was:

```terminal
Error: undefined method 'subclasses' for A.class
```
😢

I was like: ok, no problem! Let's search in [Crystal's API](https://crystal-lang.org/api/1.4.1/index.html) for such method ... 📖 ... oh! here it is! [#subclasses](https://crystal-lang.org/api/1.4.1/Crystal/Macros/TypeNode.html#subclasses:ArrayLiteral(TypeNode)-instance-method). 

The method exists so ...

**So why didn't it work?** 🤔
The method `#subclasses` exists in [Macro land](https://crystal-lang.org/reference/1.4/syntax_and_semantics/macros/index.html) meaning it's available at compile time but we were trying to use it at runtime. 

**Note:** `Macro land` sounds like a whole new world and let me be honest: It is! And it's a powerful feature in Crystal!

So at that point I had a new challenge: implement `subclasses` to be used as we originally intended, and of course the implementation would be using `Macros`. I discussed different solutions with some of the [core team members] (https://crystal-lang.org/team/). Here are two possible and interesting solutions:

## Solution 1 🤯
[Beta](https://forum.crystal-lang.org/u/beta-ziliani) proposed the following solution:

> **Open the class** `Class` and define a new method `subclasses` implemented like this:

```crystal
class Class
  def self.subclasses
    {{ @type.subclasses }}
  end
end
```

Now let's use the new method with the following example:

```crystal
class A
end
 
class B < A
end
 
class C < A
end
 
class D < C
end
 
class E < B
end
 
p A.subclasses, B.subclasses
```

The example outputs:

```terminal
[B, C]
[E]
```

It worked! 🤓🎉

But now let's try to be more specific on the method's type. We are going to say that the method returns an `Array` of objects of the same type as the class for which we are defining the new method:

```crystal
class Class
  def self.subclasses : Array(self.class)
    {{ @type.subclasses }}
  end
end
```

The example outputs:

```terminal
Error: method B.subclasses must return Array(B.class) but it is returning Array(E.class)
```

No! What happened?!🤔
The compiler is complaining about `B.subclasses` returning `Array(E.class)` instead of `Array(B.class)`. Well, the compiler is right! But, why it didn't complain about `A.subclasses`?

Well, let's make a pause and write some examples:

### The type of an array with classes

Suppose we have classes `A` and `B` and an `Array` with these two classes, what would be the type of said `Array`:

```crystal
class A; end
class B; end

arr = [A, B]
p typeof(arr)
```

The output:

```terminal
Array(A.class | B.class)
```

The type of `Array` is `Array` (of course) and the type of its content is the union of the type of `A` and `B`.

And now, let's redefine `B` inheriting from `A`:

```crystal
class A; end
class B < A; end

arr = [A, B]
p typeof(arr)
```

The output:

```terminal
Array(A.class)
```

Oh! so when the union type can be "merged" to a common type then Crystal will do that "type merge".

With this hypothesis in mind, let's write a little modification of the previous example:

```crystal
class A; end
class B < A; end
class C < A; end

arr = [B, C]
p typeof(arr)
```

The output:

```terminal
Array(A.class)
```

Exactly what we were expecting! 🤓🎉

---

Now, let's go back to our main example:

```crystal
class Class
  def self.subclasses : Array(self.class)
    {{ @type.subclasses }}
  end
end

class A
end
 
class B < A
end
 
class C < A
end
 
class D < C
end
 
class E < B
end
 
p A.subclasses, B.subclasses
```

The **Error** output:

```terminal
Error: method B.subclasses must return Array(B.class) but it is returning Array(E.class)
```

Now it's clear what's going on:
- It doesn't raise an error on `A.subclasses` because there are more than one element in the returned `Array`, and they all have the same common upperclass `A`. So the returned type is `Array(A.class)`, which matches the specification of the method.
- It does raise an error on `B.subclasses` because there is only one element `E`, and so the type is `Array(E.class)` (it doesn't try to find an upperclass) and so the returned type doesn't fit the specification.

The solution would be to tell the compiler to treat each element [as](https://crystal-lang.org/reference/1.4/syntax_and_semantics/as.html) its parent type:

```crystal
class Class
  def self.subclasses : Array(self.class)
    {{ @type.subclasses }}.map(&.as(self.class))
  end
end

class A
end
 
class B < A
end
 
class C < A
end
 
class D < C
end
 
class E < B
end
 
p A.subclasses, B.subclasses
```

The output:

```terminal
[B, C]
[E]
```

And it's working again! 🎉

## Solution 2 🤯
[Johannes](https://forum.crystal-lang.org/u/straight-shoota) proposed this other solution:

> **Open the class** `Class` and define a new method `subclasses` implemented like this:

```crystal
class Class
  def self.subclasses : Array(self.class)
    {% begin %}
      [{{ @type.subclasses.join(",").id }}] of self.class
    {% end %}
  end
end
```

It also worked! 🤩

## The difference between both solutions 
The first one executes the "type conversion" at runtime while the second one specifies the type of the returned `Array` at compile time (when expanding the macro) without the need to "convert" each element at runtime.

## Print all the subclasses 💪
Now, here is another new challenge: we want to define a new method `#all_subclasses`. For the previous example, this new method should return:

```crystal
A.all_subclasses # => [B, E, C, D]
```

As we can see, it should return the _direct_ subclasses of `A` (as before) and the subclasses of the subclasses (recursively). Meaning we should return the possible paths starting from `A`:

```
A -> B -> E
A -> C -> D
```

{% spoiler Here is the solution: %} 

### Based on Solution 1:

```crystal
class Class
  def self.subclasses : Array(self.class)
    {{ @type.subclasses }}.map(&.as(self.class))
  end
  
  def self.all_subclasses : Array(self.class)
    {{ @type.all_subclasses }}.map(&.as(self.class))
  end
end

class A
end

class B < A
end

class C < A
end

class D < C
end

class E < B
end

p A.subclasses
p A.all_subclasses
```

The output:

```terminal
[B, C]
[B, E, C, D]
```

### Based on Solution 2:

```crystal
class Class
  def self.subclasses : Array(self.class)
    {% begin %}
      [{{ @type.subclasses.join(",").id }}] of self.class
    {% end %}
  end
  
  def self.all_subclasses : Array(self.class)
    {% begin %}
      [{{ @type.all_subclasses.join(",").id }}] of self.class
    {% end %}
  end
end

class A
end

class B < A
end

class C < A
end

class D < C
end

class E < B
end

p A.subclasses
p A.all_subclasses
```

The output:

```terminal
[B, C]
[B, E, C, D]
```

{% endspoiler %}

## Farewell and see you later

Let's recap:

We have extended the `Class` class implementing the new methods `subclasses` and `all_subclasses` using `macros`.

Hope you enjoyed it! 😃