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, and here 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:
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.
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.
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.
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
.
[] 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.
puts first_and_last(["Hello", "fellow", "developers"])
This line will work as expected and return ["Hello", "developers"]
.
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.
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:
# 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.
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.
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:
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.
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.
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.
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:
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:
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.
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
Top comments (2)
Nice explanations. Crystal looks very cool. Keep the great posts!
Thanks for reading!