Crystal
Ruby-like syntax, C-like speed.
Crystal is a compiled language with syntax inspired by Ruby. It infers types at compile time, so you get readable code with the speed of a native binary and none of the null-pointer surprises. It has built-in concurrency with fibers and channels.
Why learn Crystal?
Crystal suits developers who love Ruby's readability but need compiled performance.
- Readable: feels like Ruby.
- Fast: compiles to efficient native code.
- Safe: the compiler catches nil errors and type mismatches.
Set up and run your first program
Install Crystal from crystal-lang.org (Linux, macOS; Windows support is growing). Shards is its package manager.
crystal --version
crystal run hello.cr
crystal build --release hello.crname = "Ada"
puts "Hello, AI! Welcome to Crystal, #{name}."#{} inserts values into strings, like Ruby.Variables and types
Types are inferred, but you can write them. A variable can hold a union type such as String | Nil, and the compiler makes you handle both.
layers = 32
params = 8.03e9
model : String = "llama3.1"
puts "#{model}: #{layers} layers, #{(params * 2 / 1e9).round(1)} GB"
puts typeof(layers), typeof(params)
maybe = ENV["MODEL"]? # String | Nil
puts maybe || "default model"Arrays, hashes and blocks
Arrays and hashes have Ruby-style methods that take blocks.
gpus = ["H100", "H200", "B200"]
vram = {"H100" => 80, "H200" => 141, "RTX 5090" => 32}
gpus.each_with_index { |g, i| puts "#{i}: #{g}" }
big = vram.select { |_, gb| gb >= 80 }.keys
puts big
puts vram.values.sum
puts (1..5).map { |x| x * x }Methods and control flow
Methods can have type restrictions and default values. case matches values and ranges.
def params(layers : Int32, width : Int64 = 4096_i64) : Int64
12_i64 * layers * width * width
end
def precision(bits)
case bits
when 32 then "FP32"
when 16 then "FP16"
when 1..8 then "low precision"
else "other"
end
end
puts params(32)
puts precision(8)Classes and structs
Classes are reference types, structs are value types. property creates getters and setters.
class Gpu
property name : String
property vram : Int32
def initialize(@name, @vram)
end
def fits?(gb)
gb <= vram
end
end
g = Gpu.new("RTX 5090", 32)
puts "#{g.name} fits 16 GB: #{g.fits?(16)}"Fibers and channels
spawn starts a lightweight fiber; channels pass values between fibers, ideal for concurrent network requests.
ch = Channel(String).new
models = ["llama", "qwen", "mistral"]
models.each do |m|
spawn do
sleep 0.1.seconds # pretend to call a model
ch.send "#{m} done"
end
end
models.size.times { puts ch.receive }AI with Crystal
Crystal is a good fit for fast AI-adjacent services: API gateways, data preprocessing, scrapers and CLIs that call LLMs. The standard library includes an HTTP client and JSON support, and C bindings can reach libraries such as llama.cpp.
The essential libraries
Concurrent prompts to a local model
Send several prompts at once using fibers and gather the answers.
require "http/client"
require "json"
def ask(prompt : String) : String
body = {model: "llama3.1", messages: [{role: "user", content: prompt}]}.to_json
res = HTTP::Client.post("http://localhost:11434/v1/chat/completions",
headers: HTTP::Headers{"Content-Type" => "application/json"}, body: body)
JSON.parse(res.body)["choices"][0]["message"]["content"].as_s
end
prompts = ["What is VRAM?", "What is a tensor core?", "What is an AI agent?"]
ch = Channel({String, String}).new
prompts.each { |p| spawn { ch.send({p, ask(p)}) } }
prompts.size.times do
q, a = ch.receive
puts "Q: #{q}\nA: #{a}\n\n"
endBuild an AI agent in Crystal
Crystal's JSON::Any makes it easy to read the model's response and run a tool-calling loop.
require "http/client"
require "json"
URL = "http://localhost:11434/v1/chat/completions"
TOOLS = JSON.parse(%([{"type":"function","function":{"name":"get_weather","description":"Weather for a city",
"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}}]))
def get_weather(city : String) : String
"It is 24°C and sunny in #{city}."
end
messages = [JSON.parse(%({"role":"user","content":"Should I take an umbrella in Lahore today?"}))]
5.times do
body = {model: "llama3.1", messages: messages, tools: TOOLS}.to_json
res = HTTP::Client.post(URL, headers: HTTP::Headers{"Content-Type" => "application/json"}, body: body)
msg = JSON.parse(res.body)["choices"][0]["message"]
messages << msg
calls = msg["tool_calls"]?.try(&.as_a?) || [] of JSON::Any
if calls.empty?
puts msg["content"].as_s
break
end
calls.each do |c|
args = JSON.parse(c["function"]["arguments"].as_s)
reply = {role: "tool", tool_call_id: c["id"].as_s, content: get_weather(args["city"].as_s)}
messages << JSON.parse(reply.to_json)
end
end%( ... ) is a string literal that can contain double quotes, handy for inline JSON.
ollama pull llama3.1, and keep it running. The code talks to its OpenAI-compatible API at http://localhost:11434/v1, so the same program also works with vLLM, llama.cpp server or a cloud API by changing the URL, model name and adding an API key.Crystal learning roadmap
Tick items off as you go. Your progress is saved in this browser.
Beginner
Intermediate
Advanced
Projects to build
Crystal cheat sheet
| Task | Syntax |
|---|---|
| Interpolation | "Hi #{name}" |
| Method | def add(a, b) a + b end |
| Block | arr.map { |x| x * 2 } |
| Nil check | if x = maybe; ... end |
| Class | class P; property x : Int32; end |
| Fiber | spawn { ... } |
Common mistakes
- Ruby gems do not work in Crystal; use shards.
- Compile times grow with large programs because of global type inference.
- Handle
Nilexplicitly: methods likeENV[]?return a union type.
Where to go next
- Crystal reference — official
- Crystal API docs — standard library
- Shards — packages
On TensorCore.si: Ruby volume, Go volume.
Keyboard: [ previous language · ] next language · / search the library