http://qs321.pair.com?node_id=696538


in reply to Re: Runtime introspection: What good is it?
in thread Runtime introspection: What good is it?

This got rather long. Testimony to the thoughtfulness of your use cases; thank you again.

Here are some of the things I've used run-time reflection of some kind for, in various languages:

(My) conclusions

  1. I have no doubt that all of these use cases could be met using a language (like C) that has no introspection capabilities, if the need was strong enough. It might entail using compiler generated ancillary files, or in effect, constructing a limited reflection capability.

  2. As I think I've shown, three of the five above can not only be met using standard OO mechanisms, I would say that those three would be better met that way.

  3. The other two, (grouped together and shown last), are strong use cases for reflection, where that capability is available.

    Though whether they are common enough to warrent the inclusion of introspection in a language is open to debate. If the reflection is done, as in Java, by (as I understand it), pulling apart the bytecode at run-time, the costs when it is not used are negligable.

    But in languages or frameworks where introspection must be support through the retention of compile-time parsing tables or the construction of code tree decorations, the costs in terms of space and time can be significant enough that they should only be done on demand, through the use of compile-time switches or pragmas, not by default.

  4. In general, I think that just as inheritance was overused and abused in the poineering days OO, and still is by novice users, so introspection is equally easily abused. And 5 or 10 years from now, it will probably have gained as bad a rep. when people step back from its rising popularity on the basis of RoR and resurgent interest in Objective-C.

    Like any technology, used sparingly with understanding of the costs, it will serve some use cases in ways that nothing else can.

    But if it is overused, just because it can, to solve at run-time use cases that are better served by compile-time solutions, I see it suffering a backlash as the mainenance costs become evident.

I hope that if anyone read this far, they will find some of this as useful as I have.


Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
"Science is about questioning the status quo. Questioning authority".
In the absence of evidence, opinion is indistinguishable from prejudice.

Replies are listed 'Best First'.
Re^3: Runtime introspection: What good is it?
by sgifford (Prior) on Jul 10, 2008 at 02:55 UTC
    Hi BrowserUK,

    Glad to be able to make you think a bit! Let me try to respond to a few of your comments. But first, we should be clearer about our definitions of reflection. I'm including in my definition finding the type of an object (using Java.Lang.Class in Java or typeof in C++ or ref in Perl), and testing whether an object inherits from another object (using instanceof in Java or dynamic_cast in C++ or isa in Perl. If you don't consider these reflection, and are only thinking of getting the methods and member variables of a class, some of my examples don't really use reflection.

    You will have to know what type of shape is is, in order to instantiate the object
    Right, this part doesn't use reflection.
    For your intersection problem, any language that supports method overloading, C++, Java etc., will allow you to code methods within your Rect subclass with signatures of:
    class Rect; bool intersect( Rect* ); bool intersect( Ellipse* ); bool +intersect( Polygon* ); ...
    So that invoking someRect->intersect( someShape ); will get to the right code without introspection.
    This is called "dynamic dispatch" IIRC and the behavior I would like and expect, but unfortunately not the behavior exhibited by either Java or C++. Both determine which overload of a function/method to call at compile-time, and if all you know about the object is that it's a Shape* it will always call the overload for that type. For example, in C++:
    #include <iostream> using namespace std; class Shape { public: virtual ~Shape(){}; }; class Polygon : public Shape { }; class Circle : public Shape { }; void ShowType(Shape *obj) { cout << "Shape" << endl; } void ShowType(Polygon *obj) { cout << "Polygon" << endl; } void ShowType(Circle *obj) { cout << "Circle" << endl; } int main() { Shape *shape; shape = new Polygon(); ShowType(shape); shape = new Circle(); ShowType(shape); }
    outputs:
    Shape Shape

    To make it work, we have to use runtime reflection:

    void ShowType(Shape *obj) { if (dynamic_cast<Polygon*>(obj)) ShowType(dynamic_cast<Polygon*>(obj)); else if (dynamic_cast<Circle*>(obj)) ShowType(dynamic_cast<Circle*>(obj)); else cout << "Shape" << endl; }
    outputs:
    Polygon Circle

    This is why the distinction between knowing the type at compile-time versus runtime is important; if the compiler knows the type it can call the correct overload, otherwise it will not.


    For the "make sure it was a type I could deal with" part of the equation, if all plug-ins are derived from a base class, then the only check required is to verify that the class loaded is derived from that base class.
    Right, but IIRC the class loading code returns a Class and you have to use runtime type checking to determine if it is the right sort of class.
    Once the class is loaded, try instantiating a (minimal) instance, and exercising the required methods. Catch and report any errors. You can even check that the loaded class methods return sensible values--And that's something that no amount of reflection can do. All your validation is performed immediately after loading.
    While this is possible, it is error-prone, and violates the concept of putting things in exactly one place. If I add a new method to my class, I have to remember to go add a check for that method to all places where it is loaded dynamically. If it is loaded dynamically from 3rd party code, I have to notify those parties to check for this new method. The maintenance cost is much higher than the runtime cost of doing this check, IMHO.

    I think what you are saying here is that you can use reflection to construct a workaround to the quirk, not detect the need for it
    I'm actually simply saying to detect a need for it, by looking at the type of the object.
    the classic solution to this is to construct a subclass that inherits from the quirky class and override the troublesome methods
    Easier said than done if your code is a library being used by other programs who are creating the object in question and passing it in. All users of your code would have to switch to your subclass, then switch back when the bug in the original code is fixed, which is a maintenance nightmare.

    As far as the cost of keeping type information and reflection information around, I'm not quite sure what the cost is in different languages. I think it's quite low for C++ RTTI, and I think Perl has to keep that information around anyways, so it's also quite low. But I haven't really seen reflection overused; it seems to be just inconvenient enough that it doesn't get used unless there is a genuine need.

      Have you heard of or used "late binding"? Try it this way

      #include <iostream.h> class Shape { public: virtual ~Shape(){}; public: virtual void ShowType() { cout << "Shape" << endl; } }; class Polygon : public Shape { public: virtual void ShowType() { cout << "Polygon" << endl; } }; class Circle : public Shape { public: virtual void ShowType() { cout << "Circle" << endl; } }; int main() { Circle circle; Polygon polygon; Shape *shape1 = &circle; shape1->ShowType(); Shape *shape2 = &polygon; shape2->ShowType(); }

      Outputs:

      c:\test>696596 Circle Polygon

      This is essentially the same as "re-blessing" a blessed-reference in Perl 5.


      If I add a new method to my class, I have to remember to go add a check for that method to all places where it is loaded dynamically.

      If you add a new method, you are going to have to modify any program that uses the class anyway, otherwise it will never be called. Reflection will tell you that it's there, but it won't tell how to call it, when to call it, what it will do, or why.

      So, as you 've got to go in there and modify any program that uses the modified base class anyway, you might as well update the run-time sanity-checking code to test if the objects you are instantiating have been upgraded with that new method also.

      And checking that it appears to do the right thing at run-time is just good insurance. Think of it as adding another test to your test suite. Except that it is one that protects your code by checking that other people (whomever writes the plug-ins derived from your plug-in base class), have done theirs.


      Easier said than done if your code is a library being used by other programs who are creating the object in question and passing it in.

      Okay. Your scenario is that you are providing a library that accepts instances of the errent class, that have been instantiated by code that you did not write.

      So, you write your library in terms of the subclass of that errent class as I described above. Whenever your code gets an instance of that errent class, you use the late binding trick to "re-bless" it on the way in as as above. And then reverse the process (assignment) on the way out.


      Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
      "Science is about questioning the status quo. Questioning authority".
      In the absence of evidence, opinion is indistinguishable from prejudice.
        I seem to have oversimplified my shapes example. Certainly you can use virtual methods/late binding when you only care about the type of one object, but when you care about more than one, it's not so easy. Here's a better example: Shape::intercept has to look at run-time type information to dispatch to the right overload. You can see that because it is called first; the output is:
        (Determining type of second shape) Intersect polygon with circle

        If you add a new method, you are going to have to modify any program that uses the class anyway, otherwise it will never be called.
        It depends on how the different parts of the system are coupled. In my case, the main program simply loaded up a bunch of classes and told them to start working; they would communicate amongst themselves using their various methods. If a new method was added to one loaded class, and another loaded class started using it, the method would be called even though the main program hadn't changed at all.

        So it wouldn't work well for the type check to happen in the main program, at least. I suppose it could be broken out into a sub, like this:

        package MyModule; sub IsThisReallyMyModule { my $obj = shift; eval { $obj->method1(); $obj->method2(); # ... }; return !$@;
        but that's still extra maintenance, and MyModule::IsThisReallyMyModule($obj) is less idiomatic than $obj->isa('MyModule').
        Whenever your code gets an instance of that errent class, you use the late binding trick to "re-bless" it on the way in as as above. And then reverse the process (assignment) on the way out.
        This is certainly possible, but it strikes me as more hackish and error-prone than just checking the type and working around the bad behavior. Also, re-bless is a trick that's unique to Perl; I'm not aware of any other languages that will let you do that. Your C++ example above wasn't really changing the type in the way that a re-bless does, it was just casting an object pointer to a supertype. I don't know a way to change the behavior of an existing object at the last minute in C++ or Java.

        I think the summary of all of this is that run-time type information and reflection are one tool for solving a certain class of problems. For the most part they can be solved in other ways, which may or may not be better than using reflection. Which way is better depends on the problem, and to an extent is a matter of taste.