Crystal is a rising programming language with the slogan “Fast as C, Slick as Ruby”. It has some compelling features that make it more attractive than other modern language attempts like Go. You really can program in a Ruby-like language and achieve software that performs with the speed of a compiled language.
But the greatest advantage of Crystal, that I have experienced so far, is that it provides type-safety without excessive declarations as you would see in Java. It does this through program-wide type inference. So, if you write a function like this:
def add(a, b)
a + b
add(1, 2) # => 3, and the returned type is Int32
add(1.0, 2) # => 3.0, and the returned type is Float64
You get type-safe duck-typing at compile-time. If a method isn’t available in a type, you’ll find out at compile-time. Similarly, the type of a variable can be inferred from what you assign to it, and does not have to be declared.
Now, let’s say you never want to see
nil popping up unexpectedly as a value and your code breaking in production because
nil doesn’t have the methods you expect.
There are union types. So, if you want to see nil, you can declare your variable this way:
a : String | Nil
a : String? # Shorthand for the above.
Crystal handles metaprogramming in several ways. Type inference and duck typing gives functions and class methods parameterized types for free, without any declaration overhead. Then there are generics which allow you to declare a class with parameterized types. And there is an extremely powerful macro system. The macro system gives access to AST nodes in the compiler, type inference, and a very rich set of operators. You can call shell commands at compile-time and incorporate their output into macros. Most of the methods of String are duplicated for macros, so you can do arbitrary textual transformations.
There is an excellent interface to cross-language calls, so you can incorporate C code, etc. There are pointers and structs, so systems programming (like device drivers) is possible. Pointers and cross-language calls are “unsafe” (can cause segmentation faults, buffer overflows, etc.) but most programmers would never go there.
What have I missed so far? Run-time debugging is at a very primitive state. The developers complain that LLVM and LLDB have changed their debugging data format several times recently. There’s no
const and no frozen objects. The developers correctly point out that
const is propagated through all of your code and doesn’t often result in code optimization. I actually like it from an error-catching perspective, and to store some constant data in a way that’s easily shareable across multiple threads. But Crystal already stores strings and some other data this way. And these are small issues compared to the benefits of the language.
Paul Smith of Thoughtbot (a company well-known for their Ruby on Rails expertise) is creating the Lucky web framework, written in Crystal and inspired by Rails, which has pervasive type-safety – and without the declaration overhead as in Java.
The point of all of this is that you can create a web application as you might using Ruby on Rails, but you won’t have to spend as much time writing tests, because some of the most common problems of Ruby code are taken care of by the type system. And the combination of exceptions and type-safety does an excellent job of getting rid of most of the function return error checking I’d have to write in other languages. When you want to check for a nil rather than catch an exception, there are method versions suffixed with a ? which provide that.
Learning Crystal and Lucky, since I’m already a Rubyist, wasn’t difficult, but I took about two days including finding some bugs in Lucky and learning some non-obvious things about the language. Like it’s better not to declare the types of things a lot of the time. Rather than look up that the type of something was Lucky::AdmittedField, I could just declare the name of an argument that used it and go on with my life, and the compiler would take care of things.
The biggest problem with Lucky right now, in its pre-1.0 state, is that there is no API documentation. There are tutorial guides that tell you how to do most things, but I found myself exploring the Lucky code several times.
I am porting an application I’d written in Ruby for a new startup to Crystal and Lucky, to see if I can have more comfortable development with fewer run-time errors. If this works, I’ll have a large production application to better evaluate the language and framework.
The advantage of Node is that the native IO framework is non-blocking. Some Node enthusiasts don’t realize that almost every other web framework and server does non-blocking IO to handle the web requests, and you don’t have to concern yourselves with that. But you still have blocking by default for database queries, file I/O, and your calls to other services in the cloud. Crystal library authors could provide non-blocking I/O with promises for this, but the developers haven’t seen a good reason to do so. Crystal uses Fibers for concurrency (and will get multithreading). Fibers start with a 4K stack, and are so inexpensive that a 64-bit processor can realistically provide thousands of them per process. Having a straight-line logical flow through I/O rather than many event-handling blocks (probably nested) means more readable and maintainable code. The overhead of fibers seems a low cost for that.