Literal Ruby types
In Ruby, a type is any object that responds to ===
(tripple-equals case equality) with a truthy or falsy value. Pretty much every object in Ruby is already a type.
For example, Ruby classes respond to ===
by checking if the given object is an instance of the class or an instance of a subclass of the class.
String === "Hello" # => true
String === 42 # => false
Objects typically respond to ===
by checking if the given object is an equivalent object, it’s essentially an alias of ==
(double-equals):
"Hello" === "Hello" # => true
"Hello" === "World" # => false
The ===
method on a Range will check if the given object is within the range:
(1..10) === 5 # => true
(1..10) === 15 # => false
Procs alias ===
to call
:
is_even = proc { |it| it % 2 == 0 }
is_even === 42 # => true
is_even === 21 # => false
How Ruby uses types
Ruby uses these types in a few different ways already. For example, in a case
statement, Ruby will use the ===
method of each case to determine if it matches the case.
case 42
when String
puts "It's a string!"
when Integer
puts "It's an integer!"
end
The above case statement is essentially equivalent to the following:
object = 42
if String === object
puts "It's a string!"
elsif Integer === object
puts "It's an integer!"
end
Ruby also uses types for pattern matching, both in and out of case statements.
case [42]
in Array[String]
puts "It's a string in an array!"
in Array[Integer]
puts "It's an integer in an array!"
end
The above pattern matching is essentially equivalent to the following:
object = [42]
if Array === object && String === object.deconstruct[0]
puts "It's a string in an array!"
elsif Array === object && Integer === object.deconstruct[0]
puts "It's an integer in an array!"
end
Ruby also uses types for methods on Arrays:
[1, 2, 3].all?(String) # false
[1, 2, 3].all?(Integer) # true
[1, "2", 3].any?(String) # true
[1, "2", 3].any?(Integer) # true
The methods all?
and any?
determine if all or any of the elements in the array match the given type by calling its ===
method. This is more efficient than passing a block and (depending on the type) usually avoids object allocations.
What about generics?
If a type is any object that responds to ===
, then we can create generic functions that generate parameterized types.
Let’s make a generic function that generates a type that checks that an object is an array and all of its elements are of a certain type.
For this example, we can just make a method that generates a proc, since as we saw above, procs respond to ===
. It would make sense to call this function Array
, but since Ruby already has a method called Array
, we’ll prefix it with an underscore _Array
.
def _Array(type)
proc { |it| Array === it && it.all?(type) }
end
Now we can use this generic function, passing it a type:
_Array(String) === [1, 2, 3] # false
_Array(Integer) === [1, 2, 3] # true
Literal has a number of built-in generics just like this.