NexusCS

Crystal

Languages
Quick reference for Crystal - a statically-typed, compiled language with Ruby-like syntax that compiles to native code via LLVM.
crystal
programming
static-typing
ruby-like

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 Cheatsheet - NexusCS