💾 Archived View for dioskouroi.xyz › thread › 25005780 captured on 2020-11-07 at 00:51:38. Gemini links have been rewritten to link to archived content
-=-=-=-=-=-=-
________________________________________________________________________________
I'm a Rubyist by profession and I've tried Crystal multiple times but never really felt like I was enjoying it. I can't really put my finger on why, but perhaps it falls into the "uncanny valley" where it looks a lot like Ruby but is slightly different in peculiar ways.
I vaguely recall being confused about including other files and module definitions last time, like you could put the requires in really weird places. It has been a while, though.
Oddly, I found Elixir much more enjoyable, and extremely quick to learn. Whereas Crystal feels like an attempt to build a "compiled Ruby", Elixir feels more like it took the ideas behind what Ruby is and applied it to a wildly different design, and as a result actually feels like how Ruby felt when I first learnt it.
But I do give Crystal a go every once in a while just to see if I finally get the hang of it.
The problem is:
> Crystal’s syntax is almost identical to Ruby’s...
...But the semantics are very different.
So you're writing in one language but it's as if the meaning has been swapped out underneath for the meaning from another language.
I have a love-hate relation with Crystal. Every few months, disenchanted with the core maintainers' priorities, lack of platform and tooling updates and overall deadland syndromes -
I denounce Crystal, promise I will never use it again, startup a golang or Rust project, make some non trivial toy stuff and then come crawling back to Crystal.
I hate the fact that I love this language and the fact that nothing else (except perhaps f# and nim) seems to have the same effect on me.
My favorites Crystal features are its do-end syntax, dat sexy type system hnnnnghhh... , domain modelling using sum types, null checking using types, ultra simple OOPs, insanely productive std and its sheer performance.
The bits about the language I detest are the overuse of macros, the utter lack of any platform (windows, http/2 etc.) or tooling improvements (IDE support, slow compilation). The core maintainers are amazing but have a weird obsession with just refactoring the language semantics. Things which really would matter for any language usage are just relegated to GH issues which haven't had any comments on it for months (if not years).
The lack of a BDFL and corporate sponsorship really hurts Crystal bad. Its a language without directed growth. For a language which seems to be used the most for web servers, it lacks http/2, db pipelining and async db drivers. There have been plans for redoing the http module for years but it hasn't been done yet.
Yet, I love this language despite all its shortcomings. I find it to be one of the most readable languages out there. The community is full of amazing individuals who are ever helpful and welcoming. The core maintainers are super talented developers who really value quality of code.
Right now I am working on an api servers and have 3 early implementations - one in Rust-actix, one in pure crystal (no framework) and one in Go-Fiber. The joy of using Crystal and feeling like I am in control of the project are reasons which are pushing me towards using Crystal for the project. But I know that by the next few years, we likely still wouldn't have
http/2, async drivers, nice IDE support and many other features that I really need.
And so, I will likely have to go with Go. (Rust syntax is just too complex for my taste. And I am not talking about the celebrated borrow checker).
I like go.
Go is simple.
Go is Productive.
Go is... just not Crystal.
Go is simple. I can't, however, say I find go remotely as productive as I find ruby. In fact, literally nothing I've found is as productive as ruby. I've had high hopes that crystal would become a better, faster, more capable ruby, but I share your concerns.
I left ruby and rails years ago, and here I am in 2020 thinking about going back. I've made very strong attempts to use python, go, kotlin and groovy in the years since, but literally nothing touches ruby in terms of sheer developer productivity (at least for me...ymmv). I suppose I'll look at elixir next...
Electric Vehicle Startup Nikola Motors is using Crystal to power their software in production
Not necessarily a ringing endorsement.
Well, not from a PR standpoint (and the author may not even be aware of what’s up with Nikola lately), but it’s used in production which is a huge positive.
> Nikola .. production
Well, that is the crust of the issue. Nikola has not really shown that they are not vaporware, and hence there really is no production.
The networking side of things still isn't mature.
A couple of years ago, I tried to write a parallel download app as my first Crystal app. It ended up as slow as the Ruby version. After some digging, I found out it was because the DNS resolver wasn't multithreaded.
Years later, the issue is still open.
https://github.com/crystal-lang/crystal/pull/2829
https://github.com/crystal-lang/crystal/pull/4236
It's still a good fit for CPU-bound workloads, and efficient background jobs and where I wouldn't want to install Ruby. Also seems be a good fit for AWS Lambda.
So, what’s the catch?
No windows support. (Seems to be progressing, though)
https://github.com/crystal-lang/crystal/issues/5430
You can track progress at
https://github.com/crystal-lang/crystal/issues/5430
But for many applications that's a non-issue.
The funny thing about the lack of "windows support" is that crystal is already good enough for windows programs which don't need networking (which is the only missing piece). Crystal can already work on Windows for use cases like gamedev (see crSFML), console apps, AI/ML etc.
Yet the Crystal team hasn't made a preview windows build which can enable all those use cases and the act of using Crystal on Windows is super convoluted as a result.
Compilation times can hurt too, I'm not sure if that's improved or being worked on though.
Crystal does global type inference to be performant while “feeling like a dynamic language” - as claimed by the article. This leads to very high compilation times (which is non-linear so a program double the size will likely take more than double the time to compile) and last time I asked I was told it is not a solvable problem.
> Crystal does global type inference
I think they stepped back from this a few years ago. It's not globally inferred anymore - you need to specify some types manually.
Yes. The basic rule is that things that could be stored on the heap will need to be explicit or inferrable directly from the constructors. But as method parameters generally (except sometimes, like in procs) don't need typing it is quite possible to build full programs using only global inferrence. But that is usually not how programs are built - that would do away with things like objects and structs and other constructs that are extremely ideomatic.
Does that overhead go down if you tag lots of places with types manually?
Yes but it is unlikely to make a difference unless a major part of ecosystem changes which is unlikely to happen as it will lose its “dynamic feeling”. Even the standard library has to be compiled when you compile your project.
There is some caching to help but times are still high. IIRC the compiler is not parallel either, so having more cores won't help.
There is also a —release compile flag that you would want to use for production and it is much slower.
Mildly offtopic but this is a thread most likely to have Crystal _and_ youtube-dl fans in it:
Invidious needs your help!
invidious[1] is an open alternative frontend to youtube with low bullshit, that works without js. It's written in Crystal. The core developer decided it was too much for them and has taken a (likely permanent) break from the project[2], and the community is struggling to maintain it[3] because there's very few people who have ever seen or heard of Crystal and like youtube-dl it's subject to breakage whenever google tweaks something on the youtube page. If you are a Crystal person and this is of interest to you, please help. For a bit more information on the way the code is structured and the current work that's happening, see [4]
[1]
https://github.com/iv-org/invidious
[2]
https://github.com/iv-org/invidious/issues/1320
[3]
https://github.com/iv-org/invidious/issues/1411
[4]
https://github.com/iv-org/invidious/pull/1399
Crystal is impressive work and the tools work seamlessly; it demonstrates how a statically compiled language can look when intelligent type inference is involved; wonder why a similar approach has not been taken by Python (well, Nim is a rather different language); the major point which concerns me is that the compiler get's rather slow when the codebase is large.
One of Ruby’s pillars is metaprogramming. Crystal lacks that entirely, making it a non-option for many Rubyists.
Crystal has compile-time metaprogramming, which is it's own thing and has many sensible uses... but yes it's not the metaprogramming people are used to in Ruby and that the entire Ruby ecosystem is pretty fundamentally built on.
Crystal has macros which is the metaprogramming for static/compiled languages. And last time I checked, there are way more things that you can do with Crystal macros than Rust's macros. And crystal's macros feel very familiar to the language itself, are easy to understand and use them whereas Rust's macros..
According to [1], Crystal Macros "receive AST nodes at compile-time and produce code that is pasted into a program." This is basically the same as Rust procedural macros[2]. You're probably thinking of "Macros by example"[3]
[1]
https://crystal-lang.org/reference/syntax_and_semantics/macr...
[2]
https://doc.rust-lang.org/reference/procedural-macros.html
[3]
https://doc.rust-lang.org/reference/macros-by-example.html
Being able to receive AST in crystal is only good. However have a look here [1], 90% of your macros are covered with those helpers. In Rust doing the same things are either more difficult or impossible. For instance, I wanted in rust to inject allow/warn kind of derives based in env var. Nope.
[1]
https://crystal-lang.org/reference/syntax_and_semantics/macr...
It's true the runtime meta programming is missing. Other comments mentioned some compile time options. But in my experience most Ruby doesn't really use metaprogramming for really hard cases. I've translated some larger modules to crystal recently and while there are magic things like activerecord out there, most metaprogramming is... simple. Often a glorified "I'd rather write a loop than repeat 3 similar method implementations". Outside of generic framework code I never ran into something meta which can't be trivially expanded to slightly repeating (and more readable) code.
Which is fine since the language is not really aimed at replacing Ruby (despite this blog post's headline)
There are a couple of points where Crystal really feels cleaner and somehow more expressive than Ruby.
The authors of the language avoided aliases (no more size vs. length or inject vs. reduce discussions) and generally there's only one way to do things (Strings are always wrapped in double quotes).
Crystal has abstract classes/modules/methods, generics and method overloading, three powerful techniques which are missing in Ruby entirely and which can be very helpful in some cases.
You can mark global methods as private and you can also mark classes as private. The latter can prevent users of your "shard" (gem) from accessing such classes, pretty much like how Rust modules can be marked for external use or not.
Constructor arguments can be turned into instance variables automatically, if you add `@` to the argument names.
Marking a construct as private happens inline with the definition, unlike in Ruby where it functions as a divider. This makes your code a bit easier to read and you can group methods in any way you like.
I find the built-in JSON, YAML and XML parsers to be very elegant. For the common use cases, you just have to define normal classes with attributes and include the JSON::Serializable module - this will also traverse attributes in search of serializable types. You can also do more complex transformations with annotations - have a look at
https://crystal-lang.org/api/0.35.1/JSON/Serializable.html
- but writing an HTTP client for some random API in Crystal feels very natural. On top of that, the built-in HTTP client is more than decent (which I can't say about Net:HTTP).
Generally the standard library is packed with goodies and, though the ecosystem is scarce in some areas, it's usually easy to replace and you can get away with less dependencies.
The inferred static typing which allows for union types is a neat choice - it doesn't force you to write types (most of the times) but it does give you some guarantees. I personally prefer to explicitly name my types, especially in public method definitions, because it helps document the code without having to write anything on top, but it's up to you.
To be honest I'm not really missing the metaprogramming aspect. In large Ruby projects this can easily turn into a mess and it makes searching through your codebase a horrid experience. Crystal does have macros but they have some limitations (they operate at compile-time).
Performance is just amazing and you can also build static binaries.
If you like Ruby, I'd really encourage you to give it a try, at least as an experiment to an "alternative Ruby".
To conclude, here are some code samples (shameless plug, but you can also browse
for other projects):
https://github.com/defense-cr/defense/blob/master/src/defens...
https://github.com/defense-cr/defense/blob/master/src/defens...
https://github.com/lipanski/kilometric/blob/master/src/kilom...
I have written almost the identical Scheme interpreters in Ruby and Crystal: [1] and [2]. The biggest difference I have felt between them is the absence of good old Object, which can represent everything at runtime, from Crystal. I had to declare Obj and Val:
class Obj end # Value operated by Scheme alias Val = Nil | Obj | Bool | String | Int32 | Float64 | BigInt
to define Cons Cell of Scheme:
# Cons cell class Cell < Obj include Enumerable(Val) getter car : Val # Head part of the cell property cdr : Val # Tail part of the cell def initialize(@car : Val, @cdr : Val) end ... end # Cell
Note that you see generics, Enumerable(Val), and constructor arguments with '@' in the excerpt above.
As for performance, Crystal is faster than Ruby 8.6 times as interpreter and 39.4 times as compiler [3]. You can use Crystal as a superfast (and typed) Ruby interpreter, in a sense.
[1]
https://github.com/nukata/little-scheme-in-ruby
[2]
https://github.com/nukata/little-scheme-in-crystal
[3]
https://github.com/nukata/little-scheme#performance
Anyone have production experience with Crystal? I know it's not 1.0 yet, but there are some fairly mature web frameworks available that make the language look pretty attractive.
I've build the backend of Base API, and the Mint programming language in it.
Base API runs on Heroku and it's memory consumption is really low (around 20Mb on average) also it's slug size is 3.3MB.
I think it's a really good language, the syntax is way more clearer then any of the other similar languages (Go, Rust, Nim).
It has two problems currently as far as I can tell:
- Windows support - when it hits there is no reason for me to use anything else, I'll be able to write desktop applications in it with a Webview or CLI apps
- Lack of mature libraries - in time I think this will remedy itself
Base API: www.base-api.io
Mint: www.mint-lang.com
At my previous job there was a need for a microservice that would compute a PhotoDNA hash of images and then compare that to a list of "known bad" material. Since this was very CPU-intensive we opted for Crystal instead of the usual Ruby that was used for almost everything else there. When I left it was churning away at ~500 images per second (over many cores, obv).
Development was pretty smooth and it was pretty easy to get new people onboarded since it look so much like Ruby. Some caveats though:
- If you want every last bit of performance, type signatures are very needed. Otherwise the compiler will still have to determine at runtime if the argument to a function is a uint32 or a uint64 for example.
- Library support is definitely lacking compared to Ruby. For example, AWS has an official SDK for Ruby with support for everything you can think of. Crystal has a 3rd party library with support for a few services. It's just not comparable.
not production yet, but if you are looking to make simple apis, you can just go with the stdlib. Like golang, the Crystal http stdlib module is good enough for use in your project. Here is my half done repo where I have used it for a repo
https://github.com/rishavs/noir
, if you are looking for a reference.
The major things you might struggle with;
* Lack of IDE support. The DevEx is not that great compared to the other languages.
Seeing as this is published in the LogRocket blog, is it being used by the company? Would be great to hear more about experiences in production.
Probably not, they have like hundreds of thousands similar articles, which they use as marketing and SEO for their product AKA content marketing.
I tried Crystal at the start of this year for an experiment and found the language is still in its infancy stage. Thou there are good things happening in their community but still a long road towards becoming a serious contender for switching.
Having said that, Crystal can definitely be considered for building micro-services to delegate more resource intensive tasks.