Integers in Crystal

Mark Connell Engineering Director, Applications

DALL·E's interpretation of crystal integers

Another blog post on the journey of discovering the differences between Ruby and Crystal. This time, we’re taking a look at some of the differences with integers.

Types

Ruby

All integers in Ruby (2.4 onwards) are of type Integer regardless of size. Using a 64-bit CPU, integers up to 63-bits (those that fit in a single machine word), have pre-determined Object IDs. And will always be represented by the same object reference by the interpreter:

# Ruby
# All whole numbers are of type Integer, including those
# that are larger than machine addressable space.
1.class                         # => Integer
4_611_686_018_427_387_904.class # => Integer

# machine word integers have an object_id == value * 2 + 1
1.object_id #=> 5
1.object_id #=> 5

# max signed 32-bit integer
2_147_483_647.object_id #=> 4294967295
2_147_483_647.object_id #=> 4294967295

# max signed 63-bit integer
4_611_686_018_427_387_903.object_id # => 9223372036854775807
4_611_686_018_427_387_903.object_id # => 9223372036854775807

Beyond 4_611_686_018_427_387_903, the interpreter creates objects to represent the values supplied. These numbers are not guaranteed to be represented by the same object ID:

# Ruby
# (max 63-bit + 1) integer generates a ruby object to represent it
4_611_686_018_427_387_904.object_id # => 70167250121400
4_611_686_018_427_387_904.object_id # => 70167250065500

Crystal

In the Crystal language, there are 10 Integer types: Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64, Int128, and UInt128.

Depending on the size of the integer, Crystal will automatically infer the type of Integer it should use to represent the number. The default integer type is Int32:

# Crystal
1.class                         # => Int32
2_147_483_647.class             # => Int32
2_147_483_648.class             # => Int64
9_223_372_036_854_775_807.class # => Int64
9_223_372_036_854_775_808.class # => UInt64
# Warning: 9_223_372_036_854_775_808 doesn't fit in an Int64,
# try using the suffix u64 or i128

The above warning hints at a behaviour you can use to define an integer as a specific type should you want it:

5i8.class  # => Int8
5u16.class # => UInt16
5i64.class # => Int64

We can also instanciate the same examples using a String value representing the integer:

Int8.new("5").class   # => Int8
UInt16.new("5").class # => UInt16
Int64.new("5").class  # => Int64

Division

In Ruby, dividing two integers will return an integer, and the remainder is obtained using the modulus % operator:

# Ruby
55 / 3 # => 18
55 % 3 # => 1

In Crystal however, the same operations will result in an unexpected answer if you’re used to that behaviour of Ruby

# Crystal
55 / 3 # => 18.333333333333332
55 % 3 # => 1

To get a similar result in Ruby, one of the numbers needs to be coerced to a type that can represent decimal numbers:

55 / 3.to_f # => 18.333333333333332

This is actually an intended behaviour within the Crystal language. If you want to divide integers in the same way as you would expect to in Ruby, we need to perform a floor division:

# Crystal
55 // 3 # => 18