Beginner

Operators in Crystal

Master arithmetic, comparison, logical, bitwise, and range operators in Crystal with type-aware examples and Docker-ready code

Operators are the verbs of a programming language - they tell the compiler how to combine, compare, and transform values. In Crystal, operators look familiar to anyone who has used Ruby, but they carry static-type semantics under the hood. Every operator is actually a method call, which means you can overload them on your own types just like in Ruby, while the LLVM-based compiler still produces efficient native machine code.

Because Crystal is statically typed with inference, the compiler resolves which method an operator dispatches to at compile time. This produces clear errors when you mix incompatible types, and it makes operator overloading a first-class tool rather than a runtime cost. In this tutorial you will see arithmetic, comparison, logical, bitwise, assignment, and range operators, plus a glimpse at how Crystal treats operators as methods.

You will also encounter operator precedence, the <=> “spaceship” operator used by Comparable, and Crystal’s strict approach to mixing numeric types.

Arithmetic Operators

Crystal supports the standard arithmetic operators along with integer division and exponentiation. Integer division (//) always rounds toward negative infinity, while / between two integers produces a Float64.

Create a file named operators_arithmetic.cr:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
a = 17
b = 5

puts "a + b  = #{a + b}"
puts "a - b  = #{a - b}"
puts "a * b  = #{a * b}"
puts "a / b  = #{a / b}"      # Float division -> Float64
puts "a // b = #{a // b}"     # Integer (floor) division
puts "a % b  = #{a % b}"      # Modulo
puts "a ** b = #{a ** b}"     # Exponentiation
puts "-a     = #{-a}"         # Unary minus

Notice that 17 / 5 produces a Float64, while 17 // 5 stays in the integer domain - a deliberate design choice that distinguishes mathematical division from integer truncation.

Comparison and the Spaceship Operator

Comparison operators return Bool. The <=> operator returns -1, 0, or 1 (an Int32) and is the foundation of the Comparable module.

Create a file named operators_comparison.cr:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
x = 10
y = 20

puts "x == y : #{x == y}"
puts "x != y : #{x != y}"
puts "x <  y : #{x < y}"
puts "x <= y : #{x <= y}"
puts "x >  y : #{x > y}"
puts "x >= y : #{x >= y}"
puts "x <=> y: #{x <=> y}"     # -1 because x < y

# Strings compare lexicographically
puts "\"apple\" <=> \"banana\": #{"apple" <=> "banana"}"

# Object identity vs value equality
s1 = "hello"
s2 = String.build { |io| io << "hello" }
puts "s1 == s2   : #{s1 == s2}"      # value equality
puts "s1.same?(s2): #{s1.same?(s2)}" # reference identity

Logical and Bitwise Operators

Crystal uses &&, ||, and ! for short-circuit boolean logic. Only nil and false are falsy - every other value, including 0 and "", is truthy. The &, |, ^, ~, <<, and >> operators work bitwise on integers.

Create a file named operators_logical.cr:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
t = true
f = false

puts "t && f : #{t && f}"
puts "t || f : #{t || f}"
puts "!t     : #{!t}"

# Short-circuit returns the operand, not always a Bool
name = nil
display = name || "anonymous"
puts "display = #{display}"

# Truthiness: only nil and false are falsy
puts "0 is truthy   : #{!!0}"
puts "\"\" is truthy: #{!!""}"

# Bitwise operators on Int32
puts "0b1100 & 0b1010 = #{(0b1100 & 0b1010).to_s(2).rjust(4, '0')}"
puts "0b1100 | 0b1010 = #{(0b1100 | 0b1010).to_s(2).rjust(4, '0')}"
puts "0b1100 ^ 0b1010 = #{(0b1100 ^ 0b1010).to_s(2).rjust(4, '0')}"
puts "1 << 4          = #{1 << 4}"
puts "32 >> 2         = #{32 >> 2}"

Assignment, Ranges, and Operators as Methods

Compound assignment (+=, -=, etc.) is shorthand for x = x + y. Crystal also has the special ||= and &&= forms for conditional assignment - very useful when working with nilable values. Ranges (.. and ...) build Range objects that play well with iteration and collections.

Most operators are actually methods, so you can define them on your own types. Below we overload + on a small Vector struct.

Create a file named operators_advanced.cr:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# Compound assignment
score = 10
score += 5
score *= 2
puts "score = #{score}"

# Conditional assignment - only assigns when target is nil/false
greeting : String? = nil
greeting ||= "Hello"
greeting ||= "Ignored"   # not assigned, greeting is already truthy
puts "greeting = #{greeting}"

# Ranges
inclusive = 1..5      # 1, 2, 3, 4, 5
exclusive = 1...5     # 1, 2, 3, 4
puts "inclusive sum = #{inclusive.sum}"
puts "exclusive to_a = #{exclusive.to_a}"

# Operators are methods - overload + on your own type
struct Vector
  getter x : Int32, y : Int32

  def initialize(@x, @y)
  end

  def +(other : Vector) : Vector
    Vector.new(@x + other.x, @y + other.y)
  end

  def to_s(io : IO) : Nil
    io << "(" << @x << ", " << @y << ")"
  end
end

v = Vector.new(1, 2) + Vector.new(3, 4)
puts "v = #{v}"

# Operator precedence: * binds tighter than +
puts "2 + 3 * 4    = #{2 + 3 * 4}"
puts "(2 + 3) * 4  = #{(2 + 3) * 4}"

Running with Docker

1
2
3
4
5
6
7
8
# Pull the official Crystal image
docker pull crystallang/crystal:1.14.0

# Run each example
docker run --rm -v $(pwd):/app -w /app crystallang/crystal:1.14.0 crystal run operators_arithmetic.cr
docker run --rm -v $(pwd):/app -w /app crystallang/crystal:1.14.0 crystal run operators_comparison.cr
docker run --rm -v $(pwd):/app -w /app crystallang/crystal:1.14.0 crystal run operators_logical.cr
docker run --rm -v $(pwd):/app -w /app crystallang/crystal:1.14.0 crystal run operators_advanced.cr

Expected Output

operators_arithmetic.cr:

a + b  = 22
a - b  = 12
a * b  = 85
a / b  = 3.4
a // b = 3
a % b  = 2
a ** b = 1419857
-a     = -17

operators_comparison.cr:

x == y : false
x != y : true
x <  y : true
x <= y : true
x >  y : false
x >= y : false
x <=> y: -1
"apple" <=> "banana": -1
s1 == s2   : true
s1.same?(s2): false

operators_logical.cr:

t && f : false
t || f : true
!t     : false
display = anonymous
0 is truthy   : true
"" is truthy: true
0b1100 & 0b1010 = 1000
0b1100 | 0b1010 = 1110
0b1100 ^ 0b1010 = 0110
1 << 4          = 16
32 >> 2         = 8

operators_advanced.cr:

score = 30
greeting = Hello
inclusive sum = 15
exclusive to_a = [1, 2, 3, 4]
v = (4, 6)
2 + 3 * 4    = 14
(2 + 3) * 4  = 20

Key Concepts

  • Operators are methods - a + b is sugar for a.+(b), which is why you can overload operators on your own types.
  • / vs // - Integer / produces a Float64; // performs floor division and stays in the integer domain.
  • Strict truthiness - Only nil and false are falsy in Crystal; 0, "", and empty collections are all truthy.
  • Short-circuit results - || and && return one of their operands, not a coerced Bool, which makes idioms like value ||= default natural.
  • <=> powers Comparable - Implementing the spaceship operator on a type, plus include Comparable, automatically gives you <, <=, >, >=, and between?.
  • Compile-time type checking - Crystal rejects nonsense like 1 + "x" at compile time rather than failing at runtime.
  • Ranges are first-class - .. (inclusive) and ... (exclusive) produce Range objects that integrate with iteration, slicing, and pattern matching.
  • Precedence follows convention - Multiplicative operators bind tighter than additive ones; use parentheses whenever clarity is in doubt.

Running Today

All examples can be run using Docker:

docker pull crystallang/crystal:1.14.0
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining