Operators

Crystal supports a number of operators, with one, two or three operands.

Operator expressions are actually parsed as method calls. For example a + b is semantically equivalent to a.+(b), a call to method + on a with argument b.

There are however some special rules regarding operator syntax:

  • The dot (.) usually put between receiver and method name (i.e. the operator) can be omitted.
  • Chained sequences of operator calls are restructured by the compiler in order to implement operator precedence. Enforcing operator precedence makes sure that an expression such as 1 * 2 + 3 * 4 is parsed as (1 * 2) + (2 * 3) to honour regular math rules.
  • Regular method names must start with a letter or underscore, but operators only consist of special characters. Any method not starting with a letter or underscore is an operator method.
  • Available operators are whitelisted in the compiler (see List of Operators below) which allows symbol-only method names and treats them as operators, including their precedence rules.

Operators are implemented like any regular method, and the standard library offers many implementations, for example for math expressions.

Defining operator methods

Most operators can be implemented as regular methods.

One can assign any meaning to the operators, but it is advisable to stay within similar semantics to the generic operator meaning to avoid cryptic code that is confusing and behaves unexpectedly.

A few operators are defined directly by the compiler and cannot be redefined in user code. Examples for this are the inversion operator !, the assignment operator =, combined assignment operators such as ||= and range operators. Whether a method can be redefined is indicated by the colum Overloadable in the below operator tables.

Unary operators

Unary operators are written in prefix notation and have only a single operand. Thus, a method implementation receives no arguments and only operates on self.

The following example demonstrates the Vector2 type as a two-dimensional vector with a unary operator method - for vector inversion.

  1. struct Vector2
  2. getter x, y
  3. def initialize(@x : Int32, @y : Int32)
  4. end
  5. # Unary operator. Returns the inverted vector to `self`.
  6. def - : self
  7. Vector2.new(-x, -y)
  8. end
  9. end
  10. v1 = Vector2.new(1, 2)
  11. -v1 # => Vector2(@x=-1, @y=-2)

Binary operators

Binary operators have two operands. Thus, a method implementation receives exactly one argument representing the second operand. The first operand is the receiver self.

The following example demonstrates the Vector2 type as a two-dimensional vector with a binary operator method + for vector addition.

  1. struct Vector2
  2. getter x, y
  3. def initialize(@x : Int32, @y : Int32)
  4. end
  5. # Binary operator. Returns *other* added to `self`.
  6. def +(other : self) : self
  7. Vector2.new(x + other.x, y + other.y)
  8. end
  9. end
  10. v1 = Vector2.new(1, 2)
  11. v2 = Vector2.new(3, 4)
  12. v1 + v2 # => Vector2(@x=4, @y=6)

Per convention, the return type of a binary operator should be the type of the first operand (the receiver), so that typeof(a <op> b) == typeof(a). Otherwise the assignment operator (a <op>= b) would unintentionally change the type of a. There can be reasonable exceptions though. For example in the standard library the float division operator / on integer types always returns Float64, because the quotient must not be limited to the value range of integers.

Ternary operators

The conditional operator (? :) is the only ternary operator. It not parsed as a method, and its meaning cannot be changed. The compiler transforms it to an if expression.

Operator Precedence

This list is sorted by precedence, so upper entries bind stronger than lower ones.

CategoryOperators
Index accessors[], []?
Unary+, &+, -, &-, !, ~
Exponential, &
Multiplicative, &, /, //, %
Additive+, &+, -, &-
Shift<<, >>
Binary AND&
Binary OR/XOR|,^
Equality and Subsumption==, !=, =~, !~, ===
Comparison<, <=, >, >=, <=>
Logical AND&&
Logical OR||
Range..,
Conditional?:
Assignment=, []=, +=, &+=, -=, &-=, =, &=, /=, //=, %=, |=, &=,^=,=,<<=,>>=, ||=, &&=
Splat*,

List of operators

Arithmetic operators

Unary

OperatorDescriptionExampleOverloadableAssociativity
+positive+1yesright
&+wrapping positive&+1yesright
-negative-1yesright
&-wrapping negative&-1yesright

Multiplicative

OperatorDescriptionExampleOverloadableAssociativity
exponentiation1 2yesright
&wrapping exponentiation1 & 2yesright
multiplication1 2yesleft
&wrapping multiplication1 & 2yesleft
/division1 / 2yesleft
//floor division1 // 2yesleft
%modulus1 % 2yesleft

Additive

OperatorDescriptionExampleOverloadableAssociativity
+addition1 + 2yesleft
&+wrapping addition1 &+ 2yesleft
-subtraction1 - 2yesleft
&-wrapping subtraction1 &- 2yesleft

Other unary operators

OperatorDescriptionExampleOverloadableAssociativity
!inversion!truenoright
~binary complement~1yesright

Shifts

OperatorDescriptionExampleOverloadableAssociativity
<<shift left, append1 << 2, STDOUT << “foo”yesleft
>>shift right1 >> 2yesleft

Binary

OperatorDescriptionExampleOverloadableAssociativity
&binary AND1 & 2yesleft
|binary OR1 | 2yesleft
^binary XOR1 ^ 2yesleft

Relational operators

Relational operators test a relation between two values. They include equality, inequalities, and subsumption.

Equality

The equal operator == checks whether the values of the operands are considered equal.

The not-equal operator != is a shortcut to express the inversion: a != b is supposed to be equivalent to !(a == b).

Types that implement the not-equal operator must make sure to adhere to this. Special implementations can be useful for performance reasons because inequality can often be proven faster than equality.

Both operators are expected to be commutative, i.e. a == b if and only if b == a. This is not enforced by the compiler and implementing types must take care themselves.

OperatorDescriptionExampleOverloadableAssociativity
==equal1 == 2yesleft
!=not equal1 != 2yesleft

Info

The standard library defines Reference#same?:Bool-instance-method) as another equality test that is not an operator. It checks for referential identity which determines whether two values reference the same location in memory.

Inequalities

Inequality operators describe the order between values.

The three-way comparison operator <=> (also known as spaceship operator) expresses the order between two elements expressed by the sign of its return value.

OperatorDescriptionExampleOverloadableAssociativity
<less1 < 2yesleft
<=less or equal1 <= 2yesleft
>greater1 > 2yesleft
>=greater or equal1 >= 2yesleft
<=>three-way comparison1 <=> 2yesleft

Info

The standard library defines the Comparable module which derives all other inequality operators as well as the equal operator from the three-way comparison operator.

Subsumption

The pattern match operator =~ checks whether the value of the first operand matches the value of the second operand with pattern matching.

The no pattern match operator !~ expresses the inverse.

The case subsumption operator === (also, imprecisely called case equality operator or triple equals) checks whether the right hand operand is a member of the set described by the left hand operator. The exact interpretation varies depending on the involved data types.

The compiler inserts this operator in case … when conditions.

There is no inverse operator.

OperatorDescriptionExampleOverloadableAssociativity
=~pattern match“foo” =~ /fo/yesleft
!~no pattern match“foo” !~ /fo/yesleft
===case subsumption/foo/ === “foo”yesleft

Chaining relational operators

Relational operators ==, !=, ===, <, >, <=, and >= can be chained together and are interpreted as a compound expression. For example a <= b <= c is treated as a <= b && b <= c. It is possible to mix different operators: a >= b <= c > d is equivalent to a >= b && b <= c && c > d.

It is advised to only combine operators of the same precedence class to avoid surprising bind behaviour. For instance, a == b <= c is equivalent to a == b && b <= c, while a <= b == c is equivalent to a <= (b == c).

Logical

OperatorDescriptionExampleOverloadableAssociativity
&&logical ANDtrue && falsenoleft
||logical ORtrue || falsenoleft

Range

The range operators are used in Range literals.

OperatorDescriptionExampleOverloadable
..range1..10no
exclusive range1…10no

Splats

Splat operators can only be used for destructing tuples in method arguments. See Splats and Tuples for details.

OperatorDescriptionExampleOverloadable
splatfoono
double splatfoono

Conditional

The conditional operator (? :) is internally rewritten to an if expression by the compiler.

OperatorDescriptionExampleOverloadableAssociativity
? :conditionala == b ? c : dnoright

Assignments

The assignment operator = assigns the value of the second operand to the first operand. The first operand is either a variable (in this case the operator can’t be redefined) or a call (in this case the operator can be redefined). See assignment for details.

OperatorDescriptionExampleOverloadableAssociativity
=variable assignmenta = 1noright
=call assignmenta.b = 1yesright
[]=index assignmenta[0] = 1yesright

Combined assignments

The assignment operator = is the basis for all operators that combine an operator with assignment. The general form is a <op>= b and the compiler transform that into a = a <op> b.

Exceptions to the general expansion formula are the logical operators:

  • a ||= b transforms to a || (a = b)
  • a &&= b transforms to a && (a = b)

There is another special case when a is an index accessor ([]), it is changed to the nilable variant ([]? on the right hand side:

  • a[i] ||= b transforms to a[i] = (a[i]? || b)
  • a[i] &&= b transforms to a[i] = (a[i]? && b)

All transformations assume the receiver (a) is a variable. If it is a call, the replacements are semantically equivalent but the implementation is a bit more complex (introducing an anonymous temporary variable) and expects a= to be callable.

The receiver can’t be anything else than a variable or call.

OperatorDescriptionExampleOverloadableAssociativity
+=addition and assignmenti += 1noright
&+=wrapping addition and assignmenti &+= 1noright
-=subtraction and assignmenti -= 1noright
&-=wrapping subtraction and assignmenti &-= 1noright
=multiplication and assignmenti = 1noright
&=wrapping multiplication and assignmenti &= 1noright
/=division and assignmenti /= 1noright
//=floor division and assignmenti //= 1noright
%=modulo and assignmenti %= 1yesright
|=binary or and assignmenti |= 1noright
&=binary and and assignmenti &= 1noright
^=binary xor and assignmenti ^= 1noright
=exponential and assignmenti = 1noright
<<=left shift and assignmenti <<= 1noright
>>=right shift and assignmenti >>= 1noright
||=logical or and assignmenti ||= truenoright
&&=logical and and assignmenti &&= truenoright

Index Accessors

Index accessors are used to query a value by index or key, for example an array item or map entry. The nilable variant []? is supposed to return nil when the index is not found, while the non-nilable variant raises in that case. Implementations in the standard-library usually raise KeyError or IndexError.

OperatorDescriptionExampleOverloadable
[]index accessorary[i]yes
[]?nilable index accessorary[i]?yes