Crystal: Check if class inherits from another class at runtime?

Created on 24 Jan 2016  Â·  14Comments  Â·  Source: crystal-lang/crystal

How do I check if a particular class inherits from another class, at runtime?

class A; end
class B < A; end
valid_bases = [B]

puts valid_bases.any? { |b| A.inherits?(b) }
feature compiler

Most helpful comment

The code provided by @bew doesn't work in the general case, when two types are combined into a virtual type (a parent time), and I'm 100% sure there's no way to implement this without changing the compiler. So please don't try any further :-)

All 14 comments

No, that doesn't work. is_a? requires a CONSTANT argument.

https://play.crystal-lang.org/#/r/qex

Oh, at runtime. It's not possible right now.

That is a problem.

If you tell us in which scenario you need this, we might consider adding it.

Sure. I'm writing a unit testing framework.

module Assert
    def self.raises(exceptions = [] of Exception : Array(Exception))
        yield
    rescue e : Exception
        raise e unless exceptions.none? || exceptions.includes?(e.class)
    else
        raise AssertException
    end
end

The problem is as follows:

class ConnectionException < Exception; end
class NotConnectedException < ConnectionException; end
class ConnectionLostException < ConnectionException; end
test "something" do
    Assert.raises([ConnectionException]) do
        ...some code...
    end
end

The test will only succeed if ConnectionException is raised; not if any of the derived exceptions are raised.

That's a good use case. In fact we stumbled upon this on our spec library. We "solved" it by using a macro, which basically pasts the type into the rescue. You could do the same with many exceptions by receiving a splat, iterating them and generating one rescue for each one.

I'll leave this issue open because it could be done in a simpler way, if we could check this at runtime.

Any progress on this? Would be a good thing to haveâ„¢ :)

+1 to this. Right now at runtime it is only possible to check if an object is a direct instance of a klass with klass == obj.class. The comparison fails if the object's type is a subclass of klass. This greatly limits the power of inheritance.

There is a way using macro defs:

class A; end
class B < A; end
valid_bases = [B]

class Class
  def <(klass : T.class) forall T
    {{ @type < T }}
  end
end

puts valid_bases.any? { |b| b < A } # => true

puts [String].any? { |b| b < A } # => false

https://play.crystal-lang.org/#/r/244c

Note: It doesn't work in the general case, e.g: when two types are combined into a virtual type (a parent type)

Hmm, I have some weird behaviour with your code @bew

class A < Exception; end
class B < A; end
class C < Exception; end
class D < Exception; end

exceptions = [A, C]

exceptions.each do |klass|
  puts klass # => A  |  C
  puts D < klass # => true  |  true
end
puts D < A # => false
puts D < C # => false

If I loop over the classes (same with using .any?) the behaviour is different from just checking it. Any ideas why that might be?

exceptions.class is generalized to Array(Exception:Class) which makes some kind of sense but klass.class is then also Exception:Class (instead of A:Class and C:Class).
Therefore < on D will be called with an argument of type Exception:Class and D < Exception is true.

Here is a more specific and working example:

class A < Exception; end
class B < Exception; end

class Class
  def <(klass : T.class) forall T
    {{ @type < T }}
  end
end

a = (A).as(Exception.class)

puts a == A  # => true
puts a.class # => Exception.class - should be A:Class
puts B < a   # => true - should be false
puts a.class == A.class # => false - should be true

https://carc.in/#/r/27z3

The code provided by @bew doesn't work in the general case, when two types are combined into a virtual type (a parent time), and I'm 100% sure there's no way to implement this without changing the compiler. So please don't try any further :-)

Thanks for the response, too bad it won't work ^^

Was this page helpful?
0 / 5 - 0 ratings