Beyond Hashes and Arrays: More Composite Types

Composite types, like the arrays and hashes we met in the previous chapter, make it easier to create more complex applications. Crystal offers more options, like tuples and sets, to address other data models.

Tuples group related values of possibly different types. You can create them with values inside { }, or with Tuple.new:

 tpl = {42, ​"silver"​, ​'C'​}
 tpl.​class​ ​# => Tuple(Int32, String, Char)
 a = Tuple.​new​(42, ​"silver"​, ​'C'​)

In case you’re wondering how multiple assignments, such as n, m = 42, 43, work, they use tuples. You can access items of a tuple by index:

 tpl[0] ​# => 42 (Int32)
 tpl[1] ​# => "silver" (String)
 tpl[2] ​# => 'C' (Char)
 var = 89
 tpl[var] ​# => Index out of bounds (IndexError)
 tpl[var]? ​# => nil

The index is checked and, if it’s wrong, it generates an error at compile time if literals are involved, but otherwise a runtime IndexError. As with arrays, it’s safer to use the []? method, which returns nil in this case. You can use all methods from class Tuple, such as size, each, includes?, map, and so on. Because tuples are created on the stack, they’re lighter than arrays, which are allocated on the heap, especially when used inside loops.

Using the shortcut ||= syntax we introduced earlier in Getting Input offers a great way to add data pairs:

 h = {1 => ​'A'​}
 h[3] ||= ​'C'
 h ​# => {1 => 'A', 3 => 'C'}

We could have used that form in our conversion project like this:

 rates[curr] ||= rate.​to_f

This means that the exchange rate is set only to the first value; subsequent values would not change it.

If you want more meaning in your data structure, Crystal also has named tuples, which are like records in some other languages:

 tpl = {​name: ​​"Crystal"​, ​year: ​2017} ​# NamedTuple(name: String, year: Int32)
 tpl[​:name​] ​# => "Crystal" (String)

As tuples are fixed-size and stack-allocated, they’re very efficient. Also, the compiler can see the type at each position. This is in contrast to arrays, which can change in size, so you should give preference to tuples over arrays if the size is constant and there are different types.

In the case of symbol or string type keys, you can use the named tuples notation:

 # Instead of {:key1 => 'a', :key2 => 'b'} you can use:
 {​key1: ​​'a'​, ​key2: ​​'b'​}
 # Instead of {"key1" => 'a', "key2" => 'b'} you can use:
 {​"key1"​: ​'a'​, ​"key2"​: ​'b'​}

If you don’t need names, indexes, or order, but you need to store unique values, use a set:

 set = Set{41, 42, 43} ​# => Set{41, 42, 43}
 set.​class​ ​# => Set(Int32)
 # The above is equivalent to
 set = Set(Int32).​new
 set << 42
 set << 41
 set << 43
 set << 41
 set ​# => Set{42, 41, 43}

Your Turn 4

Destructuring: What are the values of the variables on the left side?

 var1, var2, var3 = [78, 56, 42] ​# array
 var1, var2, var3 = {78, 56, 42} ​# tuple

This is a nice way to get values out of an array or a tuple. We can use it in our currency convertor to replace:

 arr = input.​split​(​" - "​)
 curr = arr[0]
 rate = arr[1]

with:

 curr, rate = input.​split​(​" - "​)

From the preceding examples, it’s clear that Array, Tuple, Hash, and Set can take different types for their items, while their methods work for all these types. You could say that they have a Type T or K or V as a parameter, like Array(T), Hash(K, V), and so on, where T is Int32 or String or Char or whatever type. In other words: they’re generic types. In Chapter 6, you’ll see how to define your own generic classes that are able to work on any type.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset