Beefy Boxes and Bandwidth Generously Provided by pair Networks Cowboy Neal with Hat
Perl: the Markov chain saw
 
PerlMonks  

Re^3: Runtime introspection: What good is it?

by sgifford (Prior)
on Jul 10, 2008 at 02:55 UTC ( #696596=note: print w/ replies, xml ) Need Help??


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

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.


Comment on Re^3: Runtime introspection: What good is it?
Select or Download Code
Re^4: Runtime introspection: What good is it?
by BrowserUk (Pope) on Jul 10, 2008 at 06:39 UTC

    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.

        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:

        Okay. It took a while to drag the steps back from my memories of a former life, and I have a feeling that a few of these steps could be eliminated, but try this:

        #include <iostream.h> class Circle; class Polygon; class Shape { public: virtual ~Shape(){}; virtual void ShowType() { cout << "Shape" << endl; } virtual void intersect( Shape *that ) { cout << "Shape:intersect" << endl; } virtual void intersect_( Circle * ); virtual void intersect_( Polygon * ); }; class Polygon : public Shape { public: virtual void ShowType() { cout << "Polygon" << endl; } virtual void intersect_( Polygon *that ) { that->intersect( this ) +; } virtual void intersect( Polygon *that ) { cout << "Intersect Polygon with Polygon" << endl; } void Polygon::intersect_( Circle *that ); virtual void intersect( Shape *that ) { that->intersect_( this ); +} virtual void intersect( Circle *that ) { cout << "Intersect Polygon with Circle" << endl; } }; void Shape::intersect_( Polygon * ) { } class Circle : public Shape { public: virtual void ShowType() { cout << "Circle" << endl; } virtual void intersect_( Circle *that ) { that->intersect( this ); + } virtual void intersect( Circle *that ) { cout << "Intersect Circle with Circle" << endl; } virtual void intersect_( Polygon *that ) { that->intersect( this ) +; } virtual void intersect( Polygon *that ) { cout << "Intersect Circle with Polygon" << endl; } virtual void intersect( Shape *that ) { that->intersect_( this ); +} }; void Shape::intersect_( Circle * ) { } void Polygon::intersect_( Circle *that ) { that->intersect( this ); } int main() { Shape *circle = new Circle; circle->ShowType(); Shape *polygon = new Polygon; polygon->ShowType(); circle->intersect( polygon ); circle->intersect( circle ); polygon->intersect( circle ); polygon->intersect( polygon ); return 1; }

        Outputs:

        c:\test>696596 Circle Polygon Intersect Circle with Polygon Intersect Circle with Circle Intersect Polygon with Circle Intersect Polygon with Polygon

        This evolved in the days before RTTI. Whether I'd use it now in preference to RTTI is debateable. I guess it would depend upon the scale of the project and the affects of enabling RTTI. If it could be localised to some small part of a large project, it might be worth the costs.


        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.

Log In?
Username:
Password:

What's my password?
Create A New User
Node Status?
node history
Node Type: note [id://696596]
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others meditating upon the Monastery: (16)
As of 2014-04-18 18:19 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    April first is:







    Results (471 votes), past polls