Structuring Data with Container Types

Simple variables provide a solid foundation, but as your programs grow, you won’t want to keep juggling hundreds of variable names in your head. Crystal provides a number of collection types that let you store a lot of information in structured ways. Arrays and hashes will get you started, and tuples and sets will appear later in the book.

Using Arrays

Sometimes you need to create a list of values, kept in order, and accessible by position. Because this is Crystal, let’s collect some minerals. (All of these mineral names are real, and there are lots of them!). The simplest way is to use an array:

 minerals = [​"alunite"​, ​"chromium"​, ​"vlasovite"​]
 typeof(minerals) ​# => Array(String)
 
 # or, to use a different notation
 minerals2 = ​%w(alunite chromium vlasovite)
 typeof(minerals2) ​# => Array(String)

Crystal tells you that minerals isn’t just an array; it’s an array of String. Its type, Array(String), also contains the type of the items it contains.

You can add minerals easily with the << operator. The size method tells you the number of items it contains:

 minerals << ​"wagnerite"
 minerals << ​"muscovite"
 minerals
 # => ["alunite", "chromium", "vlasovite", "wagnerite", "muscovite"]
 minerals.​size​ ​# => 5

Crystal checks the contents: adding something of another item type isn’t allowed. Try adding a number:

 minerals << 42
 # => Error: no overload matches 'Array(String)#<<' with type Int32

You’ll get an error while compiling. You’ll also get an error if you try to start with an empty array:

 precious_minerals = []
 # => Error: for empty arrays use '[] of ElementType'

This is because the compiler doesn’t have enough information to infer its type, and so it can’t allocate memory for the array. This is a sharp contrast to Ruby practice, where the type is figured out at runtime. You can create an empty array, but you need to specify a type, either with the [] notation or by creating an object of class Array with new:

 precious_minerals = [] of String
 precious_minerals2 = Array(String).​new

As you’d expect, you can read items by index, the position of the item in the array:

 minerals[0] ​# => "alunite"
 minerals[3] ​# => "wagnerite"
 minerals[-2] ​# => "wagnerite"
 # negative indices count from the end, which is -1

You can read subarrays, contiguous sections of arrays, in two different ways. You can give a start index and a size, or use an index range:

 minerals[2, 3] ​# => ["vlasovite", "wagnerite", "muscovite"]
 minerals[2..4] ​# => ["vlasovite", "wagnerite", "muscovite"]

What if you use a wrong index? The first item in a Crystal array is always at index 0, and the last at index size - 1. If you try to retrieve something outside of that range, you’ll get an error, unlike most Crystal errors, at runtime:

 minerals[7] ​# => Runtime error: Index out of bounds (IndexError)

If your program logic requires that you sometimes look for keys that don’t exist, you can avoid this error. Use the []? method, which returns nil instead of crashing the program:

 minerals[7]? ​# => nil

You saw in Chapter 1 that the compiler prevents the use of nil when disaster lurks around the corner. You will soon see how to deal with this in an elegant way.

What if you try to put items of different types in your array?

 pseudo_minerals = [​"alunite"​, ​'C'​, 42]

This works, but the resulting type of this array is peculiar:

 typeof(pseudo_minerals) ​# => Array(Char | Int32 | String)

The compiler infers that the item type is either Char, Int32, or String. In other words, any given item is of the union type Char | Int32 | String. (If you want to structure a variable so that it contains specific types at specific positions, you should explore tuples.)

Union types are a powerful feature of Crystal: an expression can have a set of multiple types at compile time, and the compiler meticulously checks that all method calls are allowed for all of these types. You’ll encounter more examples of union types and how to use them later in the book.

The includes? method lets you check that a certain item exists in an array.

 arr = [56, 123, 5, 42, 108]
 arr.​includes?​ 42 ​# => true

Need to remove the start or end item? shift and pop can help.

 p arr.​shift​ ​# => 56
 p arr ​# => [123, 5, 42, 108]
 p arr.​pop​ ​# => 108
 p arr ​# => [123, 5, 42]

If you want to loop through every value in an array, it’s best if you do it with the each method or one of its variants.

 arr.​each​ ​do​ |i|
  puts i
 end

The API for class Array[24] describes many more useful methods. Also, arrays can add and delete items because they are stored in heap memory, and have no fixed size. If you need raw performance, use a StaticArray, which is a fixed-size array that is allocated on the stack during compilation.

Displaying Arrays

images/aside-icons/tip.png

Want to show an array? Here are some quick options:

 arr = [1, ​'a'​, ​"Crystal"​, 3.14]
 print arr ​# [1, 'a', "Crystal", 3.14] (no newline)
 puts arr ​# [1, 'a', "Crystal", 3.14]
 p arr ​# [1, 'a', "Crystal", 3.14]
 pp arr ​# [1, 'a', "Crystal", 3.14]
 p arr.​inspect​ ​# "[1, 'a', "Crystal", 3.14]"
 printf(​"%s"​, arr[1]) ​# a (no newline)
 p sprintf(​"%s"​, arr[1]) ​# "a"

pp and inspect are useful for debugging. printf and sprintf accept a format string like in C, the latter returning a String.

Your Turn 2

Deleting by value: Most of the time, when you work with arrays, you’ll want to manipulate their content based on the positions of items. However, Crystal also lets you manipulate their content based on the values of items. Explore the Crystal API documentation and figure out how to go from ["alunite", "chromium", "vlasovite"] to ["alunite", "vlasovite"] without referencing the positions of the values.

 minerals = [​"alunite"​, ​"chromium"​, ​"vlasovite"​]
 minerals.​delete​(​"chromium"​)
 
 p minerals ​#=> ["alunite", "vlasovite"]

Using Hashes

Arrays are great if you want to retrieve information based on its location in a set, but sometimes you want to retrieve information based on a key value instead. Hashes make that easy.

Let’s build a collection that contains minerals and their hardness property using the Mohs Hardness Scale. Given a mineral name, you need to quickly find its hardness. For this, a hash (sometimes called a map or dictionary) is ideal:

 mohs = {
 "talc"​ => 1,
 "calcite"​ => 3,
 "apatite"​ => 5,
 "corundum"​ => 9,
 }
 typeof(mohs) ​# => Hash(String, Int32)

Its type, Hash(String, Int32), is based on the types of key (String) and value (Int32). You can quickly get the value for a given key using key indexing:

 mohs[​"apatite"​] ​# => 5

What if the key doesn’t exist, like “gold”? Then, as you saw with arrays, you’ll get a runtime error:

 mohs[​"gold"​]
 # => Runtime error: Missing hash key: "gold" (KeyError)

As you saw with arrays, if your logic needs to handle a situation where the key doesn’t exist, it’s safer to use the []? variant, which returns nil:

 mohs[​"gold"​]? ​# => nil

Or, still better, check the existence of the key with has_key?:

 mohs.​has_key?​ ​"gold"​ ​# => false

Adding a new key-value pair, or changing an existing pair, is easy. You’ll use the index notation from arrays, except that you now use the key instead of the index:

 mohs[​"diamond"​] = 9 ​# adding key
 mohs[​"diamond"​] = 10 ​# changing value
 mohs
 # => {"talc" => 1, "calcite" => 3, "apatite" => 5,
 # "corundum" => 9, "diamond" => 10}
 mohs.​size​ ​# => 5

Notice that the size of the hash has increased from 4 to 5. What happens when you add a (key, value) combination where the type of key or value differs from the original items?

 mohs[​'C'​] = 4.5 ​# Error: no overload matches
 # 'Hash(String, Int32)#[]=' with types Char, Float64

Again, you’ll get an error at compile-time: Crystal statically controls your types!

What if you want to start off with an empty hash?

 mohs = {} ​# Error: Syntax error: for empty hashes use
 # '{} of KeyType => ValueType'

This doesn’t work. Just as with arrays, you need to specify the types again:

 mohs = {} of String => Int32 ​# {}
 mohs = Hash(String, Int32).​new

As you can guess by now, hashes inherit all their methods from the Hash class, which you can find in the API docs.[25]

Your Turn 3

Is that hash empty? Crystal will let you create an empty hash as long as you specify types for both the values and the keys. But empty hashes can create errors and nils when you aren’t expecting them. Explore the API docs and find the ways to test for empty hashes and manipulate hashes safely, even when they might be empty.

 mohs = {
 "talc"​ => 1,
 "calcite"​ => 3,
 "apatite"​ => 5,
 "corundum"​ => 9
 } of String => Int32
 
 p mohs.​empty?​ => ​false
..................Content has been hidden....................

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