crystal icon indicating copy to clipboard operation
crystal copied to clipboard

`is_a?` fails when using `self.class`

Open MatheusRich opened this issue 6 months ago • 8 comments

Bug Report

I know this type check isn't necessary because of the type annotation in the method signature, but it is a simplification of the original code. I've seen a few issues about is_a?, but nothing with this specific scenario.

class MyArray
  getter elements

  def initialize(elements = [] of Bool)
    @elements = elements
  end

  def ==(other : MyArray) : Bool
    p! self.class
    p! other.class
    p! other.is_a?(self.class)
    p! other.is_a?(MyArray)
    if other.is_a?(self.class)
      return @elements == other.elements
    end

    false
  end
end

p MyArray.new([true]) == MyArray.new([true])

this outputs

self.class # => MyArray
other.class # => MyArray
other.is_a?(self.class) # => false
other.is_a?(MyArray) # => true
false

Crystal 1.16.3 [3f369d2c7] (2025-05-12) LLVM: 15.0.7 Default target: x86_64-apple-macosx11.0

MatheusRich avatar May 13 '25 13:05 MatheusRich

I'm somewhat surprised it compiles at all given normally you can't pass a method call to is_a?. Could just be that self.class is "special" so it's evaluating as like .is_a?(typeof(self.class)) which would explain why it returns false.

Blacksmoke16 avatar May 13 '25 14:05 Blacksmoke16

Some more investigation. If I change it to

if other.is_a?(self) # returns true!
  return @elements == other.elements
end

Shouldn't is_a? only receive classes?

I tried calling random methods on self but there seem to be an explicit check for self.class:

if other.is_a?(self.test)
  return @elements == other.elements
end
Error: expecting identifier 'class', not 'test'

MatheusRich avatar May 13 '25 14:05 MatheusRich

AFAIK: there are a few things at play:

  1. #is_a? is a pseudo method that the parser handles specifically and it expects the value to be a type;
  2. self can be a valid type, since the parser expects a type, self then refers to the current type (e.g. MyArray) not the variable;

My guess is that self.class is parsed as the MyArray.class type. For example in x = MyArray the x variable has the MyArray.class type (not an instance of).

So other.is_a?(self.class) doesn't work, but other.is_a?(self) does.

ysbaddaden avatar May 13 '25 15:05 ysbaddaden

@ysbaddaden that makes sense. It sounds like a bug to me, but now I'm confused whether this is the expected behavior

MatheusRich avatar May 13 '25 15:05 MatheusRich

I'm pretty sure it is as @ysbaddaden described. self inside is_a? references the class, not the instance. So it's not the same self as in p! self.class. This is entirely intended behaviour, but at the same time it's super unexpected, of course.

I don't think there's an easy way to improve this. Perhaps we should consider introducing a different identifier for the self type (for lack of any better idea: Self). But this requires some effort to implement and find a good name in the first place. And it might still cause some friction. So probably not a very high priority.

straight-shoota avatar May 13 '25 20:05 straight-shoota

@straight-shoota I'm happy to provide a PR, if you can give me any pointers.

MatheusRich avatar May 13 '25 21:05 MatheusRich

This requires a lot of careful thinking before we can implement anything. First step would be to analyze the problem (ambiguity of self between regular code and type grammar) deeper. E.g. find out where this matters, what other issues this can cause, how do other languages handle this. Then try to come up with ideas for how we can improve this. That could be language changes, but also little workarounds to improve the ergonomics.

straight-shoota avatar May 13 '25 21:05 straight-shoota

Maybe we can improve the docs of pseudo methods to explain that self can be a type?

ysbaddaden avatar May 14 '25 08:05 ysbaddaden