Doing Crystal #3: Types, types, types
Tags: crystal, tutorial, series, ruby
Published: 2019-05-23 11:26:58

Welcome to the third post in my Doing Crystal series. In this post I'll be focusing on Crystal's type system, it's benefits, and the drawbacks. If you haven't read the other articles in this series you can find them [here](https://dev.to/watzon/doing-crystal-1-a-ruby-comparison-4g9c), and [here](https://dev.to/watzon/doing-crystal-2-getting-started-with-crystal-13dk) for posts one and two respectively.

## Static vs Dynamic Typing

Static typing has existed in programming for years, going back as far as FORTRAN which was first released back in 1957. Now in truth all programming languages are typed, even languages such as JavaScript and Ruby have types such as Integers, Arrays, and Strings. This is necessary because in the real world things have specific properties which are unique to that thing. Numbers and strings are inherently different and, even though you could technically add a number to a string by taking the string down to its binary representation and adding the number to that, in real world terms it just doesn't make sense. Hence, we have types.

The difference is in how the types are handled. With languages like JavaScript and Ruby types are handled dynamically. This allows you to do things like the following Ruby example:

```ruby
def first_and_last(arr)
  if arr.length > 0
    return [arr.first, arr.last]
  end
  
  []
end

puts first_and_last(["Hello", "fellow", "developers"])
# => ["Hello", "developers"]

puts first_and_last(42)
# => NoMethodError (undefined method `length' for 42:Integer)
```

As you can see, the method `first_and_last` is supposed to accept an Array and return the first and last elements as a new Array. It works fine when handed the type of data it expects, but when handed a number it throws a runtime error `NoMethodError`. Dynamic typing can be extremely handy, but it can also be detrimental as a large number of runtime errors (or errors that occur while your program is running as opposed to when it's compiled) occur when the program attempts to use a method that doesn't exist, or when a variable's type changes unexpectedly.

Now let's look at the same code from before, but in Crystal.

```crystal
def first_and_last(arr : Array(U)) forall U
  if arr.size > 0
    return [arr.first, arr.last]
  end
  
  [] of U
end

puts first_and_last(["Hello", "fellow", "developers"])

puts first_and_last(42)
```

For this one I didn't show an output. Why? Because the program won't compile. Let's go over the program line by line, and then I'll explain why the compilation fails.

```crystal
def first_and_last(arr : Array(U)) forall U
```

First we do a method definition. This is similar to the Ruby example, but there a a couple of important differences. First we have the weird `arr : Array(U)` syntax. This is setting the property name to `arr` and the type of `arr` to an `Array` of type `U`. `U` in this case is a placeholder type, and outside the scope of this tutorial, but suffice it to say it allows `U` to be anything. The `forall U` part at the end creates the `U` generic.

```crystal
if arr.size > 0
  return [arr.first, arr.last]
end
```

This is exactly the same as the Ruby example save the slight method name change for getting the size of an array. In Ruby it's `length` and in Crystal it's `size`.

```crystal
[] of U
```

In Crystal all things have a specific type. This prevents runtime errors and lowers memory usage since the program can allocate the resources it knows it needs. As such, all Arrays, Hashes, Sets, etc. have to be explicitly typed and that is what this line is doing. If the Array the method is handed doesn't have anything in it we just return an empty Array. Truthfully we probably should've returned the same array we were given, but I wanted to show an example of assigning an Array a type.

```crystal
puts first_and_last(["Hello", "fellow", "developers"])
```

This line will work as expected and return `["Hello", "developers"]`.

```crystal
puts first_and_last(42)
```

This is the line that breaks things. Our method expects an `Array`, but instead we handed it an `Int32`. Because of this breach of contract the compiler throws an exception.

```shell
no overload matches 'first_and_last' with type Int32
Overloads are:
 - first_and_last(arr : Array(U))

  first_and_last(42)
  ^~~~~~~~~~~~~~
```

Yes, it's still an error, but this time it's happening before you release your code. Catching bugs early is a wonderful thing.

## Type Declaration in Crystal

You've seen a little of how types are declared in Crystal, now let's look at some more examples. Types are almost always declared in the following way:

```crystal
# var_name : Type
arr  : Array(Int32)         = [1, 2, 3]
str  : String               = "Hello, world!"
int  : Int32                = 42
hash : Hash(String, String) = {"user" => "watzon"}
```

Like with Ruby, everything in Crystal is an Object and all Objects are valid Types, so custom classes, structs, etc. are also valid types. Now, there is another way to declare a type when it comes to Hashes and Arrays.

```crystal
arr : Int32
arr = [] of Int32            # Assigning type declaration

hsh : Hash(String, String)
hsh = {} of String => String # Assigning type declaration
```

With most types (Int, String, Class) you can just assign the object to a variable without explicitly declaring the type. This is called type inference and it's extremely handy. You can do the same with Arrays and Hashes as well, provided they contain data, but if they're empty as in the previous example you will have to explicitly declare the type of the Array or Hash.

### Type Inference

Type inference is a handy feature that almost gives the appearance of dynamic typing... sometimes. Let's use our first example again.

```crystal
def first_and_last(arr : Array(U)) forall U
  if arr.size > 0
    return [arr.first, arr.last]
  end
  
  [] of U
end
```

Because we explicitly declare that the parameter `arr` is an Array we know that that parameter will have several helpful methods that allow us to act on the data stored in the Array. Array, however, is not the only class to contain many of those methods. There are other enumerable classes in Crystal that have `#size`, `#first`, and `#last` methods such as `Set` and `Deque`. Currently though, you could not use any of those classes in our `first_and_last` method. You could of course do this:

```crystal
def first_and_last(arr : Indexable(U)) forall U
  if arr.size > 0
    return [arr.first, arr.last]
  end
  
  [] of U
end
```

Now any class that includes the `Indexable` module can be passed into `first_and_last`, but there is an easier, albeit less explicit way to handle things.

```crystal
def first_and_last(arr)
  if arr.size > 0
    return [arr.first, arr.last]
  end
  
  arr
end
```

But wait, where did the types go? They're still there don't worry, but now instead of you having to explicitly declare the type of the parameter `arr` the compiler will infer the type based on what operations you perform on it. There are several classes that have `#first`, `#last`, and `#size` methods, and now all of them are valid inputs.

### Union Types

One very powerful aspect of Crystal's type system is the ability to create type "unions". Here is an example of a union type.

```crystal
arr = [] of Int32 | String
```

The pipe `|` operator creates a union between two types, allowing you to use either both `Int32` and `String` types in that array. How awesome is that? Union types can also be generated dynamically by the compiler.

```crystal
arr = ["Age", 32]
```

The variable `arr` in this case would be assigned the type `Array(String, Int32)` by the compiler. This also, of course, means that any operations performed on the data in the array have to check the type of the item before doing anything, unless they are doing something that is applicable to both types. For example:

```crystal
arr = ["Age", 32]
arr.map { |a| a.chars }
```

`chars` is a method that exists on the `String` class and returns an array of all the characters in the String. The method does not, however, exist on the `Int32` class. Because of that this code will not compile. Instead you'd have to do something like the following:

```crystal
arr = ["Age", 32]
arr.map { |a| a.chars if a.is_a?(String) }
```

Things can definitely get messy when dealing with unions, so keep that in mind.

### Conclusion

Crystal's type system is very powerful and I've really only scratched the surface. If you want to learn more about it I'd suggest looking at [the Crystal reference](https://crystal-lang.org/reference/syntax_and_semantics/types_and_methods.html).

Please don’t forget to hit one of the the Ego Booster buttons (personally I like the unicorn), and if you feel so inclined share this to social media. If you share to twitter be sure to tag me at @_watzon.

Some helpful links:
https://crystal-lang.org/
https://github.com/kostya/benchmarks
https://github.com/kostya/crystal-benchmarks-game
https://github.com/crystal-lang/crystal

Find me online:
https://medium.com/@watzon
https://twitter.com/_watzon
https://github.com/watzon
https://watzon.tech