Skip to content
Volume C · The TensorCore.si Library

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.

Prologue

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.
Prologue

Set up and run your first program

Install Crystal from crystal-lang.org (Linux, macOS; Windows support is growing). Shards is its package manager.

terminal
crystal --version
crystal run hello.cr
crystal build --release hello.cr
hello.cr
name = "Ada"
puts "Hello, AI! Welcome to Crystal, #{name}."
Tip#{} inserts values into strings, like Ruby.
Chapter 1

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.

variables.cr
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"
Chapter 2

Arrays, hashes and blocks

Arrays and hashes have Ruby-style methods that take blocks.

collections.cr
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 }
Chapter 3

Methods and control flow

Methods can have type restrictions and default values. case matches values and ranges.

methods.cr
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)
Chapter 4

Classes and structs

Classes are reference types, structs are value types. property creates getters and setters.

classes.cr
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)}"
Chapter 5

Fibers and channels

spawn starts a lightweight fiber; channels pass values between fibers, ideal for concurrent network requests.

fibers.cr
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 }
Part II · AI with Crystal

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

HTTP::ClientBuilt-in HTTP client.
JSONBuilt-in JSON parsing and serialisation.
KemalFast web framework.
C bindings (lib)Call C libraries directly.

Concurrent prompts to a local model

Send several prompts at once using fibers and gather the answers.

concurrent.cr
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"
end
Part III · Build an AI agent

Build an AI agent in Crystal

Crystal's JSON::Any makes it easy to read the model's response and run a tool-calling loop.

Your goal→Model plans→Tool call→Your code runs the tool→Result back to model→Final answer
agent.cr
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.

Run it locallyInstall Ollama, run 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.
Part IV · Your path

Crystal learning roadmap

Tick items off as you go. Your progress is saved in this browser.

Beginner

Intermediate

Advanced

Part IV · Your path

Projects to build

Log parserSummarise large log files quickly.
Web APIA JSON API with Kemal.
LLM CLIA command-line chat tool for a local model.
Reference

Crystal cheat sheet

TaskSyntax
Interpolation"Hi #{name}"
Methoddef add(a, b) a + b end
Blockarr.map { |x| x * 2 }
Nil checkif x = maybe; ... end
Classclass P; property x : Int32; end
Fiberspawn { ... }
Reference

Common mistakes

  • Ruby gems do not work in Crystal; use shards.
  • Compile times grow with large programs because of global type inference.
  • Handle Nil explicitly: methods like ENV[]? return a union type.
Reference

Where to go next

On TensorCore.si: Ruby volume, Go volume.

Keyboard: [ previous language · ] next language · / search the library