Note: this was an April’s Fool post. However, with Crystal macros you could do this.
We Crystal developers believe compilers should be smart. You don’t need to add type annotations everywhere: only when needed, or when you want them. Can we make the compiler even smarter?
This past month we have been thinking that since Crystal is a relatively young language with a still incomplete standard library and ecosystem, there are lots of things to code yet. It’s unfortunate that many of these algorithms and data structures are already present in other languages. Not only that, but these other languages have been used for many years now, so their implementation is pretty robust and bug free. We will have to walk the same road in Crystal. Or will we?
Well, not anymore. The next release of Crystal will have a tiny but powerful addition: an auto keyword. To understand how it works, let’s see it in action:
class String
auto def succ
end
end
"hello".succ #=> "hellp"
The first thing you need to know is that String#succ
is not in Crystal’s standard library. In the code
above we define it with the auto keyword, leaving the body empty. We then invoke the method on some
string and it gives the correct value. Awesome! Crystal not only deduced the return type of succ
,
it also deduced its behaviour!
How auto is implemented
When we said auto is a keyword, we lied: it’s a macro. Macros in Crystal receive AST nodes, that is, they receive syntax. auto then receives a method definition and processes it at compile-time to generate a method definition that implements the desired functionality:
macro auto(method)
...
end
Macros can inspect the arguments: they can ask the method’s name, arguments or where the method is defined (String in the above example). If you need to do more complex stuff, you can invoke run in a macro, like this:
macro auto(method)
{{ run("auto/process", @type, method.name, *method.args) }}
end
This will invoke the program auto/process.cr
passing the type name, method name and splatted method arguments
to the program. The program then receives these arguments in the usual ARGV
array, processes them and
outputs a method definition that will then be embedded in our original program. Neat, right? We use a similar
technique for ECR (similar to ERB): the ECR templates are processed at compile-time.
The auto/process.cr
program does a few things: it searches the internet for relevant method definitions together
with their source code and possibly associated tests/specs. Right now this is only done for Ruby code because
of its similarity with Crystal, but support for other languages is coming soon.
Then it processes the code and generates Crystal code.
Now, this can be quite slow. In fact, it takes a few seconds (5 seconds on one of our machines). Luckily, the generated code is cached in the usual “.crystal” directory so the next time auto is used for the same method of a same type, it will reuse the cached version. But even with this penalty, think of the time you save by using auto: you don’t have to write the method, plus you reuse existing robust and well-tested code!
auto types
You can even use auto on a type:
auto class LinkedList(T)
end
list = LinkedList(Char).new
list.push 'a'
list.push 'b'
list.push 'c'
puts list.size #=> 3
puts list #=> ['a', 'b', 'c']
So the auto macro actually checks whether the received AST node is a class or method. For the class
case, auto/process.cr
will search that class name on the internet and generate a definition for it
together with every method it can find for it, reusing the previous logic.
Trying it
You can try all of this by checking out the auto branch in our GitHub repository, but you’ll need to compile a new compiler because we added some macro methods for this feature. Please understand that this is still very new so any bug you find, please report it!