English for Crystal Language Developers
Vocabulary for developers using Crystal — compile-time type inference, macros, the fiber-based concurrency model, and shard dependency talk for teams shipping fast, Ruby-like binaries.
Crystal reads like Ruby but compiles like a systems language, and that combination creates a specific vocabulary gap: developers coming from Ruby have the syntax vocabulary already, but not the compile-time and concurrency vocabulary Crystal actually requires to discuss its behavior precisely.
Key Vocabulary
Type inference (compile-time) — Crystal’s process of determining every variable’s type at compile time without explicit annotations in most cases, which is why Crystal code looks untyped like Ruby but behaves like a statically typed compiled language. “You don’t need to annotate that return type — type inference already resolves it at compile time from the two branches, and the compiler will reject the build if they don’t unify into one consistent type.”
Union type — a type representing “one of several possible types,” which Crystal’s compiler tracks precisely so that every downstream operation is verified against all members of the union, not just the common case.
“This variable ends up with a union type of String | Nil because of that early return — the compiler’s forcing you to handle the nil case explicitly before you can call a string method on it.”
Macro (compile-time) — Crystal code that runs during compilation to generate other code, distinct from a runtime function call, used for eliminating boilerplate without paying any runtime cost. “We’re using a macro to generate the getter and setter methods for all twelve fields at compile time, instead of hand-writing them or reflecting on the object at runtime the way Ruby would.”
Fiber — Crystal’s lightweight, cooperatively scheduled unit of concurrency, similar to a goroutine, that lets thousands of concurrent tasks run without the overhead of OS threads. “Don’t spawn an OS thread for each incoming connection — spawn a fiber instead. Crystal’s scheduler multiplexes thousands of fibers onto a small number of OS threads for you.”
Shard — Crystal’s term for a package/dependency, managed via shard.yml and the shards command, analogous to a Ruby gem but resolved and compiled statically into the final binary.
“Pin that shard to an exact version in shard.yml — since Crystal compiles dependencies directly into the binary, an unpinned shard update can change runtime behavior on your next build, not just your next bundle install.”
Common Phrases
- “Does type inference resolve that at compile time, or do we need an explicit annotation here?”
- “Is this a union type we need to narrow, or does the compiler already know it’s just one type?”
- “Is that a compile-time macro, or a regular runtime method call?”
- “Should this spawn a fiber, or does it actually need a real OS thread?”
- “Is that shard pinned to an exact version in shard.yml?”
Example Sentences
Explaining a compiler error to a teammate:
“The compiler’s complaining because this ends up with a union type of Int32 | String, and you’re calling a method that only exists on Int32. You need to narrow the union with a type check before calling it.”
Describing a performance choice: “We’re using macros to generate the serialization methods at compile time instead of using runtime reflection — Crystal doesn’t do runtime reflection the way Ruby does, so this is genuinely the idiomatic approach here, not just an optimization.”
Explaining a concurrency design: “Each incoming request spawns its own fiber, and Crystal’s scheduler handles multiplexing them onto a handful of OS threads — that’s why we can handle tens of thousands of concurrent connections without the memory overhead of one thread per connection.”
Professional Tips
- Let type inference do the work in most cases, and only add explicit type annotations at public method boundaries, where they double as documentation for callers — over-annotating internal code just adds noise.
- Treat a union type the compiler reports as useful information, not an obstacle — narrow it explicitly with a type check or
caserather than reaching for an unsafe cast to silence the compiler. - Reach for a compile-time macro only when you’re eliminating genuine boilerplate across many similar definitions — macros are harder to debug than plain methods, so the payoff needs to be real.
- Default to spawning a fiber for concurrent work rather than an OS thread — fibers are Crystal’s idiomatic concurrency primitive, and mixing in real threads should be a deliberate, justified exception.
- Always pin a shard to an exact version for anything shipping to production, since Crystal statically compiles dependencies into the binary — an unpinned version means your next build can silently change behavior.
Practice Exercise
- Explain the difference between compile-time type inference and a runtime type check.
- Describe when you’d reach for a compile-time macro instead of a regular method.
- Write a sentence explaining why fibers are preferred over OS threads for concurrent I/O in Crystal.