Getting started
Introduction
Crystal is a statically-typed, compiled language with Ruby-like syntax. It compiles to native code via LLVM and offers performance comparable to C while maintaining high-level syntax.
Installation
# macOS (Homebrew)
brew install crystal
# Linux (Debian/Ubuntu)
curl -fsSL https://crystal-lang.org/install.sh | bash
# Check version
crystal --version
Hello World
# hello.cr
puts "Hello, World!"
# Type inference
message = "Crystal" # String
number = 42 # Int32
# Explicit typing
name : String = "Alice"
age : Int32 = 30
Compilation & Execution
# Interpret (slower)
crystal hello.cr
# Compile to binary
crystal build hello.cr
# Compile with optimizations
crystal build --release hello.cr
# Run specs (tests)
crystal spec
Syntax Basics
Variables & Constants
# Variables (type inferred)
name = "Crystal"
count = 10
# Constants
MAX_SIZE = 100
PI = 3.14159
# Multiple assignment
x, y = 1, 2
# Swap values
a, b = b, a
Type Annotations
# Explicit types
age : Int32 = 25
price : Float64 = 9.99
active : Bool = true
# Nilable types
name : String? = nil
value : Int32? = 42
# Type restrictions
def greet(name : String) : String
"Hello, #{name}"
end
String Interpolation
name = "Crystal"
version = 1.19
# Interpolation
puts "#{name} v#{version}"
# Multi-line strings
text = <<-TEXT
Multi-line
string content
TEXT
# Raw strings
path = %(C:\Users\file.txt)
Arrays
# Array literals
numbers = [1, 2, 3, 4, 5]
mixed = [1, "two", 3.0] # Array(Int32 | String | Float64)
# Typed arrays
names = [] of String
scores = Array(Int32).new
# Array operations
numbers << 6 # Append
numbers[0] # Access
numbers.size # Length
numbers.map { |n| n * 2 }
Hashes
# Hash literals
user = {
"name" => "Alice",
"age" => 30
}
# Symbol keys
config = {
:host => "localhost",
:port => 8080
}
# Typed hashes
scores = {} of String => Int32
scores["Alice"] = 100
# Access
user["name"]
config[:host]
Ranges
# Inclusive range
range = 1..10
# Exclusive range
range = 1...10
# Iterate
(1..5).each { |i| puts i }
# Array from range
array = (1..5).to_a # [1, 2, 3, 4, 5]
Type System
Union Types
# Union type (String or Int32)
def flexible(value : String | Int32)
puts value
end
flexible("text")
flexible(42)
# Nilable (shorthand for T | Nil)
name : String? = nil
# Type check
if value.is_a?(String)
puts value.upcase
end
Type Aliases
# Alias definition
alias StringOrInt = String | Int32
alias Point = {x: Int32, y: Int32}
def process(value : StringOrInt)
# ...
end
point : Point = {x: 10, y: 20}
Generics
# Generic class
class Box(T)
def initialize(@value : T)
end
def get : T
@value
end
end
box = Box(Int32).new(42)
box = Box.new("text") # Type inferred
# Generic method
def identity(x : T) forall T
x
end
Structs vs Classes
# Class (reference type, heap)
class Person
property name : String
def initialize(@name)
end
end
# Struct (value type, stack)
struct Point
property x : Int32
property y : Int32
def initialize(@x, @y)
end
end
# Structs are passed by value
point = Point.new(10, 20)
Control Flow
Conditionals
# If statement
if age >= 18
"Adult"
elsif age >= 13
"Teen"
else
"Child"
end
# Unless
unless logged_in
redirect_to_login
end
# Ternary
status = active ? "On" : "Off"
# Case
case value
when 1, 2, 3
"Low"
when 4..6
"Medium"
else
"High"
end
Loops
# While
while count < 10
count += 1
end
# Until
until done
process_next
end
# Each
[1, 2, 3].each do |n|
puts n
end
# Times
5.times { puts "Hello" }
# Loop (infinite)
loop do
break if done
end
Iterators
# Map
squares = [1, 2, 3].map { |n| n ** 2 }
# Select/Reject
evens = [1, 2, 3, 4].select(&.even?)
odds = [1, 2, 3, 4].reject(&.even?)
# Reduce
sum = [1, 2, 3].reduce(0) { |acc, n| acc + n }
# Each with index
["a", "b"].each_with_index do |char, i|
puts "#{i}: #{char}"
end
Methods & Blocks
Method Definition
# Basic method
def greet(name)
"Hello, #{name}"
end
# Type annotations
def add(a : Int32, b : Int32) : Int32
a + b
end
# Default arguments
def greet(name = "World")
"Hello, #{name}"
end
# Named arguments
def create(name : String, age : Int32 = 0)
{name, age}
end
create(name: "Alice", age: 30)
Method Overloading
# Multiple signatures
def process(value : Int32)
value * 2
end
def process(value : String)
value.upcase
end
process(42) # => 84
process("hi") # => "HI"
Blocks & Procs
# Block (inline)
[1, 2, 3].each { |n| puts n }
# Block (multiline)
[1, 2, 3].each do |n|
puts n * 2
end
# Proc (stored block)
double = ->(x : Int32) { x * 2 }
double.call(5) # => 10
# Method with block
def with_timing(&block)
start = Time.monotonic
yield
elapsed = Time.monotonic - start
puts "Elapsed: #{elapsed}"
end
with_timing { heavy_operation }
Symbol to Proc
# Short syntax for simple operations
[1, 2, 3].map(&.to_s) # ["1", "2", "3"]
["a", "b"].map(&.upcase) # ["A", "B"]
[1, 2, 3].select(&.even?) # [2]
Classes & Objects
Class Definition
class Person
# Properties (getter + setter)
property name : String
property age : Int32
# Getter only
getter id : Int32
# Setter only
setter active : Bool
# Constructor
def initialize(@name, @age)
@id = Random.rand(1000)
@active = true
end
# Instance method
def greet
"Hi, I'm #{@name}"
end
# Class method
def self.species
"Homo sapiens"
end
end
person = Person.new("Alice", 30)
puts person.greet
puts Person.species
Inheritance
class Animal
property name : String
def initialize(@name)
end
def speak
"Some sound"
end
end
class Dog < Animal
def speak
"Woof!"
end
end
dog = Dog.new("Rex")
dog.speak # => "Woof!"
Modules
# Module definition
module Walkable
def walk
"Walking..."
end
end
module Swimmable
def swim
"Swimming..."
end
end
# Include modules
class Duck
include Walkable
include Swimmable
end
duck = Duck.new
duck.walk
duck.swim
Abstract Classes
abstract class Shape
abstract def area : Float64
def describe
"Area: #{area}"
end
end
class Circle < Shape
def initialize(@radius : Float64)
end
def area : Float64
Math::PI * @radius ** 2
end
end
Concurrency
Fibers (Green Threads)
# Spawn fiber
spawn do
puts "In fiber"
sleep 1
puts "Fiber done"
end
puts "Main continues"
sleep 2 # Wait for fiber
# Multiple fibers
10.times do |i|
spawn do
sleep Random.rand(1.0)
puts "Fiber #{i} done"
end
end
sleep 2
Channels
# Create channel
channel = Channel(Int32).new
# Send to channel
spawn do
5.times do |i|
channel.send(i)
sleep 0.1
end
channel.close
end
# Receive from channel
while value = channel.receive?
puts "Received: #{value}"
end
# Buffered channel
buffered = Channel(String).new(10)
Channel Select
# Select from multiple channels
ch1 = Channel(Int32).new
ch2 = Channel(String).new
spawn { ch1.send(42) }
spawn { ch2.send("Hello") }
select
when value = ch1.receive
puts "Got int: #{value}"
when value = ch2.receive
puts "Got string: #{value}"
end
Parallel Execution
# Parallel map
results = (1..10).parallel_map do |i|
sleep 0.1
i * 2
end
# Channel-based pipeline
input = Channel(Int32).new
output = Channel(Int32).new
# Stage 1
spawn do
10.times { |i| input.send(i) }
input.close
end
# Stage 2
spawn do
while value = input.receive?
output.send(value * 2)
end
output.close
end
# Collect results
while result = output.receive?
puts result
end
Macros
Macro Basics
# Simple macro
macro debug(var)
puts "{{var}} = #{{{var}}}"
end
x = 42
debug(x) # Prints: x = 42
# Macro with block
macro measure(&block)
start = Time.monotonic
{{block.body}}
elapsed = Time.monotonic - start
puts "Took #{elapsed}"
end
measure do
sleep 1
end
Property Macros
# Generate getters/setters
class User
{% for attr in [:name, :email, :age] %}
property {{attr.id}} : String
{% end %}
def initialize(@name, @email, @age)
end
end
# Generates:
# property name : String
# property email : String
# property age : String
Macro Methods
# Macro method
macro def_multiply(name, factor)
def {{name.id}}(value)
value * {{factor}}
end
end
class Calculator
def_multiply double, 2
def_multiply triple, 3
end
calc = Calculator.new
calc.double(5) # => 10
calc.triple(5) # => 15
C Bindings
Lib Declaration
# Bind to C library
@[Link("m")] # Link to libm
lib LibM
fun sqrt(x : Float64) : Float64
fun pow(x : Float64, y : Float64) : Float64
end
# Use C function
result = LibM.sqrt(16.0) # => 4.0
# System library
lib LibC
fun strlen(s : UInt8*) : Int32
end
Struct Bindings
lib LibExample
struct Point
x : Int32
y : Int32
end
fun distance(p1 : Point*, p2 : Point*) : Float64
end
# Use C struct
p1 = LibExample::Point.new(x: 0, y: 0)
p2 = LibExample::Point.new(x: 3, y: 4)
dist = LibExample.distance(pointerof(p1), pointerof(p2))
Callbacks
lib LibCallbacks
alias Callback = (Int32 -> Nil)
fun register_callback(cb : Callback)
end
# Define callback
callback = ->(x : Int32) { puts "Got: #{x}" }
# Register
LibCallbacks.register_callback(callback)
Standard Library
File I/O
# Read file
content = File.read("file.txt")
# Write file
File.write("output.txt", "content")
# Open with block
File.open("file.txt") do |file|
file.each_line do |line|
puts line
end
end
# Check existence
File.exists?("file.txt")
Dir.exists?("folder")
# File info
File.size("file.txt")
File.info("file.txt").modification_time
HTTP Client
require "http/client"
# GET request
response = HTTP::Client.get("https://api.example.com")
puts response.body
# POST request
response = HTTP::Client.post(
"https://api.example.com/users",
headers: HTTP::Headers{"Content-Type" => "application/json"},
body: %({"name": "Alice"})
)
# Custom client
client = HTTP::Client.new("api.example.com", tls: true)
response = client.get("/endpoint")
JSON
require "json"
# Parse JSON
json_str = %({"name": "Alice", "age": 30})
data = JSON.parse(json_str)
data["name"] # => "Alice"
# Generate JSON
hash = {"name" => "Bob", "age" => 25}
json = hash.to_json
# JSON mapping
class User
include JSON::Serializable
property name : String
property age : Int32
end
user = User.from_json(json_str)
puts user.to_json
Time & Date
# Current time
now = Time.utc
now = Time.local
# Create time
time = Time.utc(2024, 1, 15, 10, 30, 0)
# Formatting
now.to_s("%Y-%m-%d %H:%M:%S")
# Arithmetic
future = now + 1.hour
past = now - 2.days
# Comparison
time1 < time2
Regular Expressions
# Regex literal
regex = /\d+/
# Match
if "abc123".matches?(/\d+/)
puts "Contains digits"
end
# Capture groups
if match = "Price: $42".match(/\$(\d+)/)
price = match[1] # => "42"
end
# Scan
"a1b2c3".scan(/\d/) do |m|
puts m[0]
end
# Replace
"hello".gsub(/l/, "L") # => "heLLo"
Shards (Package Manager)
shard.yml
name: myapp
version: 1.0.0
dependencies:
kemal:
github: kemalcr/kemal
version: ~> 1.1.0
redis:
github: stefanwille/crystal-redis
version: ~> 2.9.0
development_dependencies:
ameba:
github: crystal-ameba/ameba
version: ~> 1.4.0
targets:
myapp:
main: src/myapp.cr
crystal: 1.9.0
Shard Commands
# Install dependencies
shards install
# Update dependencies
shards update
# Check for updates
shards outdated
# List installed shards
shards list
# Build targets
shards build
# Clean
shards prune
Using Shards
# In src/myapp.cr
require "kemal"
require "redis"
# Use installed shards
get "/" do
"Hello World"
end
Kemal.run
Compilation
Build Options
# Development build
crystal build src/app.cr
# Release build (optimized)
crystal build --release src/app.cr
# Static linking
crystal build --static src/app.cr
# Cross-compilation
crystal build --cross-compile --target x86_64-linux-gnu
# With debug info
crystal build --debug src/app.cr
# Multiple files
crystal build src/main.cr src/helper.cr
Compiler Flags
# Define constants
crystal build -Dpreview_mt src/app.cr
# Custom output
crystal build -o bin/myapp src/app.cr
# Optimization level
crystal build --release -Dpreview_mt src/app.cr
# Show progress
crystal build --progress src/app.cr
# Verbose
crystal build --verbose src/app.cr
Testing
# Run all specs
crystal spec
# Specific file
crystal spec spec/user_spec.cr
# With coverage (via shard)
crystal spec --coverage
# Spec example
# spec/user_spec.cr
require "./spec_helper"
describe User do
it "creates user" do
user = User.new("Alice", 30)
user.name.should eq("Alice")
user.age.should eq(30)
end
end
Error Handling
Exceptions
# Raise exception
raise "Error message"
raise ArgumentError.new("Invalid argument")
# Rescue
begin
risky_operation
rescue ex : DivisionByZeroError
puts "Cannot divide by zero"
rescue ex : Exception
puts "Error: #{ex.message}"
ensure
cleanup
end
# Rescue modifier
value = risky_method rescue default_value
Custom Exceptions
class CustomError < Exception
end
class ValidationError < Exception
property field : String
def initialize(@field, message)
super("#{@field}: #{message}")
end
end
# Raise custom
raise ValidationError.new("email", "Invalid format")
# Rescue custom
begin
validate_user
rescue ex : ValidationError
puts "Validation failed: #{ex.message}"
end
Common Patterns
Singleton
class Database
@@instance : Database?
def self.instance
@@instance ||= new
end
private def initialize
# Setup connection
end
def query(sql)
# Execute query
end
end
db = Database.instance
db.query("SELECT * FROM users")
Builder Pattern
class QueryBuilder
def initialize(@table : String)
@conditions = [] of String
end
def where(condition : String)
@conditions << condition
self
end
def to_sql
sql = "SELECT * FROM #{@table}"
unless @conditions.empty?
sql += " WHERE " + @conditions.join(" AND ")
end
sql
end
end
# Usage
query = QueryBuilder.new("users")
.where("age > 18")
.where("active = true")
.to_sql
Method Chaining
class Calculator
def initialize(@value = 0)
end
def add(n)
@value += n
self
end
def multiply(n)
@value *= n
self
end
def result
@value
end
end
# Usage
result = Calculator.new
.add(5)
.multiply(3)
.add(2)
.result # => 17
Gotchas
Nilable Types
# Compile error: can't call upcase on Nil
name : String? = nil
# name.upcase # Error!
# Must check first
if name
name.upcase # OK: name is String here
end
# Or use safe navigation
name.try(&.upcase)
# Or provide default
name || "default"
Struct Mutability
struct Point
property x : Int32
property y : Int32
def initialize(@x, @y)
end
end
# Structs are copied
p1 = Point.new(1, 2)
p2 = p1
p2.x = 10
puts p1.x # => 1 (unchanged)
puts p2.x # => 10
# Use class for shared state
Type Inference Limits
# Works
numbers = [1, 2, 3]
# Compile error: empty array needs type
# items = [] # Error!
# Must specify type
items = [] of String
items = Array(String).new
Method Resolution
class Parent
def greet
"Parent"
end
end
class Child < Parent
def greet
# Infinite recursion!
# greet + " from Child"
# Use super
super + " from Child"
end
end
Also see
- Crystal Language Official Site - Main documentation
- Crystal API Documentation - Standard library reference
- Crystal Book - Language reference guide
- Awesome Crystal - Curated list of Crystal libraries
- Shards Database - Crystal package registry
- Crystal Forum - Community discussions
- Crystal GitHub - Source code and issue tracker