Compiler error for array of union types

Hey all,

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.

1 Like

This is probably one of the most asked questions 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.

4 Likes

Could you please explain more on this? i see no different in this case?

struct Moe
end

struct Curly
end

alias Stooge = Moe | Curly

stooges = [Moe.new, Curly.new]

p! stooges         # => [Moe(), Curly()]
p! typeof(stooges) # => Array(Curly | Moe)

x = Array(Stooge).new(stooges.size) { |i| stooges[i] }

p! x         # => [Moe(), Curly()]
p! typeof(x) # => Array(Curly | Moe)

p! stooges == x # => true

Hello @zw963,

In your example it’s doesn’t change the type because stooges is already a Array(Moe|Curly)=Array(Stooge). You forgot to add Larry to a Stooge :).

Still don’t understand the key.

What i don’t understand is, How

Array(Stooge).new(stooges.size) { |i| stooges[i] }

do the type covariant?

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).

4 Likes