It's a typeof magic
The story of typeof
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
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 | 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.
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
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
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.
Simple uses of typeof
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
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.
Advanced uses of typeof
Let’s write the
Array#compact method. This method returns an
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
The type grammar allows creating unions. For example
Int32 | Char creates a union of
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:
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
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,
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,
NoReturn with any type
T basically gives you
T back. Because, if the
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
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
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