Hacker News new | ask | show | jobs
by edvinbesic 803 days ago
What is the current state of crystal toolchain? Last time I used it I really enjoyed the language but the compilation times were a bit of a turn off.
4 comments

You won't ever see fast compilation times with it, and here's why:

> type annotations are rarely necessary, thanks to powerful type inference. This keeps the code clean and feels like a dynamic language.[1]

I prefer Sorbet[2] for Ruby, which is pretty fast and a reasonable compromise. People complain about the verbosity, but you can't have both speed and type inference. Pick one.

Some of the verbosity in Sorbet, however, comes as an indirect result of an underdeveloped Generic type system (the rest of the parts work great though). Some of the verbosity comes from lack of fluency with the tool, which gets better with practice, and doing research. The bottom line is I often think its a skill issue when people say it leads to code that is too verbose.

1. https://crystal-lang.org/#type_system 2. https://sorbet.org/

> You won't ever see fast compilation times with it, and here's why:

> > type annotations are rarely necessary, thanks to powerful type inference. This keeps the code clean and feels like a dynamic language.[1]

There are fast type inference implementations. For example, the OCaml compiler is pretty fast

No reason Crystal couldn't improve this part of compile time in future

I suspect that doesn't hold true for some larger projects (10K+ LOC) with no annotations but point taken.

My experience with F# (which was way back in version 2.0, but based on OCaml which does type inference very similarly), was that the inference would become slower as the size of the project increased, eventually taking multiple seconds. I eventually adopted a habit of adding type annotations to commonly-used functions because it mitigated the problem. These projects were not huge by any means (5-10K LOC).

There is probably a good compromise to be found here. It should be possible for a tool to suggest areas to add annotations where it would do the most good for performance. But maybe those are non issues with modern implementations.

Adding type annotations in OCaml never reduce the typechecking time: it adds more information for the typechecker to process and it can only increase the size of type. Typechecking time is proportional to the size of types but those tends to stay constants and small inside normal project. I think that F# should have a similar behaviour?
Kotlin strikes a good middle ground on the fast compilation <-> inference continuum IMO. within function bodies you rarely need to think about the typing system. Function return types can be inferred for expression bodies and star projections can make some of the more tricky parts of generics more tractable.

Inference is a big part of what increases Kotlin ergo over Java and while it does compile slower you are going from a very fast base so it doesn't feel slow relative to other options.

> but you can't have both speed and type inference. Pick one.

With OCaml you can pick two :)

But then Id have to use Ocaml. ;)
Well, there's Idris which is eager fp like OCaml but looks like Haskell and has dependent typing.
what annoys me to no end with sorbet is that it subtly pushes to worse looking code, even if to ignore all the verbosity and annotations.

let's say there's a generic data transformation in a method which could be extracted to a private helper method. Sorbet didn't have any issue figuring types of the result while those lines were in the parent method. But when those three lines are extracted, sorbet now requires you to write a generic signature for that method, otherwise type checking is not working anymore. Generic signatures are even more verbose and hard to write than for methods with specific types.

So, a kneejerk reaction would be just not extract those, because it becomes too much work, and readability is not improved anymore, because signature for three lines of code is taking five lines. And that's until someone "clever" will add a rule to rubocop limiting all methods to a specific amount of lines indiscriminately, so I'd spend additional work time imagining that person being punched in a face.

In theory something like that could be solved by marking helper methods as inlined, basically promising that you're not going to use them in subclasses or whatever and allowing the same inference to work inside. But "sorbet is feature complete".

Sorbet is not feature complete, we are working on it every day!

I hear you on the verbosity of generic type declarations. It's something that I pushed hard for us to improve in the early phases of the project, but I was outvoted by other members of my team. But... at this point those members have all left the team and in the meantime we've heart actual users complain about the verbosity (not our own hypothetical "what if" complaints in the design phase) so I'm optimistic we'll be able to reduce generic type verbosity in the future. For example, a while back I did a prototype/experiment to drastically drop the verbosity of generic type annotations[1]. It's definitely on our radar.

Happy to chat more either on Sorbet's issue tracker or Slack group.

[1] https://github.com/sorbet/sorbet/pull/7322

That's actually reassuring to hear, thank you. I'll take a look on recent changes in sorbet, I don't even remember where I've seen that "feature complete" remark, maybe I'm just imagined it.

I'd love and use sorbet if it'd be truly an optional typing, I'd happily supply signatures to public interfaces of my classes, but only to them, as a documentation and a somewhat verifiable helper to LSP. Basically, what RBS should be doing. In my experience I rarely if ever make type-like errors in the code I produce, so sorbet just added a lot of chore and forced to dumb down my code significantly, just to be ingestable by sorbet. And worse of all, it added some false sense of security, because absense of sorbet errors didn't mean absense of type mismatches in many cases. But maybe some of those concerns are gone already.

Could not in theory the stdlib and most used libraries be heavily type annotated to assist the compiler?
The compiler could grow a cache of sorts, analysing "bubbles" of code which does not touch other bubbles and keep type info (and compilation output!).
Do you have some examples of programs using generics in Sorbet you would prefer worked better or differently?
I'll have to dig deeper into it to put together some sorbet.run code and identify which issues are the most serious. I'll share with the community group whenever I have things worth posting.

That said, in general, I often end up rewriting things a different way if something doesn't work, and the vast majority of the time that is good enough for me. But it takes time to find the one solution that doesn't create unacceptable bloat or complexity, and even then it is not as simple as I think it could be if there was another layer of polish to the Sorbet ergonomics (don't get me wrong, it's already in a good spot considering where Ruby was before that). Though someone actually complained to my boss that I spend too much time getting things working with Sorbet instead of "just shipping it", even though one of my main areas of responsibility is with architecture.

This is where having redundant features (multiple ways to accomplish the same thing) could be of some benefit, but I'll have to think about it some more with specifics. And I understand from looking through the repository that it's not an easy task to add features at all, so I'm thankful enough for what it is there as it has made my job much easier overall.

The main thing I struggle with regarding generics is the scenario where you must have separate type_template and type_member for class methods vs instance methods, and there are many use cases where these values are equal. But there is no way to tell sorbet that they are equal, requiring casts which makes the code less readable and often frustrating to write.

I also think that on a broader level, based on my searches, that the ruby community criticizes the idea of Sorbet for the reasons I mentioned in my previous comment. Adding more polish to the ergonomics, even when difficult or seemingly unreasonable, could do more for the branding of the project, which would lead to more community adoption, which would lead to better "just-works" type support with many common ruby libraries that are currently untyped. That could be a massive improvement to the status quo IMO. But it's hard to say because it could also end up not really moving the needle...

Appreciate all the work you've done! C++ isn't my strong point but someday I hope to find time to do a deep dive on the internals of the project.

> The main thing I struggle with regarding generics is the scenario where you must have separate type_template and type_member for class methods vs instance methods, and there are many use cases where these values are equal.

Makes sense, this is a big one for us too. For example, we'd like to do a better job of typing the T::Enum `serialize` and `deserialize` methods, but that's blocked on having a better mechanism for type_members that are equivalent to type_templates. I have done some prototypes to build this feature (you can find them in my draft PRs) but none of the solutions I arrived at were particularly satisfying. It's on our list to fix for sure.

> Adding more polish to the ergonomics [...] would lead to better "just-works" type support

People mean a lot of different things by this—I'd love to hear more about what kinds of ergonomic improvements would help. For example, some people say ergonomics to mean only tooling improvements, while others mean something else (e.g. type system improvements).

Mega thread that has been discussing this for a year: https://forum.crystal-lang.org/t/incremental-compilation-exp...
Tried crystal several times over the years. Each time, I encountered Crystal bugs, surprises, oddities, and missing pieces such that there was no viable path forward to adopt it for anything serious. Elixir + Rust with rustler is pretty compelling as a scalable, viable alternative.
Why does compile times matter? 4 minutes? 4 seconds? Who cares?
I have to assume you're trolling, but if not, the reason compile times matter is because iteration time is important.

Having to wait longer to see the results of one's work results in loss of focus and productivity.

Yeah I know what time is. My point is, this is 2024 and computers are insanely powerful now. Compilation times can't be that bad.
For large projects using complex compiled languages like Rust, C++, and ostensibly according to this post, Crystal, yes, they are bad.

I have worked on several projects where a full clean compile of the project takes 30+ minutes, and even incremental compiles and links to a large module will take 60-90s at absolute minimum because linker times are still shitty - and this is even with modern, powerful hardware, think 32 or 64 hyperthread Ryzen CPUs, not just your typical dev laptop.

It may not seem like much, but 60-90s is definitely enough to get you distracted and out of the zone.