What would you say is "Idiomatic Crystal"?

Hello folks,

For languages like Ruby we had lot of books talking about design patterns applied, or what OOP looks like (excellent resources like Sandi Metz’s POODR)

However, outside of Crystal Programming one, we don’t have a lot of resources that talks about what a good design in Crystal looks like, right?

I’m wondering if some of you can provide references what are those that you see are good patterns or design decisions (outside of Crystal’s own stdlib codebase).

Thank you!

For me idiomatic crystal is add types everywhere, for every function header, because after some time you stop understands code. look at the example: crystal/src/compiler/crystal/codegen/call.cr at master · crystal-lang/crystal · GitHub you won’t understand what these arguments are or what their types are, and because of that, it’s also hard to understand what the function does. Also this is helps LLM better read code.

Recently, I have been using this prompt:

“Process the data as a stream instead of loading it all into memory.”

Crystal makes it easy to write programs that run close to C speed.
But writing a program in Crystal does not always reduce memory use.

When I ask AI to process the data as a stream, memory use often drops.

Many differences between Ruby and Crystal are related to memory allocation.

String.build do |io|
  File.each_line("input.txt") do |line|
    io << line.upcase
  end
end
to_s(io)

These patterns avoid unnecessary intermediate strings by writing data to an output buffer as it is processed.

I am not a professional software engineer, so my view may be different.
Still, streaming seems to come up often in the Crystal code and discussions.

For example: User-space pipe implementation

I don’t think there’s necessarily one form of idiomatic code. It depends on what you try to implement.

Taking the example for upcasing a file’s content from the previous comment.
I’d say that’s idiomatic. It’s a medium optimization improving efficiency for some forms of input data.

A single step conversion is also idiomatic: File.read("intput.txt").upcase.
That’s much more concise. But less optimized, of course. Optimization is not always useful. When the input is known to be small, this is entirely fine.

The impact of optimization also depends heavily on the use case. The one-shot might even be more efficient than iterating per line for some forms of input data because we only need exactly two string allocations of known size.

On the other end of the spectrum, an even more efficient implementation would read fixed-size chunks of bytes, instead of lines, and convert them. Another idiomatic form. This wouldn’t even need any intermediary allocations. But it’s much more complexity. So it only pays off if optimizing this code path has a significant effect.

I think one idiomatic treat of Crystal is to provide a good base implementation that works well for 80% of use cases and is easy to use.
If you need to go deeper because you need more performance, you can.

Crystal tries to avoid different way’s to do the same thing. As long as these ways are similar. But sometimes it’s good to have a choice between different behaviour characteristics. It’s usually a trade-off between code complexity and performance.

This is the beauty of Crystal.

You have a super easy syntax that is more productive than Javascript. This means super fast prototyping and validation of ideas. *

On the other hand, you can literally go as deep as you want, when needed, and compete with C and Rust if it makes sense in your scenario.

Sorry guys, I cannot recommend Crystal enough.

** I say this after working with Javascript for more than 23 years.

tl;dr

Try not to over optimize things. Instead: make it work, make it right, make it fast (if you need to).

Focus on making your idea to work. Understand it. And about the “make it right” part? There is no way to improve something that doesn’t exist.

Create first. Make it happen. Improve later.


not so tl;dr

Then, when you create, the experience grows. You will have “god classes” (bad, but works), then you will split them. Then you will not listen to the advice of not over optimizing. And will move things from the heap to the stack. Them you will understand that, many times, this is the wrong thing to do.

Design patterns? Learn them not as “laws”. The pain creates the necessity. Only apply a pattern when it actually makes sense.

Having the perfect code that doesn’t deliver any value to no one, means nothing.

Having a not perfect code (that you understand) but which delivers value for its users, means everything.

Solve real necessities. Software is not the end goal. It is a tool that helps achieving it.

And Crystal is really good to achieve goals in a productive and efficient way.

For me, “idiomatic” is pretty situational. For example, I think a lot of folks focusing on app development may not consider code that looks like this idiomatic, but they probably think the code that it lets them write is. And sometimes you need unidiomatic implementations, but you probably want to stick to idiomatic interfaces whenever feasible.

It’s like how idiomatic Rust code is often considered provably safe, but even the safest Rust code usually depends on Rust code that’s explicitly marked unsafe, even if not directly. It’s all about how you compartmentalize the code that deviates from established norms.

I’m not sure this really answers the question, tbh, but it’s something I keep in mind when writing Crystal code.