Crystal tuples: the immutable data structure of crystal

Tuple is a data structure which has immutable elements and is of a fixed size. Its similar to an array, but unlike arrays in crystal and ruby which allows adding more values over time a tuple is of fixed and cannot change. (Disclaimer: This article is meant for Ruby developer and explaining what a tuple is to a ruby developer). In crystal we have two types of tuple 1) Tuple {1, "hello", :world} 2) NamedTuple {x: 1, y:2, z: 5} They are immutable, which means if you try changing the value of an element in a tuple you will get an exception. Since crystal programs are compiled before execution you will get to see these errors while you compile the program itself. example:

x = {1, 2, 3}
# to get the value use
x[0]
If you try assigning a value to it, like so
x[0] = 10
You will get an exception like bellow.
undefined method '[]=' for Tuple(Int32, Int32)
In crsytal, tuples are the preferred way to return a multiple results from a method. For example inside the crystal core we have a method to get the minimum and maximum of an array.
(1..100).minmax
the result would be {1, 100}
Note: Since we just mentioned minmax, have a look at minmax_by method as well. It would let you apply a block of code over your range and then return the minimum and maximum based on the returned collection.
["1234", "12", "123"].minmax_by { |i| i.size }
# => {"12", "1234"}
Advantage of using tuple to return results instead of something like hash, is that we can be sure that our result cannot be altered accidentally. (since the data structure is immutable) ? You can build a tuple from an array by using the .from method
Tuple(Int32, Int32).from([1, 2])
As a developer, the place where we use tuple the most in crystal are with splats(symbol: *). Passing arguments to method using splat and double splat operator is something we use widely in ruby keep our code small and readable. So if you wish to do the same in crystal you need to make a tuple not a hash or array. If you use splat on an array directly like test(*[1,2]) it would return an error
argument to splat must be a tuple, not Array(Int32)
So to achieve the same effect as a splat with array in crystal we would need to do test(*{1,2})

Named Tuple

Named Tuple are everything as above, but with a name for each element. Named Tuple looks like {x: 1, y:2} it gives more meaning to our tuple. Like the above you can access the values but not change them.
data = {x: 1, y: 2}
# to get the value
data[:x]
# raises errors when we try to change it
data[:x] = 1
Double splats are meant for Named Tuple where in we can pass in the values for a particular argument using named tuple and double splat.
def print_date(year = nil, month = nil, day = nil)
  puts "#{year}/#{month}/#{day}"
end
birth_day = { year: 1990, month: 4, day: 3}
print_date(**birth_day)
card_expiry = { year: 2020, month: 1}
print_date(**card_expiry)
You can build a NamedTuple from a hash.
NamedTuple(name: String, val: String).from({"name" => "number", "val" => "Harisankar P S"}
Note: Crystal has a nifty feature called Union types (a variable can store data of multiple data types), so if it happen to pass such a variable to a named tuple/tuple, it will still check for the exact type that we want if the data is not in that variable then an exception would be raised Example
k = 42.as(Int32 | String)
NamedTuple(name: String, val: String).from({"name" => "number", "val" => K}
Exception:
cast from Int32 to String failed, at /usr/local/Cellar/crystal-lang/0.23.1_1/src/class.cr:41:5:41 (TypeCastError)
0x10e8f1085: *CallStack::unwind:Array(Pointer(Void)) at ??
0x10e8f1021: *CallStack#initialize:Array(Pointer(Void)) at ??
0x10e8f0ff8: *CallStack::new:CallStack at ??
0x10e8ec295: *raise<TypeCastError>:NoReturn at ??
0x10e90feb8: *[email protected]::cast<(Int32 | String)>:String at ??
0x10e95faa3: *NamedTuple(name: String:Class, val: String:Class)@NamedTuple(T)#from<Hash(String, Int32 | String)>:NamedTuple(name: String, val: String) at ??
0x10e95f787: *NamedTuple(name: String, val: String)@NamedTuple(T)::from<Hash(String, Int32 | String)>:NamedTuple(name: String, val: String) at ??
0x10e8ef8a6: *__icr_exec__:NamedTuple(name: String, val: String) at ??
0x10e8db130: __crystal_main at ??
0x10e8ee578: main at ??

Extra Note:

If you put a splat before method argument and pass in arguments, they will be converted to a tuple
def a_method(*data)
  puts data
end
a_method(1,2,3)
#=> {1,2,3}
If you put a double splat before method argument and pass in data as keyword argument it gets converted to a NamedTuple
def a_method(**data)
  puts data
end
a_method(x: 1, y: 10)
#=> {x: 1, y: 10}
 

To summarize:

  • Tuples are immutable data structure
  • Regular tuple is like a frozen array
  • You can use splat only with a tuple
  • NamedTuple is like a frozen hash
  • Double splat can only be used with NamedTuple
]]>