abstract class Foo
end
class Bar < Foo
end
class Baz
def initialize(@foo : Array(Foo))
end
end
Baz.new([Bar.new])
# in temp.cr:8: instance variable '@foo' of Baz must be Array(Foo), not Array(Bar)
However it compiles when def initialize(foo : Array(Foo)) :thinking:
This is not a bug. Array(Foo) != Array(Bar). You should use Baz.new([Bar.new] of Foo) to get it to work: https://carc.in/#/r/2hi4
@straight-shoota is right, but to give some added context on why this would be very bad:
abstract class Foo
end
class Bar < Foo
end
class Bar2 < Foo
end
class Baz
def initialize(@foo : Array(Foo))
@foo.push Bar2.new
end
end
ar = [Bar.new] # type is Array(Bar)
Baz.new(ar)
puts ar # type is Array(Bar), but now it also contains a Bar2
@kirbyfan64 it's related to that union everything compiler's strategy... IMHO it's the compiler's weak spot
@vladfaust ...no, it isn't. Try googling programming covariance problem and you'll see this is a problem with any language that has generics.
@kirbyfan64 too hard for me :open_mouth:
Is it related to #4775?
No, it isn't related to unions. Covariance isn't too hard concept.
Imagine we have Array(Bar). We can consider it is an Array(Foo) for purpose of reading from it, as any Bar we can get from it is a Foo. So reading from array is called covariant.
But we can't consider Array(Bar) as Array(Foo) if we add items to it. Because we can add any Foo to Array(Foo), so if Array(Bar) is Array(Foo), we would be able to add Bar2.new to Array(Bar) (like in a @kirbyfan64 example), that is wrong. Actually, it's quite opposite - we can consider any Array(Foo) as Array(Bar) for purpose of adding elements. So adding items is called contravariant.
In Crystal currently all generics are invariant - Array(Bar) and Array(Foo) are just incompatible.But even if it has proper covariant\contravariant concept, Array would be still invariant because it allows both covariant (reading) and contravariant (adding) actions.
In your case confusion is possible due to the fact that [Bar.new] looks like a valid initializer for Array(Foo). But that's not how type inference in Crystal works - first, compiler determines a type of [Bar.new] - and it is Array(Bar). After that it tries to pass Array(Bar) to def initialize(@foo : Array(Foo)), that is expecting Array(Foo) and it's not possible due to explained above contravariance.
You have also said that def initialize(foo : Array(Foo)) compiles. That's another interesting moment. To a some extent, we can even say that Crystal actually implements covariance(not contravariance), lol. If you not initialize a field, but just pass a parameter to a function, Crystal allows passing Array(Bar) instead of Array(Foo). Actual type checking will happen in a body of function, so it won't allow you to do any contravariant actions, like adding Bar2 item to Array(Bar) thinking it's a good old Array(Foo).
TL'DR: as you can see, my answer doesn't include any unions, so the problem is not related to unions.
@konovod Maybe we should put this explanation in the FAQ or language reference...
@konovod when I firstly glanced at your response I thought like "meh, later". I've tried several more times later and now I think that leaving a university was not such a good idea :roll_eyes: Anyway, this is the cause I love programming - we, developers, always help each other and this is great! Thanks for such a detailed answer. I may be not understanding everything you wrote, but you've selflessly spent your time to help others who will ever see this issue. Spasibo!
Is that code related to covariance too? We see class variables here
class A
def name
p @@name
end
macro inherited
@@name : String = {{@type.name}}.to_s
end
end
class B < A
end
class C < A
end
a = [] of A
a << B.new
a << C.new
a.each &.name
# Can't infer the type of class variable '@@name' of A
#
# p @@name
# ^
UPD: Looks relevant to https://github.com/crystal-lang/crystal/issues/2661
Imagine you add A.new to a. You didn't that, but compiler can't know it - a is [] of A, so A.new is valid element of it, and for A class field @@name isn't declared, so compiler can't know it type.
@konovod abstract class A doesn't change the thing. Is that a BUG?!
I'm not sure about bug, but definitely an oversight\possible improvement.
What's a use case though? Can't you just add @@name = "A" to the A class? Or even @@name : String? once #4871 is merged.
@konovod take a look:
module Service
def self.send(api_method, payload)
p "Sent to external service: #{api_method}, #{payload}"
end
end
abstract class Request
def send_to_service
Service.send(@@service_api_method, @payload)
end
def initialize(@payload : Hash(String, String))
end
macro inherited
@@service_api_method = {{@type.name}}.to_s
end
end
class GetUser < Request
end
class CreateUser < Request
end
a = [] of Request
a << GetUser.new({"id" => "42"})
a << CreateUser.new({"name" => "foo"})
a.each &.send_to_service
# => Can't infer the type of class variable '@@service_api_method' of Request
Request is an abstract class, it is never going to be instantiated. But the compiler insists on specifying the Request's class variable type explicitly while it's defined in inherited macro.
Yes, one can add @@service_api_method = "undefined" to the Request, but this is odd and just an workaround, because the Request itself is never meant to be instantiated.
This should probably work, in an ideal world. I'm not sure if there will be any edge cases preventing this from being fixed though. You should open a seperate issue.
Most helpful comment
No, it isn't related to unions. Covariance isn't too hard concept.
Imagine we have
Array(Bar). We can consider it is anArray(Foo)for purpose of reading from it, as anyBarwe can get from it is aFoo. So reading from array is called covariant.But we can't consider Array(Bar) as Array(Foo) if we add items to it. Because we can add any Foo to Array(Foo), so if
Array(Bar)isArray(Foo), we would be able to addBar2.newto Array(Bar) (like in a @kirbyfan64 example), that is wrong. Actually, it's quite opposite - we can consider anyArray(Foo)asArray(Bar)for purpose of adding elements. So adding items is called contravariant.In Crystal currently all generics are invariant - Array(Bar) and Array(Foo) are just incompatible.But even if it has proper covariant\contravariant concept,
Arraywould be still invariant because it allows both covariant (reading) and contravariant (adding) actions.In your case confusion is possible due to the fact that
[Bar.new]looks like a valid initializer forArray(Foo). But that's not how type inference in Crystal works - first, compiler determines a type of[Bar.new]- and it isArray(Bar). After that it tries to passArray(Bar)todef initialize(@foo : Array(Foo)), that is expectingArray(Foo)and it's not possible due to explained above contravariance.You have also said that
def initialize(foo : Array(Foo))compiles. That's another interesting moment. To a some extent, we can even say that Crystal actually implements covariance(not contravariance), lol. If you not initialize a field, but just pass a parameter to a function, Crystal allows passing Array(Bar) instead of Array(Foo). Actual type checking will happen in a body of function, so it won't allow you to do any contravariant actions, like adding Bar2 item to Array(Bar) thinking it's a good old Array(Foo).TL'DR: as you can see, my answer doesn't include any unions, so the problem is not related to unions.