Jump to content

Late-Bound Data and Type Reflection


Recommended Posts

I'm looking into creating some kind of late-bound type reflection into some of my objects. For now I'm restricting myself to data fields, though there's no reason methods shouldn't be implementable as well. I'm wondering if anyone else has thought about this?

If the buzzwords are foreign to you, I basically want to be able to determine what properties are defined in an object at run-time, and be able to query/change the values. This is explicitly not dynamic dispatching. I'm not looking to query the sum total of an object's interface, but have a class expose a subset of it's interface.

A warning: I'm sort of thinking out loud here, I don't have anything at this point. I find discussions on lava help a lot, so hoping to get some feedback because there are some very well educated people here with regards to LabVIEW OOP.

Requirements: the resulting interface should be enumerable, type safe, and extendable.

Enumerability exists to allow code to be able to query the interface of the object and determine what can be operated on. The querying code could be completely foreign, or exist within an extending class. At this point I'm not sure if I want to force public visibility of the resulting data, or actually deal with public/protected/community level scopes somehow.

Extendable is an obvious feature that is desired: if I have a class that defines an interface, any extending classes should be able to further extend or override members of that interface.

So what are we to do? Invent an interface that essentially allows us to define another layer of interface.

I've iterated through a few ideas, and this is where I've landed. Let's define the class LBObject (late-bound object). The heart of the implementation would be some sort of dictionary with the values of each item being stored as a variant: the variant allows for type information to be kept yet allows anything to be stored. The implementation details of the dictionary at this point is undefined.

A pair of methods would exist to provide access to interface elements defined in this dictionary, and enumerating the interface is as simple as returning an array of names defined in the dictionary:

Get(inout LBObject lbo, inout Error err, in string name, out variant value);Set(inout LBObject lbo, inout Error err, in string name, in variant value);GetNames(inout LBObject lbo, inout Error err, out string[] names);

If visibility is to be considered, sets of these methods might need to exist for each protected, public, and community scope.

The set method would be responsible for checking that the type of supplied value is consistent with the stored value, and would return an error if a particular name is not defined.

Type safety would demand that some sort of contract be established ahead of time on what a default set of values are:

[dynamic, protected] InitializeLBItems(inout LBObject lbo, inout Error err);

This method would be called automatically by the LBObject methods any time an operation is requested for an uninitialized LBObject wire, the responsibility of override methods is to create their interface via another method:

[protected] CreateLBItem(inout LBObject lbo, inout Error err, in string name, in variant value);

The CreateLBItem is essentially a Set operation, but prior existence of the item is not verified, though type-consistency would be enforced for items already defined such that overrides don't mutate. An InitializeLBItems override should call their parent implementation, then operate on the object, thus allowing the interface to be extended or overriden.

Implementing this doesn't seem like it will be to hard. The dictionary can be handled a few ways, I see the best way perhaps with a variant. The value of the variant can be a simple boolean indicating if the interface has been initialized. Any call to Get/Set/GetNames would check the initialization state and call InitializeLBItems if required. The dictionary entries would be stored as attributes in the variant.

A class storing its data fields in the LBObject interface could still create properties via a pair of Get/Set VIs which simply call the LBObject accessors rather than accessing data in the class' private data cluster.

The downside to all of this is obviously it will be slow compared to compiled code, but that's really a true of any late-bound mechanism.

Ideas?

-michael

Link to comment

Haven't digested the whole things yet, but here are some initial thoughts...

I'm wondering if anyone else has thought about this?

Well, I thought about it while I was developing the Interface Framework as a way for callers to get details at runtime about the interfaces an object has implemented. I even implemented a little bit, but I didn't pursue it. I think I included a way to query the names of the interfaces an object has implemented, but decided for several reasons that the calling code has to know which interface it wants to use and how to use it.

Requirements: the resulting interface should be enumerable, type safe, and extendable.

...

The heart of the implementation would be some sort of dictionary with the values of each item being stored as a variant: the variant allows for type information to be kept yet allows anything to be stored.

You lost me here. I guess I need a bit of clarification on exactly what you mean by "type safe." In Labview, I equate type safety with strong typing. i.e. Mistakes generate compiler errors instead of run time errors. By that definition once you've exposed a variant on your public api you've lost all type safety. From the user side, if I see a sub vi with a variant terminal I'd expect it to accept all variants, not throw a runtime error if the variant doesn't contain a "string variant" or "boolean array variant."

But like I said, I'll have to digest more later...

Link to comment

You lost me here. I guess I need a bit of clarification on exactly what you mean by "type safe." In Labview, I equate type safety with strong typing. i.e. Mistakes generate compiler errors instead of run time errors. By that definition once you've exposed a variant on your public api you've lost all type safety. From the user side, if I see a sub vi with a variant terminal I'd expect it to accept all variants, not throw a runtime error if the variant doesn't contain a "string variant" or "boolean array" variant.

Dak, thanks for the quick feedback. I see how this could be ambiguous. Hence the need for discussion!

What I meant was that if a class defines an LB property as a double, extending classes will have to honor that contract by only using that property only as a double. As you implied, even though the data is a variant, it has embedded type information. The idea of generating compiler errors in this case is impossible, since the evaluation of the existence of a given property won't be performed until run-time, hence we can only be dealing with run-time errors propagated through error clusters. Perhaps type safety is the wrong word?

I'll also note that strict typing does not always equal type safety. Some languages such as javascript have strongly typed data, but do not enforce any form of safety: what used to be defined as a Number could be replaced with an Array or Function, for example. In a compiled language like LabVIEW the two properties are often inseparable because you always know the strict type ahead of time, but once you start doing run-time evaluation this assumption breaks down. However nothing is stopping an interface from enforcing consistent types at run-time once something has been defined.

Link to comment

What I meant was that if a class defines an LB property as a double, extending classes will have to honor that contract by only using that property only as a double.

And if I understand correctly you want the base LBO to be able to define new properties at runtime, and force child classes to adhere to the rules of that new property? (Otherwise I don't see what you're gaining by using a variant. If all the interfaces are defined at edit time you can get what you want--as I understand it--and maintain strong type safety. Of course, I strongly suspect I haven't wrapped my head around the bigger picture yet...)

Link to comment

And if I understand correctly you want the base LBO to be able to define new properties at runtime, and force child classes to adhere to the rules of that new property?

Yes. I'll see if I can come up with a demonstration soon, probably will have to wait for the weekend though.

Link to comment

Hmm... interesting problem. Can I ask what your real-world use case is? This kind of capability appears to be far outside how Labview is intended to be used. My gut instinct is to look for alternative solutions, possibly even using a different language.

Still, I am strangely intrigued by this idea...

Link to comment

This kind of capability appears to be far outside how Labview is intended to be used. My gut instinct is to look for alternative solutions, possibly even using a different language.

Indeed, which is part of the reason this pursuit is entirely academic at this point. I don't have a use case for the entire lot of my discussion, but I've found myself having implemented parts of it entirely by accident. I have an application that interacts with other executables which I don't have control over. There are a lot of configuration options for these executables, and the user is will occasionally have to tweak them. Currently I allow the user to define an unbounded number of name/value pairs of configuration options which get passed to the executable when run. These settings are all bound at run-time, that is they don't map to fields which I've set out in the development environment: while I might know some of the possible configuration settings, I don't know them all and they are liable to change as the executables evolve.

The implementation I have now simply keeps these settings in a dictionary, which becomes part of the application's data document and/or global configuration defaults depending on context. At this point I'm pretty much just implementing a late-bound enumerable interface. There is no type-safety, nor inheritance in the system (and there doesn't need to be, works fine the way it is).

This just got my mind going as to what happens if I ever had a situation where I would have to start inheriting objects like this? What would the requirements be? I'd like to be able to define types for everything, and if so, I'd also like to ensure that those types are enforced at all levels of the hierarchy, hence my original post. Near as I can tell the only problem with what I've described is the underlying properties are always tied to the base class: there's never any knowledge of what class actually "owns" the property. But I'm not even sure such a distinction needs to ever be made, or even if it can.

Link to comment

Indeed, which is part of the reason this pursuit is entirely academic at this point.

It's all academic until we get paid for it... then it's progressive. :lol:

This just got my mind going as to what happens if I ever had a situation where I would have to start inheriting objects like this? What would the requirements be? I'd like to be able to define types for everything, and if so, I'd also like to ensure that those types are enforced at all levels of the hierarchy, hence my original post.

<Thinking out loud>

I assume you're passing config options to the other executables via command line options? How are you going to dynamically discover the data type each config option accepts? You might be able to extract that information from the 'SupportApp.exe /?' text, but that's an ugly solution. Making your program's operation dependent on a help file author is asking for trouble.

I suppose a safer solution is to itemize the options and data types in a config file your base class reads when the app is started. Then your Set ConfigOption method calls the appropriate type checking code based on the config file. 'Course, with this scheme you'll have to add new type checking code any time a new type is added to the configuration options.

Or (to complete the circle) you could flatten the data to a variant, extract and store the type information in a dictionary, and check the dictionary every time the Set ConfigOption method is called. (You know, like what you said in the original post. :lol: ) The main difference between this and the config file implementation is with this implementation the code that creates dictionary entry also defines the data type associated with the entry. Might be an issue... might not...

As far as enforcing types through all levels of the hierarchy, if Set ConfigOption is written so it can handle any arbitrary data type (variant parsing), make it static dispatch. Child classes don't need to override it. (Seems too simple... what am I missing? [See Dak's First Law])

If you were to go with a config file implementation, each class in the hierarchy maintains a list of config options assigned specifically to it and can only check those options, so you need to to override Set ConfigOption. At each level of the hierarchy the class checks to see if it recognizes the input key. If so, it does the type checking and returns the result of the operation. If it doesn't recognize the input key it calls the parent method, which repeats the process.

Near as I can tell the only problem with what I've described is the underlying properties are always tied to the base class: there's never any knowledge of what class actually "owns" the property. But I'm not even sure such a distinction needs to ever be made, or even if it can.

I don't think you need to worry about this. (At least, I can't think of a reason why you would need to worry about it.) I think of it like this...

At runtime there are no classes... only objects. Once you instantiate a grandchild object it will always be a grandchild object. It doesn't contain a parent object and a grandparent object within it; it only contains the same data types they use. (Under the hood it might actually contain a parent object, but it isn't accessable to us.) Calling Parent:Set ParamY on the grandchild object doesn't change the parent object because there is no parent object. All the data is "owned" by the grandchild object, even though it doesn't have unfettered access to that data. Why does it matter where exactly within the grandchild object the data is stored as long as it is available?

Link to comment

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

By using this site, you agree to our Terms of Use.