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) }
No, that doesn't work. is_a? requires a CONSTANT argument.
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
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 ^^
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 :-)