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

def method(stooges : Array(Moe | Curly | Larry))
  puts stooges

# This is fine

# This fails with the compiler error:
# instance variable '@stooges' of Slapstick must be Array(Curly | Larry | Moe), not Array(Moe)[])

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[] of Stooge)

# if the array comes from an variable of a different type:
stooges = [] # : Array(Moe)

# works for any `Enumerable` source, not just `Array` &.as(Stooge))

# slightly faster, but less terse { |i| stooges[i] })

# available in recent versions of Crystal[*stooges] of Stooge)

There are plans to make method parameter restrictions invariant by Crystal 2.0.


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

struct Moe

struct Curly

alias Stooge = Moe | Curly

stooges = [,]

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