Buggy example
class ResultSet
abstract def read
def read(type : T.class) : T forall T
read.as(T)
end
end
class MyResultSet < ResultSet
@iter = 0
def read : Nil | String | Int32
res = case @iter
when 0 then nil
when 1 then "one"
else @iter
end
@iter += 1
res
end
def read(type : Int64.class) : Int64
read.as(Int32).to_i64
end
end
# Following code works
myrs = MyResultSet.new
p myrs.read(Nil)
p myrs.read(String)
p myrs.read(Int32)
p myrs.read(Int64)
# Following code doens't work !!!
myrs = MyResultSet.new.as(ResultSet)
p myrs.read(Nil)
p myrs.read(String)
p myrs.read(Int32)
p myrs.read(Int64) ## <-- this one breaks
###
### Error in a.cr:40: instantiating 'ResultSet+#read(Int64:Class)'
### p myrs.read(Int64)
### ^~~~
### in a.cr:5: can't cast (Int32 | String | Nil) to Int64
### read.as(T)
###
It looks like when cast as parent, it doesn't search in original classes's instances methods.
EDIT: simpler example
EDIT: original example
[invalid example]
Another example without generics
class Animal
def milk(type : Int32.class) : Int32
1
end
end
class Cow < Animal
def milk(type : String.class) : String
"2"
end
end
# Following code works
cow = Cow.new
p cow.milk(Int32) # 1
p cow.milk(String) # "2"
# Following code doens't work !!!
cow = Cow.new.as(Animal)
p cow.milk(Int32) # 1
p cow.milk(String) # <-- this one breaks
### $ crystal a.cr
### Error in a.cr:22: no overload matches 'Animal#milk' with type String:Class
### Overloads are:
### - Animal#milk(type : Int32.class)
###
### p cow.milk(String) # <-- this one breaks
### ^~~~
A reduced version would be
abstract class ResultSet
def read
true ? 0i32 : nil
end
def read(type : T.class) : T forall T
read.as(T)
end
end
class MyResultSet < ResultSet
def read(type : Int64.class) : Int64
1i64
end
end
myrs = MyResultSet.new
p myrs.read(Int32)
p myrs.read(Int64)
myrs = MyResultSet.new.as(ResultSet)
p myrs.read(Int32)
p myrs.read(Int64)
Without the cast the compiler is able to infer that no other classes could be called.
But with the .as(ResultSet) the method call could "potentially" call the base read(T) implementation that is performing a 0i32.as(Int64).
I wouldn't say that your second sample should work. Because you are extending the interface in Cow with respect the Animal one. On the ResultSet sample at least the interface is the same, yet it won't ever compile the base implementation for the call with Int64:class, which is what triggers the issue.
@bcardiff Oh, you're right
@bcardiff But is that expected behavior?
So, the code that is complaining is equivalent to:
abstract class Foo
def read
0i32.as(Int64)
end
end
class Bar < Foo
def read
1i64
end
end
pp Bar.new.read # ok
pp Bar.new.as(Foo).read # can't compile
If foo wasn't abstract I would say that the behavior is correct. Foo#read is unable to compile and there could be a Foo potentially.
But Foo is abstract, the only concrete Foo's are Bar's so there is no actual need for the Foo#read method body. So it could compile, but I see it as a corner case.
If there was no clear default implementation the method could be abstract:
abstract class Foo
abstract def read
end
class Bar < Foo
def read
1i64
end
end
pp Bar.new.read # ok
pp Bar.new.as(Foo).read # ok! :-)
You found the issue due to metaclass arguments and how overloading might overwrite.
On another hand there is an order problem here:
class Foo
def read
0i32
end
def read(type : T.class) forall T
read.as(T)
end
def read(type : Int64.class)
1i64
end
end
foo = Foo.new
p foo.read(Int32)
p foo.read(Int64) # does not compile
But changing the order of the method
class Foo
def read
0i32
end
def read(type : Int64.class)
1i64
end
def read(type : T.class) forall T
read.as(T)
end
end
foo = Foo.new
p foo.read(Int32)
p foo.read(Int64) # compiles
That's quite interesting. Right now I fixed my problem differently
abstract class ResultSet
abstract def read
def read(type : T.class) : T forall T
read.as(T)
end
end
class MyResultSet < ResultSet
def read(type : Int64.class) : Int64
1i64
end
def read
(true ? 0i32 : nil).as(Int64 | Int32 | Nil)
end
end
myrs = MyResultSet.new
p myrs.read(Int32)
p myrs.read(Int64)
myrs = MyResultSet.new.as(ResultSet)
p myrs.read(Int32)
p myrs.read(Int64) # now this works
In #4247 there are some additional shorter samples and discussions.
It seems like there are many conflated issues here, some of which are fixed by now. That is I can't reproduce the ordering problems anymore in Crystal 0.29 and the original example fails due to defining an abstract method on a non-abstract class being invalid now.
I would vote to close this and let any more specific part of this resurface as a new issue as it's a problem for someone. Any objections?
Most helpful comment
It seems like there are many conflated issues here, some of which are fixed by now. That is I can't reproduce the ordering problems anymore in Crystal 0.29 and the original example fails due to defining an abstract method on a non-abstract class being invalid now.
I would vote to close this and let any more specific part of this resurface as a new issue as it's a problem for someone. Any objections?