Crystal: Ruby-Inspired Compiled Language
Crystal is a compiled language with Ruby-like syntax, global type inference, and native performance via LLVM. It feels like Ruby — readable, expressive, developer-friendly — but compiles to a single static binary that runs as fast as C code.
In this tutorial, you’ll learn Crystal’s Ruby-inspired syntax, type inference, concurrency with fibers, C bindings, popular web frameworks (Amber, Lucky), and how Crystal compiles to native executables.
What You’ll Learn
- Ruby-like syntax with static typing
- Global type inference — no type annotations needed
- Compilation to native binary via LLVM
- Concurrency with fibers and channels
- C bindings without a FFI library
- Crystal web frameworks: Amber, Lucky, Kemal
Why Crystal Matters
Crystal targets the same sweet spot as Go: the performance of a compiled language with the productivity of a dynamic one. It’s used for web services, CLI tools, and high-performance APIs. At DodaTech, Crystal’s type inference and binary compilation make it ideal for DodaZIP’s command-line tools.
Learning Path
flowchart LR
A[Ruby Basics] --> B[Crystal Basics<br/>You are here]
B --> C[Type Inference]
C --> D[Fibers & Concurrency]
D --> E[Crystal Web Apps]
style B fill:#f90,color:#fff
Crystal Syntax
If you know Ruby, you already know Crystal syntax:
# hello.cr
puts "Hello, Crystal!"
# Variables — type inferred
name = "Alice"
age = 30
height = 1.75
# String interpolation
puts "Name: #{name}, Age: #{age}"
# Everything is an object
puts "hello".upcase # HELLO
puts 42.to_s # "42"
# Methods
def greet(name : String)
"Hello, #{name}!"
end
puts greet("Bob") # Hello, Bob!
# Blocks
3.times { |i| puts "Iteration #{i}" }
# Iteration 0
# Iteration 1
# Iteration 2Expected output: The Crystal syntax is nearly identical to Ruby. Key difference: Crystal requires type annotations for method parameters in public APIs.
Type Inference
Crystal infers types globally — no annotations needed for local variables:
# Type inference — Crystal tracks every type
def process(x)
if x.is_a?(Int32)
x * 2 # Infers: Int32
elsif x.is_a?(String)
x.upcase # Infers: String
end
end
puts process(42) # 84
puts process("hello") # HELLO
# Union types — variable can be one of several types
value = rand > 0.5 ? "text" : 42
# value is Int32 | String (union type)
puts typeof(value) # Int32 | String
# Case expression type narrowing
case value
when String
puts value.upcase # Crystal knows it's a String here
when Int32
puts value * 2 # Crystal knows it's an Int32 here
endExpected behavior: process returns either Int32 or String. Crystal tracks union types through all code paths. Accessing a union type requires type narrowing via is_a? or case.
Compilation to Native Binary
Compile and run in one step:
crystal run hello.cr # Compile + run (cached)
crystal build hello.cr # Produce static binary
./hello # 1.2MB binary, immediate startup# Build a production binary
# crystal build --release hello.cr
# Stripped binary: ~300KB for a simple program
puts "Hello from a native binary!"Expected behavior: crystal run compiles and executes. crystal build produces a standalone binary with zero dependencies.
Concurrency with Fibers
Crystal fibers are lightweight concurrency primitives — like goroutines but with channels:
# Spawn fibers and communicate via channels
channel = Channel(String).new
# Fiber 1
spawn do
sleep 0.5.seconds
channel.send("Hello from fiber 1")
end
# Fiber 2
spawn do
sleep 0.3.seconds
channel.send("Hello from fiber 2")
end
# Receive in order they complete
2.times do
msg = channel.receive
puts msg
endExpected output: Hello from fiber 2 (0.3s) then Hello from fiber 1 (0.5s). The second fiber finishes first despite being spawned second.
C Bindings
Call C libraries directly with Crystal’s lib syntax:
# Bind to libc's printf
lib LibC
fun printf(format : UInt8*, ...) : Int32
end
LibC.printf("Hello from C: %d\n", 42)
# Bind to SQLite
@[Link("sqlite3")]
lib SQLite3
fun open(path : UInt8*, db : Void**) : Int32
fun close(db : Void*) : Int32
end
db = Pointer(Void).malloc(1)
SQLite3.open("test.db", pointerof(db))
puts "Database opened!"
SQLite3.close(db)Expected behavior: Crystal links directly to shared libraries. No FFI wrapper needed — the lib block generates bindings automatically.
Web Frameworks
Crystal has several web frameworks:
| Framework | Style | Features |
|---|---|---|
| Kemal | Sinatra-like | Simple routing, middleware, WebSocket |
| Amber | Rails-like | Full-stack, ORM, templates, WebSocket |
| Lucky | Type-safe | Compile-time checks, strong typing |
| Athena | DI-based | Dependency injection, testing |
# Kemal — minimal web server
require "kemal"
get "/" do
"Hello, Crystal!"
end
get "/hello/:name" do |env|
name = env.params.url["name"]
"Hello, #{name}!"
end
Kemal.runExpected behavior: crystal run starts a web server on port 3000. Visiting http://localhost:3000/ shows “Hello, Crystal!”. Visiting /hello/Alice shows “Hello, Alice!”.
Common Mistakes
1. Not Using Type Annotations for Public Methods
Crystal requires type annotations for public method parameters. Private methods can infer types from their callers.
2. Forgetting That Nil Is a Type
String? means String | Nil. You must check for nil before using the value. Crystal won’t compile if you don’t.
3. Confusing spawn with fork
spawn creates a fiber (lightweight, cooperative). It does NOT create an OS thread or process.
4. Blocking the Event Loop
Crystal uses an event loop for I/O. Long synchronous operations block all fibers. Use spawn for CPU-heavy work.
5. Not Using --release for Production
Debug builds are slower and larger. Always compile with --release for production deployment.
6. Ignoring Crystal’s Formatter
Run crystal tool format to auto-format your code. Crystal’s style is enforced, not debated.
Practice Questions
1. How does Crystal achieve type safety without type annotations?
Crystal uses global type inference. It traces all code paths and infers types at compile time.
2. What is a fiber in Crystal?
A lightweight unit of concurrency. Fibers are cooperative (not preemptive) and multiplexed onto OS threads.
3. How do you call a C function from Crystal?
Use a lib block to declare the function signature. Crystal generates bindings automatically.
4. What’s the difference between Kemal and Amber?
Kemal is minimal (like Sinatra). Amber is full-stack (like Rails) with ORM, authentication, and CLI generators.
5. Challenge: Build a concurrent URL checker.
Spawn fibers to check multiple URLs concurrently. Print each URL’s HTTP status code. Use a Channel to collect results.
Mini Project: HTTP Status Checker
require "http/client"
def check_url(url : String, channel : Channel(String))
response = HTTP::Client.get(url)
channel.send("#{url}: #{response.status_code}")
end
urls = ["https://crystal-lang.org", "https://github.com", "https://google.com"]
channel = Channel(String).new
urls.each { |url| spawn { check_url(url, channel) } }
urls.size.times { puts channel.receive }FAQ
What’s Next
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro