The story of the typeof
expression begins with array literals. In Crystal you can write
and the compiler will infer that the array is an Array(Int32)
, meaning it can only contain
32 bits integers. And you can also write:
and the compiler will infer that it’s an Array(Int32 | Char | Bool)
, where Int32 | Char | Bool
means the union of those types: the array can hold any of those type at any point during the
program’s execution.
Literals in the language, like array, hash and regular expression (regex) literals, are simple syntax rewrites to regular standard library calls. In the case of a regex, this:
is rewritten to:
The rewrite of array literals needs a bit more thought. Arrays are generic, meaning that they are parameterized
with a type T
that specifies what type they can hold, like the Array(Int32)
and Array(Int32 | Char | Bool)
mentioned earlier. The non-literal way to create one is:
In the case of an array literal we need the type to be the union type of all the elements in the array literal.
And so, typeof
was born. In the beginning this was called type merge
and it was a compiler internal thing
that you couldn’t express (there was no syntax for it), but the compiler used it for these literals. An
example rewrite:
Now this literal is invoking a regular method
to build an array. The catch is that you couldn’t write this: <type_merge>
is only the representation of this internal node
that allows you to compute a type, but if you wrote the above you would get a syntax error.
We later decided that because this <type_merge>
node worked pretty well, and we wanted literals to have no magic,
to let users use this <type_merge>
node, and named it typeof
, because this name is pretty familiar in other languages. Now
writing this:
and this:
are exactly equivalent: there’s no magic (but of course the first syntax is much easier to write and read).
Little did we know that typeof
would bring a lot of power to the language.
One obvious use-case of typeof is to ask the compiler the inferred type of an expression. For example:
At this point you might think that typeof(exp)
is similar to exp.class
. However,
the first gives you the compile-time type, while the second gives you the runtime type:
Another simple use case is to create a type based on another object’s type:
In this way we can avoid repeating or hardcoding a type name.
But these are too simple to be interesting.
Let’s write the Array#compact
method. This method returns an Array
where nil
instances are removed.
Of course, if we start with an Array(Int32 | Nil)
, that is, an array of integers and nils, we want to
end with an Array(Int32)
.
The type grammar allows creating unions. For example Int32 | Char
creates a union of Int32
and Char
.
However, there’s no way to subtract types. There’s no T - Nil
syntax. But, using typeof
, we can still
write this method.
First, we define a method whose type will be the one we want:
If exp
is Nil
we raise an exception, otherwise we return exp
. Let’s check its type:
Thanks to the way if var.is_a?(…) works,
when we give it something that’s not nil
it tells us that the type is that same type. But when we give it
nil
, the only branch in the if
that can be executed is the raise
one. Now, raise
has this NoReturn
type, which basically means there’s no value returned by that expression… because it raises an exception!
Another expression that has NoReturn
is, for example, exit
.
Let’s try and give not_nil
something that’s a union type:
Note that the NoReturn
type is gone: the “expected” type of the last expression would be Int32 | NoReturn
, that
is, the union of the possible types of the method. However, NoReturn
doesn’t have a tangible value,
so mixing NoReturn
with any type T
basically gives you T
back. Because, if the not_nil
method
succeeds (that is, it doesn’t raise), you will get an integer back, otherwise an exception will be bubbled
through the stack.
Now we are ready to implement the compact method:
The magical line is the first one in the method:
We create an array whose type is the type that results of invoking not_nil
on the first element of the array. Note
that the compiler doesn’t know what types are in each position in an array, so using 0
, 1
or 123
would be the same.
In this way we were able to forge a type that excludes Nil
without needing to extend the type grammar: the compiler’s
machinery for the type inference algorithm is all we needed.
But this is still simple. Let’s move on to something really interesting and fun.
Our next task is to implement Array#flatten
. This method returns an Array
that is a one-dimensional flattening
of the original array (recursively). That is, for every element that is an array, extract its elements into this new
array.
Note that this has to work recursively. Let’s see some expected behaviour:
Like before, let’s start by writing a method whose type will have the type that we need for the flattened array:
The method is simple: if the object is an array, we want the flatten type of any of its elements. Otherwise, the type is that of the object.
And with this, we are ready to implement flatten:
In this second example we were able to forge a type that is an array flattening.
In the end, there’s nothing really magical about typeof
. It just lets you query and use the compiler’s
ability to infer the type of an expression really well.
In the previous examples we were able to forge types from other types with regular stuff: types and methods. There’s nothing new to learn, there’s no special syntax for talking about types. And this is good, because it’s simple, but powerful.
comments powered by Disqus