Crystal allows for a variable to consist of multiple types. This is called a union type. In Crystal, it is common for a union type to be inferred by the compiler.
A union type is still a single type at runtime, even if it consists of multiple types.
This means that if a union type is built of String
and Int32
, it will not be both at the same time.
Instead, it will be either a String
or an Int32
.
A union type is declared by separating the types with a pipe (|
).
They are often placed in parenthesis, but it is not required.
The most common union type is (T | Nil)
where T
is a type, which can occur in methods that can return Nil
.
This is also known as a variable being nilable.
foo : String = "Hello"
foo = nil # Error: type must be String, not (String | Nil)
foo : (String | Nil) = "Hello"
foo = nil
It is not limited to just two types, but can be as many as you want.
foo : (String | Int32 | Nil | Float64) = "Hello"
foo = 1
foo = nil
foo = 1.0
typeof
vs Object#class
There are two ways to get the type of a variable.
Either by using typeof
or by using Object#class
.
The difference is that typeof
will return a variable's type at compile time, while Object#class
will return the type at runtime.
This means that if you want to see if a variable is a union type, for example, Object#class
will not be able to tell you that as it will only return the type at runtime, which is a single type.
foo = 0 == 0 ? "a" : 1
typeof(foo) # => (String | Int32)
foo.class # => String
As a union type is a single type at runtime, all the standard operations work on it. But when compiling the code the compiler will need to know which type it is. Thereby the code has to be setup in such a way that it can only be one of the types when wanting to use the type-specific operations.
foo : (String | Int32) = "Hello"
foo.downcase # Error: undefined method 'downcase' for (String | Int32)
Crystal does have a particular method for union types: the is_a?
method, which takes a type as an argument and returns a boolean.
The nil?
method is a shortcut for is_a?(Nil)
.
Putting the is_a?
method in a control expression will tell the compiler which type it is and thereby guarantee that it is that type.
And for an else branch it will be guaranteed that it is not that type.
foo : (String | Int32) = "Hello"
if foo.is_a?(String)
 typeof(foo)  # => String
 foo.downcase # => "hello"
end
This is_a?
is not limited to having a single type as an argument but can also have a union type.
It can also be combined with &&
for multiple types.
The is_a?
method when using it in conjunction with a control expression can't be an instance variable or class variable.
Instead these have to be assigned to a local variable first.
One way of making a union type into a single type is by making it so that a branch can only be entered if the type is a specific type.
Another approach is to use the as
method.
This will make an union type into a single type by doing a runtime check.
An exception will be raised if the type is not the expected type.
foo : String | Int32 = "Hello"
foo.as(String).downcase # => "hello"
foo.as(Int32) # Error: can't cast String to Int32
This approach is only meant for when you are sure that the type is the expected type or if you want to raise an exception when it is not.
Using this approach with an improper setup can lead to unexpected behavior.
as?
works very similarly to as
, but it will return' nil' instead of raising an exception if the type is not the expected type.
This means it will return a union type of the expected type and Nil
.
foo : (String | Int32) = "Hello"
foo.as?(String).downcase # => "hello"
foo.as?(Int32) # => nil
Nilable means a variable can be either a type or Nil
.
This can be written as (T | Nil)
.
But since Nilable types are relatively common, there is a shorthand for it: T?
.
# This:
foo : (String | Nil) = "Hello"
foo = nil
# Is the same as:
foo : String? = "Hello"
foo = nil
The company you work for is just about to launch its brand new smartphone, called the Smarty 5, it features a brand new camera system, a new design, and a revolutionary new processor running on a new operating system called smartyOS.
The new processor has the power to handle more secure passwords than ever before, and the company has decided to use this to its advantage. They have asked you to implement a password lock system for the phone that will allow the user to set a password and then check if a given password is correct.
The password system practiced in this exercise is not secure and is only used for educational purposes. It should NOT be used in any real-world applications.
The phone allows users to use multiple different types of passwords, from a simple digit password to a more secure alphanumeric password and even a password that stores their fingerprint.
These different types of passwords use different types: the digit password uses an Int32
, the alphanumeric password uses a String
, and the fingerprint password uses a Float64
.
Implement the class PasswordLock
with an initializer that takes any of the three types of passwords as an argument and stores it in an instance variable called @password
.
password_lock = PasswordLock.new(1234)
# => #<PasswordLock:0x7f8e1b8c0b80 @password=1234>
The company has decided to encrypt the password so that it is not stored in plain text and has asked you to implement a method for encrypting it.
Each password type has its unique way of being encrypted:
Int32
password: The password is divided by two and rounded to the nearest integer.String
password: The password is reversed.Float64
password: The password is multiplied by four.Implement an instance method called encrypt
that takes no arguments and modifies the @password
instance variable so that it is encrypted.
password_lock = PasswordLock.new(1234)
# => #<PasswordLock:0x7f8e1b8c0b80 @password=1234>
password_lock.encrypt
# => #<PasswordLock:0x7f8e1b8c0b80 @password=617>
The company has also asked you to implement a method that checks if a given password is correct.
They want the method to return nil
if the password is incorrect and "Unlocked"
if the password is correct.
Implement an instance method called unlock?
that takes the password to check as an argument.
The method should return nil
if the password is incorrect and "Unlocked"
if the password is correct.
password_lock = PasswordLock.new(1234)
password_lock.encrypt
password_lock.unlock?(1234)
# => "Unlocked"
Sign up to Exercism to learn and master Crystal with 26 concepts, 133 exercises, and real human mentoring, all for free.