Crystal: Inherited classes's instance cast as parent handle method calls incorrectly

Created on 22 Mar 2017  路  9Comments  路  Source: crystal-lang/crystal

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

bug compiler

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?

All 9 comments

[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?

Was this page helpful?
0 / 5 - 0 ratings