So ruby fans want Crystal to get more publicity and python fans want Nim to get more.
Has anyone used both in any serious project, and if so can you compare? I've spent a tiny amount of time with Nim but found documentation, training videos, books etc extremely thin on the ground. Lots of Nim projects on github are ancient and haven't been updated in years. Chat GPT never gave me working code. How is Crystal in this regard? How would you compare their std libraries and available modules?
I read that during the first run, it will be slower because of the crystal code being compiled. Is the compiled code stored somewhere?
I am asking this because in that case, can’t I run it myself and share the code with the precompiled parts so others don’t have to experience the slow start?
Yes that appears to be the case. If you run the single file example in the README it will add a directory called "crystalruby" with generated source code in it.
So it looks like you could easily check that in with your gem.
I don't actually see any object code in the directory but this is a new project so they may not cache that yet but I don't see why they couldn't in future. e.g. as a build step when installing a gem.
This seems to be a fantastic bit of work so far with a lot of potential for the future.
Conceptually, isn’t that what a JIT compiler does?
Crystalruby ia really interesting though. This has got me wondering:
1. How this compares to Sorbet Compiler in terms of performance?
2. If you could use Sorbet or RBS type annotations to drive crystalruby?
3. How would YJIT perform if it was fed with type annotations?
4. Could crystalruby type signatures be used by Sorbet-lsp, ruby-lsp, etc in to improve developer experience?
I really wish we had a single standard for inline types in ruby. I wish that was an inline RBS type annotation but I guess I’d take anything that works.
It would definitely be possible, but I would really hate to see coupling like that added to ruby. I also worry that it would make it too magical and difficult to figure out what code is running, where and when. Debugging an issue deep in the bowels of that is a scary thought.
How recent were your experiences with this? In recent years I've seen monkeypatching treated as more of an antipattern. If you are on a project where people are doing that without very good reasons, that says more about the team than the language.
It is still prevalent enough that when developing Ruby JITs it can be worthwhile delaying compilation until the framework has started up and finished invalidating all your method lookups, or changing your invalidation strategy to handle new methods being defined on Object without invalidating all your inline caches.
Monkeypatching was common a decade ago. Now it’s more limited to things that try to be language or framework extensions, like ActiveSupport or gems extending ActiveRecord. The whole point of those is to deeply integrate.
Metaprogramming like looping over an array to dynamic create methods is far less common. It definitely makes me cranky when I can’t find a method definition. ORMs are one of a few exceptions where the utility justifies it.
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.
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.
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.
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.
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).
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.
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.
I hacked this together last weekend to scratch an itch and have some fun. This got a lot more attention than I was expecting so early on.
I've had to scramble a bit to start hammering this into something that's less prototype, more legitimate project. I hope it's now close to a cohesive enough state that someone trying it out will have a relatively smooth experience.
Given the interest, I definitely plan to sink a bit more dedicated time into this project,
to tick off a few key missing features and add some polish.
It would be great to see it grow into something that can be useful to more than just me.
Seems like there's definitely some shared desire for this type of tool.
It probably goes without saying that you probably shouldn't convert large amounts of mission critical code to depend on this (for now).
It's still early days and the API hasn't been "crystallized" yet...
Yes the idea isn't terribly new, but this implementation is significantly more elegant (and ruby-ish) thanks to what looks to be a great design and the capabilities of Crystal. rubyinline is neat but honestly not something I'd seriously consider using in a production project. I need to try out crystalruby before I have an opinion on that, but it looks very promising!
Install the Rubocop extension in your editor and it'll bug you about it every time you have a big int (kind of annoying when you're doing one-off scripts and embed "magic numbers" like IDs from the database lol)
A lot of languages have added support for this over the last few years. It's a nice little quality of life improvement. I use it very regularly now. I find it really handy for currency amounts in cents, so I can say `10_00` for ten dollars, in cents.
Sorry, it's a been feature in a number of languages for decades. Perl has it at least as early as 5.005 in 1998. So naturally, Ruby had it since at least before 1.6 / 2002. Java added it in 2011. It didn't work with Python 2.1 from 2001, but I found it was added relatively late in PEP 515 in 2016 with Python 3.6.
As a latecomer, C23 went with ' just like C++ did almost 10 years earlier with C++14.
Awesome! I’ve been waiting to see if someone would eventually do this. With further refinement this could be a big deal for Ruby. No more “Ruby is too slow”, when you can Crystalize your bottlenecks.
One feature it seems to lack is the ability to crystalize instance methods?