Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check if class inherits from another class at runtime? #2060

Closed
stugol opened this issue Jan 24, 2016 · 14 comments
Closed

Check if class inherits from another class at runtime? #2060

stugol opened this issue Jan 24, 2016 · 14 comments

Comments

@stugol
Copy link

stugol commented Jan 24, 2016

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) }
@asterite
Copy link
Member

is_a?

@stugol
Copy link
Author

stugol commented Jan 24, 2016

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

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

@asterite
Copy link
Member

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

@asterite asterite changed the title Check if class inherits from another class? Check if class inherits from another class at runtime? Jan 24, 2016
@asterite asterite reopened this Jan 24, 2016
@stugol
Copy link
Author

stugol commented Jan 24, 2016

That is a problem.

@asterite
Copy link
Member

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

@stugol
Copy link
Author

stugol commented Jan 24, 2016

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.

@asterite
Copy link
Member

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.

@Sija
Copy link
Contributor

Sija commented Nov 23, 2016

Any progress on this? Would be a good thing to have™ :)

@straight-shoota
Copy link
Member

+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.

@bew
Copy link
Contributor

bew commented Jun 1, 2017

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)

@TPei
Copy link

TPei commented Jun 20, 2017

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?

@straight-shoota
Copy link
Member

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

@asterite
Copy link
Member

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 :-)

@TPei
Copy link

TPei commented Jun 20, 2017

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants