LabVIEW

cancel
Showing results for 
Search instead for 
Did you mean: 

Object Oriented Polymorphism; Abstracting Data Types at the Method's Connector Pane

I am having the same object-oriented issue as in this post, where I have a buffer class that accepts elements of various datatypes in, and I don’t want a variant input terminal, variant outputs, or unnecessary datatype wrapper classes. Is this possible when all Dynamic Dispatch members must have the same connector pane?

 

This seems like it might be the first problem people encounter with LVOOP. Is there something I am missing?

 

The class approach leads to having to create an abstract datatype class with child classes of each datatype used and Write/Read accessors that simply bundle/unbundle the data into/out of the classes. The drawbacks are the amount of work involved creating all the classes, the added complexity of the To More Specific Class stuff inside the buffer classes, and the “class bundling” that you have to do with your data when using the buffer class in actual code.

 

The variant approach has much the same effect without the need for all the class explosion. A simple Variant to Data can cast the data to the expected type inside the type-specific methods of the buffer class. However, when getting the data back out of the buffer, it’s a Variant again! The drawbacks here include the variant conversion coming out of the Get/Read methods and the variant coercion for all the Set/Write methods.

 

I’ve tried creating a polymorphic VI that has the strict types on the terminals and internally converts the variants. The data is then passed to the Dynamic Dispatch method that later converts the data back into the expected type based on the buffer class. This looks nice for the Set/Write methods, but still requires manual polymorphic instance selection (by menu or default type input terminal) for the Get/Read methods. This also has the issue of exploding the parent class with polymorphic instances for all child methods of all child classes. This also requires updating the parent class polymorphic VIs if ever you add or remove support for a child datatype.

 

This all seems like a lot of work that could be avoided if the Dynamic Dispatch methods could adapt their input terminals like a polymorphic VI does. Perhaps it could do all the work of treating everything as a variant so that I don’t have to deal with it!

 

I’m currently using LabVIEW 2015, but I could also try this in 2018 if there are new features like the malleable VI that may be helpful.

Also see this post about a malleable VI as a class member; don’t know if this made it into 2019 or not.

And this help page about a malleable VI adapting to classes; does this hold the key?

_______________________________________________________________
"Computers are useless. They can only give you answers." - Pablo Picasso
0 Kudos
Message 1 of 14
(4,802 Views)

You're correct in your assessment- this is a problem many people run into very quickly in LVOOP. DD functions must have identical connector panes.

 

For a thought experiment, let's say you have a (very simple) abstract class called "Value" that will store a single Value. Concrete child classes of Value might be Boolean, Integer, and String, and each of these data types are stored in the Child's private data.

 

I don't think you need DD here, but a Malleable VI would work just fine but it's not strictly necessary. If you're reading the class's private data from the buffer, you will probably know what type the class is, which means you're operating on a Child wire not a Parent wire. Just make each class have a separate Read Value function that returns the correct value... but **don't** give a Read Value method to the Parent. Sibling classes can have identically-named/different conpane subVI's as long as the Parent doesn't have one.

 

If you're having issues where your classes are defined as concrete classes but are being upcasted to the Abstract, then you have one of two issues. Either you're explicitly doing that (say for example, building an array of different classes) or one of your Parent VI's is doing it accidentally. In the first case, you'll have to use To More Specific Class to get it to the right child class anyway (otherwise how would the code know at Edit time what's going to be on the wire at Compile Time?). In the latter case, you can use Preserve RunTime Class to "force" LabVIEW to understand the class isn't changing within the Parent class.

 

A malleable VI will let you have just one VI you grab auto-adapt to the correct child implementation, but you can do it manually too.

0 Kudos
Message 2 of 14
(4,756 Views)

Your proposed though experiment is exactly what I did at first. However, it seems that at some point I'm still having to force a parent class to a child class or a variant to a type and check for errors with that conversion. Otherwise, how do you do something as simple as a Build Array or Replace Array Subset without knowing a strict datatype.

 


@BertMcMahan wrote:

For a thought experiment, let's say you have a (very simple) abstract class called "Value" that will store a single Value.


I steered away from holding an array of objects because they are not directly converted to an array of basic datatypes. That is, if I had an array of Value:Value DBL elements, I would have to iterate over the entire array and extract the individual DBL numeric values just to build the 1D DBL array. This seems like an incredible amount of overhead for something so simple.

 


Just make each class have a separate Read Value function that returns the correct value...

This was my original implementation that I was unhappy with. If every child class has non-dynamic methods to access its private data, this works just-OK for normal use; however, when I try to wrap this buffer class in a more specific type of buffer, I find myself needing to duplicate and expand this per-child method structure for every derived class of each type. Then what if I find a bug or want to change the implementation details slightly? I have to edit an untold number of methods. This seems totally intractable for more than a few classes (and even then it is a lot of work).

 


If you're having issues where your classes are defined as concrete classes but are being upcasted to the Abstract, ... you can use Preserve RunTime Class ...


This was not an issue for me, but I am aware of this functionality.

 

The other wrinkle in this story is that I have implemented the buffer data itself as a DVR to a 1D array of the buffered elements (like a DBL numeric). Perhaps there is some middle ground with having the data wrapped in a Value class. Maybe the buffer could be an object that contains the 1D array (Value:Value 1D DBL) and the incoming data to buffer is an object in the same class but the single element of that array (Value:Value DBL). Then perhaps the Dynamic Dispatch can at least make better assumptions about what data is incoming, and I can dump the entire buffer with a single unbundle/accessor.

 

An object cannot be DVR'd directly, but it can be converted to a variant or bundled into a cluster first (I prefer the latter). I don't see why this wouldn't work for the buffer object DVR. Maybe this is something I can explore further.

 

I still don't like "bundling" the DBL data into a [Value DBL] class before stuffing it into the buffer API, but perhaps this is just the price you pay for OO...

_______________________________________________________________
"Computers are useless. They can only give you answers." - Pablo Picasso
0 Kudos
Message 3 of 14
(4,649 Views)

I guess I'm not quite sure what you'd like to be able to do. Can you give me a specific example where you'd like the Read method to autoadapt, but which doesn't work well with non-DD child classes? It seems like all of your data handling is happening inside of the Child classes anyway, which means the Parent won't have access to it. Even if you do have a Parent class that does DD for some operation (like "Get Value As String" or something) you still have to make Child methods to actually "do the thing".

0 Kudos
Message 4 of 14
(4,634 Views)

Without having to take the time to create an example for upload, I'll try to explain the first use case I have in mind.

 

Let's say I have a data buffer parent class. It maintains sizes, limits, pointers, and other general properties and values. Then there's a DBL child class that holds a DVR to the 1D DBL array used to buffer DBL numeric values. Buffer calculations are methods/functions of the parent. Buffer manipulations using the DVR are the responsibility of the specifically-typed child classes.

 

At first, each child class had static methods (that did not exist in the parent) to access and modify the buffer. Each method had the specific data type on its terminals. This worked fine in a test example I made, and was manageable until I expanded to multiple child classes (datatypes). Now I'm having to copy and paste a lot of code while preserving the individual datatype of each class and data terminal on each method. I decided there was too much work being done in these child methods, and perhaps the strict datatypes on the terminals could be sacrificed.

 

So in an effort to move as much functionality as possible into the parent class, static methods were added to the parent that template the buffer actions. The parent class templates the accesses and modifications (e.g., add, get, clear, dump) using a dynamic dispatch subVI in the static method, and the children implement that "core" action dynamically. This much works great. But to get the data tunneled through the static parent method into the dynamic child methods, I needed to either wrap the data into a [dummy] class or convert it to a variant. I chose the latter because it was far easier and faster to implement. The coercion dot on the add method I can live with, but the Variant To Data outside the get API is rather ugly. Adding conversion helpers to the class is of course an option, but tedious to the user of the API (me).

 

My question is, why is this necessary? Can a malleable VI prevent me from having to do this variant business? Can it accept any datatype and pass it to the correct dynamic dispatch child? I think not because the malleable VI is basically polymorphic (edit time), not dynamic.

 

I haven't even gotten to the issue of using this class in another class. Say I want a class that has a lookup table of buffers so I can add data to a buffer by name. Now I have to wrap all the different [add/get/whatever]-by-name methods for each underlying buffer datatype, right? This is the part where I give up and say I must be doing it wrong because it seems like way too much work to do something that seemed so simple in my head. Do I just hack it all into variants and multidimensional arrays and go back to the old procedural way of doing things? I thought OO would be more helpful for a task such as this...

_______________________________________________________________
"Computers are useless. They can only give you answers." - Pablo Picasso
0 Kudos
Message 5 of 14
(4,616 Views)

Ah, I see. The short answer is yes, malleable VI's will do some of this, and I think the Interfaces coming in LV2020 will handle the rest.

 

The operations you're talking about are basically all array operations. Malleable VI's will let you do most any array operation on any array. Basically, if you can code up something that uses array primitives to operate, you can stick that in a malleable VI and you no longer have to worry about type conversions or checking.

 

For example: let's say you want to add a data value to a lossy buffer with a limit of n items. This boils down to:

 

If (myArray.size = n)

     myArray = myArray[1:(n-1)]

endif

myArray = myArray + myNewElement

 

All of those are array operations that can happen without ever needing to know the type of the array. If you implement that as a malleable VI, you only have to implement it one single time, and it'll work for variants, objects, doubles, booleans, whatever.

 

Basically, you'd have one parent level VI called "Add element.vim". Each child will then have its own "Read/write value" accessor vi, but the parent will NOT have a Read/write function.

 

In the malleable VI, you will have a Read value function, then your array manipulation, then a Write value function. If you save the VI as malleable, it'll automatically pick the right Read and Write values for you.

 

The drawback here is that this only works at Edit time, not at Run time (as you said), which means you do have to operate on specific Child classes, not Parent classes. If I understand it correctly, the new Interfaces coming in LV2020 will allow you to do something similar at Runtime, which makes it much easier for you to use this buffer with other classes.

 

Still, you can do this now with Malleable VI's as long as you're working with Child classes. I would guess this would fit your needs relatively well at the moment though, as typically any VI that needs a buffer will know what type of buffer it just created, so you'll either be dealing with child classes directly or can typecast it to the correct class since you know which class it'd be. If that's not applicable here, I'd like to know more about your use case.

0 Kudos
Message 6 of 14
(4,579 Views)

I think you're looking for some sort of class template solution. Like make one class, and make the compiler adapt it to the desired data type. That isn't there.

 

.vims don't really solve this. They allow you to make flexible interfaces, but the internals of the class won't adopt automatically.

 

Interfaces won't really solve this. AFAIK, Interfaces will still require the CP to be the same. And also, they force the interface on the classes that implement it, but those classes still need to implement the methods.

 

You can make a buffer that adapts to the internals with just vims though*. But it doesn't combine well with classes. But do you really need type adaptation and inheritance? Yes, that would be great of course, but except some corner cases, I think we can do without both.

 

* The trick is to use a cluster in stead of a class. Old school, but those vims will adapt... Not nearly as good as OO (no encapsulation nor inheritance) but if you really need 1> 'buffer classes' it will do the trick.

0 Kudos
Message 7 of 14
(4,548 Views)

I'm working on a VIM-based buffer here, if the OP wants to see what it looks like.

Message 8 of 14
(4,545 Views)

@drjdpowell wrote:

I'm working on a VIM-based buffer here, if the OP wants to see what it looks like.


I was looking for that, but couldn't find it. Thanks for the link.

0 Kudos
Message 9 of 14
(4,541 Views)

wiebe@CARYA wrote:

You can make a buffer that adapts to the internals with just vims though*. But it doesn't combine well with classes. But do you really need type adaptation and inheritance? Yes, that would be great of course, but except some corner cases, I think we can do without both.

 

* The trick is to use a cluster in stead of a class. Old school, but those vims will adapt... Not nearly as good as OO (no encapsulation nor inheritance) but if you really need 1> 'buffer classes' it will do the trick.


Could you expand on this a bit? vim's will adapt to classes just fine. It's a little confusing in that you have to design the vim using a class that DOES work (you can't use just any placeholder, you'd have to use a specific child class knowing it'll be replaced later on with a different specific child). The OP can do what he wants as long as he's operating on a Child class wire, which is the same as operating on a Cluster wire, no?

 

Edit: I recognize this is different from what @drjdpowell is doing, as you have to actually create a class for each subtype of buffer, but that's what OP was doing so that's what I'm trying to consider here. Doing it with only vims, and letting it autocreate the types and everything on the fly is definitely a better way to do things, I was just trying to clarify your comment about using clusters, that's all.

0 Kudos
Message 10 of 14
(4,521 Views)