I’ve run into a compiler error that only happens when I’m using a generic of a union type in the initialize method of a class, but not for regular def arguments. An example:
record Moe
record Curly
record Larry
class Slapstick
def initialize(@stooges : Array(Moe | Curly | Larry)); end
end
def method(stooges : Array(Moe | Curly | Larry))
puts stooges
end
# This is fine
method([Moe.new])
# This fails with the compiler error:
# instance variable '@stooges' of Slapstick must be Array(Curly | Larry | Moe), not Array(Moe)
Slapstick.new([Moe.new])
Is this a documented limitation? My workaround is to construct an alias of all the combinations of the stooges, but that gets unwieldy very quickly.
This is probably one of the mostaskedquestions for newcomers. In short, type restrictions on def parameters are covariant for the moment, but instance variable types are not. The solutions are:
alias Stooge = Moe | Curly | Larry
# use explicit type for literals
Slapstick.new([Moe.new] of Stooge)
# if the array comes from an variable of a different type:
stooges = [Moe.new] # : Array(Moe)
# works for any `Enumerable` source, not just `Array`
Slapstick.new(stooges.map &.as(Stooge))
# slightly faster, but less terse
Slapstick.new(Array(Stooge).new(stooges.size) { |i| stooges[i] })
# available in recent versions of Crystal
Slapstick.new([*stooges] of Stooge)
There are plans to make method parameter restrictions invariant by Crystal 2.0.
The problem of covariant here is that crystal cannot convert Array(Moe) to Array(Stooge) when passed as argument, but it can usually convert a Moe to Stooge. Array(Stooge).new(stooges.size) { |i| stooges[i] } works because you initialize the new array with Array(Stooge).new, on each iteration of the block, the implicit conversion of stooges[i] (Moe) to Stooge is performed because new know it expects a Stooge (it’s told in the array type). At the end, we got well an Array(Stooge).