Comparing implementations of the Monkey language IV: Here comes a new challenger: Crystal

Mario Arias
4 min readJan 2, 2022

--

Previously…

In the last episode, I tried (and failed) to compile a Kotlin Native version of the Monkey Language. This setback opened new opportunities to explore different languages.

Why I’m doing this to myself?

What? Another implementation?, Yes.

An implementation of the Monkey Language (evaluator and compiler) is a mid-size project, so it lets you explore more of a language, more features, drawbacks, and its community and tools.

Also, this has been my third implementation. Therefore, I can make an objective comparison with other languages.

Introducing Monyet

Monyet (Bahasa Indonesia for monkey) is an implementation of the Monkey Language on Crystal. From the website:

Crystal’s syntax is heavily inspired by Ruby’s, so it feels natural to read and easy to write, and has the added benefit of a lower learning curve for experienced Ruby devs.

Why Crystal?

Crystal looks like a language that you can learn in a couple of days if you know Ruby already. I don’t consider myself a Ruby programmer. but I did a couple of hobby and professional projects back in the day (around 2012–2015).

Going back on time to the first days of Kotlin, you can see how some features were inspired by Ruby (And I read it on an interview but I cannot find it): Monkey patching to Extension functions, Block parameter to Last Function parameter outside the parenthesis. So algorithms implemented in Kotlin are easily translated to Crystal code

Crystal Strengths

Type System

Crystal type system includes many excellent features, like Generics, Union types, Nullable Types (Which are a Union Type specialisation), Inference and others.

Combined, it gives you a potent and easy-to-use language that looks like a dynamic one but is statically checked.

Macros

I never used macros in any language before. I found it too cumbersome. On Groovy, macros require manipulation of the AST and Scala, at least on version 2.x, had many different and competing macro implementations.

On Crystal, they’re very easy, no plugins, no compilation options, no crazy and obscure knowledge for the initiated. Just a simple template language (sort of Jinja or FreeMarker).

In general, I have a very low tolerance for duplicated code, but I can go to the next level with Crystal macros.

Now my brain works like this:

Neuron 1: Hey, these two lines of code…

Neuron 2: Macros

Neuron 1: Wait, let’s think about it first...

Neuron 2: MACROS

Neuron 1: But…

Neuron 2: MACROOOOOOS!!

To give you an example, this code in Kotlin:

It can be expressed like this on Crystal

And this is just a minor example. The possibilities and virtually endless as you can create new types and functions through macros.

LLVM

Crystal use LLVM as its compiler backend. So that gives you a certain level of performance by default and also allow you to use LLVM tools. For example, Instruments from XCode tools on MacOS will enable you to do Time profiling on Crystal applications.

Time profiling Monyet .

Crystal Weakness

Generics aren’t there yet

A nice change coming from Go, Generics in Crystal are a welcome feature, but they aren’t at Kotlin level yet. For instance, covariance and contravariance aren’t fully supported, and you cannot define a new Generic parameter at function level, just Type level (Update 03/01/2022 is actually possible using Free variables). You can fix some problems yourself with Macros but it is not as elegant as proper support. For example:

In Kotlin, you can define Generics at function level.

In Crystal, you need to define it at Type level, Struct in this case, and instantiate it.

It’s a lot more clunky, but it works.

Update 3 Jan 2022:

Thanks to Ary Borenszweig, one of the Core Team members for pointing me out that there is indeed a way to define Generics at function level using Free variables:

It does look less clunky.

Community size

The community is very passionate about the language, but it is tiny compared to other languages in the same niche, namely native compiled languages (Rust and Go). The official team has some support and sponsors but, again, not at the level of other languages.

The documentation is very good, and it mostly resolves all your questions, but is not complete as it doesn’t cover parts of the API that are important and useful e.g. ECR. There are just a few blog posts here and there and not many questions in StackOverflow. There is an official forum, and the developers are active, but it can take a couple of days before receiving an answer.

This also affects the tools. Editor plugins are very bare bones. VS Code has more features but isn’t consistent and crashes here and there. The plugin for JetBrains IDEs isn’t official, it only has Syntax highlighting and some navigation features, but it works well with almost no crashes. Unfortunately, I didn’t test the LSP implementation.

There is a code style linter name Ameba which actually works very well, and the SDK comes with its own format tool.

Compiler Speed.

The Crystal compiler has some clever features: Union types, macros, etc. It also ignores parts of your code that are unreachable. Very good.

But it comes with a cost. The compilation speed is slow on cold. It gets better with some cache but is still miles away from Go, albeit faster than Rust.

Conclusion

I enjoy my time with Crystal. For such a small team, it is a great language and I guess it’ll get better with time. My perceived complaints about the language are fixable with time, and there is nothing in the core of the language that is fundamentally broken.

And one message for the Ruby community: If you’re facing performance problems with Ruby (and the current tricks aren’t enough), please give Crystal a go; you’ll feel at home, don’t jump wagon to Go or Rust yet.

In the next entry, we’ll compare the performance of Monyet against the Kotlin and Go implementations.

--

--