Generic Programming
Fundamentals
In programming, we often abstract out common functionalities. For example, when designing a sorting algorithm, we typically don't rewrite the algorithm's code for specific data wherever sorting is needed. Instead, we abstract this algorithm into a subfunction (sub VI) and pass the data to be sorted as a parameter. Since the abstracted algorithm is independent of specific data, the same algorithm can be applied to different datasets, such as sorting the arrays [1,3,2]
and [5,6,4]
.
Generic programming is a programming paradigm that abstracts algorithms even further, away from specific data types. Algorithms written using generic programming are not only independent of specific data but also of specific data types. For example, a generic sorting function could sort an array of integers, an array of strings, order a set of apples by size, or sort a group of students by their grades, and so on.
Previously, when creating sub VIs, the data type (control type) for inputs and outputs was fixed once chosen. For instance, a sub VI using a numeric control for input data could only process numeric data. Connecting a string data wire would result in a syntax error. To utilize generic programming in LabVIEW, we need to write a type of sub VI that can accept different types of data for its parameters. You might have noticed that many of LabVIEW's built-in functions support generics, such as the addition function, which can accept numeric types as well as clusters or arrays (as shown below). Another example is the array-related functions, which can operate on arrays of both numbers and strings.
If a VI uses a variant as its parameter data type, then its parameter can accept any type of data. Does this mean it supports generic programming? (Or, like in programming languages such as Python 2.0, where variables do not need to have a defined type, and any type of data can be passed to the same variable, does this mean it inherently supports generic programming?)
This isn't enough. For a programming language to support generic programming, it must not only allow a single parameter to support different data types but also possess static type checking capabilities. Static type checking means that if the code uses an incorrect data type, the program will report an error during compilation rather than waiting until runtime to discover the error. For instance, if an algorithm can support numeric and string data types but not boolean data types, then there should be a mechanism where connecting boolean data type to this algorithm's sub VI immediately triggers a syntax error, without needing to run the VI to identify a data type error. Obviously, a sub VI that uses a variant data type cannot achieve this, as passing any type of data to a variant won't cause a syntax error. Even more complex scenarios, like creating an array of string types and then operating on this array with other array functions (such as insert, index, etc.), should only accept string data types, and connecting other data types should also result in an error.
In mainstream programming languages, two common methods are employed to support generic programming:
One method is the type erasure technique, represented by Java. When the compiler compiles Java code with generics, it performs type checking and type inference. If it encounters a type error, it reports an error. However, the executable code generated by the compiler is generic-free, meaning the data type information is erased. Thus, the same piece of code can be utilized by the Java Virtual Machine to process various types of data, allowing Java's generic programming to support multiple data types while ensuring type safety.
For example, in the code, types like List<Int\>
or List<String\>
are defined. During compilation, the compiler checks whether the data passed is of type Int or String, etc. After compilation, they all turn into List. What the Java Virtual Machine sees is just List, so related functions can operate normally regardless of the List's data type.
Another approach is the code injection (code bloat) technique, represented by C++. In C++, when writing generic functions or classes, you're not directly writing a specific function or class but a template for that function or class. For instance, if you need a List that supports generics, you must write a template for List. In places within the program that use types like List<Int>
or List<String>
, the compiler automatically generates code for the specific data types from the template during compilation. List<Int>
and List<String>
generate different codes. During program compilation, the compiler checks the code for data type errors to ensure type safety. Due to a large amount of code automatically generated from templates, the code space will be quite large. Compared to Java's support for generics, C++'s technique trades space for time.
In LabVIEW, there are also several methods to write VIs that support many different types of data, which will be introduced one by one in this section.
Utilizing Variants as Sub VI Parameter Types
The Variant Data Type
In LabVIEW, the Variant stands out as a unique data type, capable of being transformed from any other data type through the "Programming -> Cluster, Class & Variant -> Variant -> To Variant" function. When required, the variant can be converted back to its original data type using the "Variant To Data" function. This variant data type mirrors the Variant in VB and the void data type in C.
Type Transformation
Upon converting different data types into a variant, the variant retains the original data's type information. Hence, the "Variant To Data" function can only revert a variant to its original data type, not to any other type. For instance, the program below shows a cluster data type being converted into a variant, which is then restored back to a cluster:
On the variant control, choosing "Show Type" and "Show Data" from the context menu allows for the display of the original data's type and value within the variant, based on the user's preference:
In scenarios where the original type of variant data needs to be identified within the program, the "Get Type Information.vi" from the "Data Type Parsing" sub-palette under the "Variant" functions palette can be employed to extract the variant's original data type. Subsequent actions can then be tailored according to its original data type. This "Data Type Parsing" sub-palette also includes various VIs for dissecting data types, enabling the parsing of diverse data types, such as identifying the data type of each element within a cluster:
The program depicted below converts cluster type data into a variant, followed by an analysis of the data type:
Utilizing Variants for Sub VI Parameters
Variant Data Type
In LabVIEW, the Variant data type is somewhat unique, capable of being converted from any other data type via the "Programming -> Cluster, Class & Variant -> Variant -> To Variant" function. When needed, the "Variant To Data" function can revert it back to the original data type. The Variant type is akin to the Variant in VB or the void data type in C.
Type Conversion
When converting other data types to a Variant, the Variant retains information about the original data type. Therefore, the "Variant To Data" function can only revert a Variant back to its original data type, not any other type. For example, the following program converts cluster data to a Variant and then back to a cluster:
You can choose to display the type and value of the original data within the Variant control by selecting "Show Type" and "Show Data" from the right-click context menu:
To extract the original data type from a Variant in your program, use the "Get Type Information.vi" from the "Variant" palette's "Data Type Parsing" subpalette. This palette contains various VIs for parsing data types, such as determining the data type of each element in a cluster:
The following program converts cluster data to a Variant and then parses the data type:
Applying Variants
Generally, LabVIEW sub VIs have fixed parameter data types, meaning a sub VI can only operate on a specific type of data. However, some algorithms can be applicable to multiple data types. It's not efficient to create multiple sub VIs for an algorithm to handle various data types. An effective method is to use the Variant data type as the sub VI's parameter. This way, any type of data, once converted to a Variant, can invoke this sub VI. Variants are also used when different data types need to be stored in the same array. By converting all data to Variants first, an array of Variants can be formed.
Using Variants as parameters doesn't fully achieve generic programming because it lacks the ability for static type checking. Static type checking means identifying incorrect data types during program compilation rather than at runtime. The VIs for parsing data types from a Variant, introduced earlier, operate at runtime and can only perform dynamic type checking. Despite this, Variants remain crucial in certain applications, so this section details how to pass different types of data with Variants and discusses the limitations of the Variant type.
Imagine needing a sub VI with addition functionality that supports both numerical and string data types. If two numerical values are input, the output should be their sum; if two strings are input, the numerical values they represent should be added and output as a string. Using a Variant as the parameter data type allows the sub VI to handle multiple data types. The sub VI for this purpose is shown below.
The "Variant Addition.vi" has Variant as its parameter data type, allowing it to accept any type of data:
Although using Variants as sub VI parameters allows handling multiple data types, there are drawbacks.
First, its type safety is poor. For example, the algorithm can only process two numerical values or two strings. However, connecting any type of data to this sub VI or inputting different types of data for parameters x and y won't cause an error. An error is only discovered when the sub VI attempts data type conversion during execution.
Ideally, such errors should be detected and flagged during programming for timely correction. This means, if the input data type is incorrect, the VI should immediately be prevented from running and prompt an error message, rather than waiting until runtime to discover the issue.
Secondly, since the sub VI's output is also a variant, subsequent programs must add an extra step to convert it to a specific data type for further use.
Therefore, using Variant data types as parameters doesn't perfectly solve the problem of a VI supporting multiple data types. A better solution for this kind of issue is to use polymorphic VIs, which will be introduced next.
Implementing Map Container Functionality with Variants
The Variant function palette in LabVIEW includes some useful functions for working with Variant attributes, such as "Get Variant Attribute", "Set Variant Attribute", and "Delete Variant Attribute". Variant data can have attributes composed of a unique name and data of any type. For example, waveform data converted to a Variant type can have added properties for extra information, such as a property named “Device Model” for storing hardware information, or “Operator” for personnel details. However, this is not an ideal way to store information. If these details are required, it's better to design specific data structures (like clusters or classes) or file formats for storing data, rather than relying on the overly flexible and error-prone Variant data type.
Despite this, Variant properties have their advantages, such as their underlying implementation using a hash table data structure, which ensures exceptionally fast write and search speeds. Before LabVIEW introduced the Map data type, programmers often used Variant properties to store key-value type data for fast read/write access. Here's a specific example:
Imagine we need a program to look up student grades by name. The data is in a table format, with the first column for "Name" and the second for "Grade". When a user inputs a name, the program finds the corresponding grade and returns it. Each entry in the grade sheet (each row) can be considered as a property of a Variant, with the student's name as the property name and their grade as the property data. To create and modify the grade sheet, we use "Set Variant Attribute" and "Delete Variant Attribute" functions; to query a student's grade, we simply use the "Get Variant Attribute" function:
The program produces the following results:
While using Variant attributes for implementing highly efficient queries has its benefits, it is not a true data container and has limitations, such as only allowing strings as keys. Now that LabVIEW has introduced the Map data type, there's no longer a necessity to use Variants for storing query data.
Using Polymorphic VIs
LabVIEW is not only equipped with functions that can accept a wide variety of data types, but it also includes built-in subVIs capable of handling multiple data types. For instance, the VIs for reading and writing configuration files can process not just numerical data but also strings, booleans, and more. This functionality extends to VIs for audio output, data acquisition, and beyond.
A popular method for creating VIs that can deal with different data types is known as a "Polymorphic VI". It's important to note that this type of polymorphism differs from what’s typically discussed in object-oriented programming, although they share the commonality of being able to execute different code based on the input data type.
A polymorphic VI itself doesn’t directly implement any specific program functionality. Instead, its role is to select and invoke one of several specific VIs—known as "instance VIs"—each of which is tailored to perform the desired functionality for a particular data type based on the data type of the input parameter.
Employing a polymorphic VI addresses the issue of data type safety mentioned earlier. A polymorphic VI’s parameters are restricted to those data types for which its instance VIs have an implemented method; it cannot accept other data types. For example, if we provide a polymorphic VI with two instance VIs designed to process either two numerical values or two strings, then this polymorphic VI can only accept either two numbers or two strings as inputs; any mismatch or incorrect data type would trigger an error, preventing the VI from running.
When you need to offer a user-friendly subVI that implements a certain algorithm capable of handling several different data types, it’s preferable to provide a single subVI with a unified interface rather than a collection of distinct VIs. This approach allows users to interact with a single interface that automatically adapts to different input data types, selecting the appropriate algorithm for each. This is the essence of what polymorphic VIs achieve, offering streamlined functionality that adapts to the user's data types without necessitating prior selection of the correct VI for each specific data type.
Creating Polymorphic VIs
Let's follow the steps to implement a subVI capable of performing addition on inputs of two different data types: numeric and string. This will be exemplified with a polymorphic VI named "add polymorphic.vi", designed to support numeric and string data types. Depending on the input data type, "add polymorphic.vi" will delegate the operation to one of two instance VIs - "add numeric.vi" or "add string.vi", as illustrated below:
Before you can create a polymorphic VI, you must first develop its instance VIs. These are the specific VIs that execute the functionality for each data type, in this case, add numeric.vi and add string.vi. Instance VIs are essentially standard subVIs. For example, here's the block diagram for add string.vi:
Once the instance VIs are ready, you can start creating the polymorphic VI. To do this in LabVIEW, open the New dialog box (File -> New) and select "VI -> Polymorphic VI" to initiate a new polymorphic VI.
Polymorphic VIs look quite different from standard VIs as they lack a front panel and block diagram, offering only a configuration interface. Since the functionality of a polymorphic VI is fulfilled within its instance VIs, you simply need to assign these instance VIs to it.
The essence of a polymorphic VI is a list that specifies which instance VIs the polymorphic VI can invoke. Use the "Add" button to include instance VIs in this list. The list in the above illustration contains an extra entry for demonstration purposes related to polymorphic VI menus, discussed subsequently. For the intended functionality of our program, only the first two instance VIs are necessary.
The VI's icon, displayed in the top right of the polymorphic VI interface, can be customized by clicking the "Edit Icon" button.
Choosing "Draw polymorphic VI icon" on the bottom left ensures the polymorphic VI's icon is consistently shown on the block diagram. Opting for "Draw instance VI icon" changes the displayed icon according to the data type of the input, thus clarifying the polymorphic VI's current function.
The two checkboxes at the bottom right offer further configuration options. When "Allow polymorphic VI to automatically match data types" is checked, the polymorphic VI will automatically select the suitable instance VI based on the input data types. If left unchecked, programmers are required to manually select the necessary instance VI for each use. Checking "Default to showing selector" will display a purple data type selector box beneath the icon on the block diagram whenever the polymorphic VI is added, enabling users to select their desired data type.
Regardless of the status of "Allow polymorphic VI to automatically match data types", the specific instance VI that the polymorphic VI calls can be altered through its context menu.
The "Edit Names" button in the instance VI configuration dialogue allows for editing the "Menu Name" and "Selector Name" entries, which appear in the polymorphic VI's right-click menu and selector, respectively.
Considerations When Designing Polymorphic VIs
There are several important considerations to keep in mind when designing polymorphic VIs.
Firstly, polymorphic VIs are limited to handling only a specific set of data types - those for which instance VIs have been created. Some algorithms might theoretically apply to an unlimited variety of data types. For instance, an addition algorithm could apply to various types of clusters, each considered a distinct data type based on its composition. Polymorphic VIs, however, cannot cater to every possible data type. While LabVIEW’s built-in functions like addition can handle an unlimited array of data types, achieving this with polymorphic VIs is not possible.
Each instance VI within a polymorphic VI can be entirely unique, with distinct front panels, block diagrams, and utilized subVIs. Nevertheless, for ease of use and comprehension, a polymorphic VI should typically represent a specific algorithm with each instance VI tailored to a different data type. Moreover, to simplify the transition between data types for users, each instance VI should maintain consistent connector pane patterns and wiring positions, as illustrated below:
It's also worth noting that polymorphic VIs cannot be nested; a polymorphic VI cannot act as an instance VI for another polymorphic VI.
Tips for Designing Menus
You can create multi-level "Select Type" right-click menus for a polymorphic VI by specifying the menu hierarchy in the "Menu Name" section of the VI’s settings dialog. Colons ":" are used to denote different levels of the hierarchy. For instance, for a first level of "Numeric" and a second level of "Float", you would write "Numeric:Float" in the "Menu Name".
This approach is equally applicable to polymorphic VI selectors and other areas where menu customization is necessary.
Drawbacks of Polymorphic VIs
Although polymorphic VIs offer convenience, creating them can be particularly challenging. A distinct VI must be developed for each potential data type. This becomes even more complex when considering algorithms that could theoretically apply to an infinite array of data types, as it's impossible for developers to cover all possibilities with polymorphic VIs. Solutions for such broad applicability, which cannot be achieved through polymorphic VIs, require an alternative approach, which leads us to the next topic: the "Adaptive VI".
Malleable VI
LabVIEW comes equipped with a host of built-in Malleable VIs, recognizable in the function palette by their distinct orange color.
When you drag one of these VIs onto the block diagram of a new VI, you'll find that it can accept a variety of different data types at its inputs:
How Malleable VIs Work
Malleable VIs are a unique kind of VI that, on the surface, look almost identical to regular VIs. The most noticeable difference is their file extension: *.vim. Simply changing the extension of a regular VI to .vim turns it into an Malleable VI. It's essential for Malleable VIs to be reentrant and inlined.
Much like inlined subVIs, when a Malleable VI is placed onto a block diagram, it integrates its code directly into the diagram of the calling VI. However, while inlined subVIs have fixed parameter types based on their front panel controls, Malleable VI controls act more like placeholders, dynamically adapting to the data types passed by the calling VI. Any data type the Malleable VI can handle is deemed valid, thanks to this flexible mechanism, somewhat similar to C++'s templates used in generic programming.
When contrasting Malleable VIs with polymorphic VIs, key distinctions emerge: polymorphic VIs are a collection of pre-made VIs, each potentially completely different from the others, even in terms of the number of parameters; an Malleable VI, however, is a single entity. Crafting polymorphic VIs demands more effort but yields stronger capabilities, making them ideal for complex, customer-facing toolkits. Conversely, Malleable VIs are less labor-intensive to produce and can easily switch between being a standard VI and an adaptive one, making them particularly well-suited for algorithms or functionalities that are applicable across a range of data types.
Writing a Malleable VI
This example demonstrates how to write and use a Malleable VI using a VI included with LabVIEW. Let's say we need to create a shuffling sub VI that accepts a deck of cards, randomly rearranges all cards, and outputs the new order. The shuffling algorithm is as follows:
- Sequentially select a card from front to back.
- Randomly select another card from the remaining ones behind the chosen card.
- Swap the positions of the two cards.
- End the program if the last card has been processed. Otherwise, return to the first step for the next card.
This algorithm guarantees that each card has an equal probability of being placed in any position. Interested readers are encouraged to verify this.
Not limited to cards, this algorithm can randomly reorder any set of data, whether integers, strings, or cards, using the same shuffling approach. It is independent of the specific data type. It would be too limiting if our sub VI worked only with cards. By making this VI malleable, it can be applied to any data type.
First, we create an empty Malleable VI:
(To transform an existing VI into a Malleable VI, simply change the VI's file extension.)
Next, we implement the logic for the shuffling algorithm. A Malleable VI also needs input and output controls, which can be of any legal data type: any one-dimensional array data type. This data type will be replaced when the Malleable VI is inserted into the caller VI. For our example, we used the most common numeric array:
The completed Malleable VI can now randomly sort arrays of various types:
Working with Type Specialization Structures
The previously developed shuffle program, shuffle_array.vim, is limited to processing one-dimensional arrays. Attempting to use it with other data types, such as two-dimensional arrays or strings, will cause errors, preventing the program from executing.
Here, if using a certain type of control in a Malleable VI leads to a syntax error, then passing that type of data to the Malleable VI in the calling program will result in an error as well. For instance, changing the input/output controls in the Malleable VI example to string controls would display a syntax error:
If we want this Malleable VI to support string data, we must recognize that the current code in shuffle_array.vim is unsuitable for string data. We need to design a new algorithm specifically for strings. Suppose our requirement is as follows: if a string is input, the Malleable VI should randomly shuffle each character within the string. If the input data is a one-dimensional array, the behavior remains as previously described. Having defined this behavior, we can implement a new Malleable VI: shuffle_string_and_array.vim. In this new Malleable VI, we'll use a "Type Specialization Structure" to select code that corresponds to different input data types.
A Type Specialization Structure is specific to Malleable VIs and can include multiple branches. LabVIEW will try each branch in the structure during code compilation, ignoring branches with syntax errors until it finds the first error-free branch, which it then adopts for compilation. This way, we can prepare multiple sets of code for one VI, letting LabVIEW automatically select the appropriate set for the current input type.
Below is the block diagram for shuffle_string_and_array.vim:
The core of the program is a Type Specialization Structure with two branches: Branch 0 directly calls shuffle_array.vim; Branch 1 converts the string to a byte array, shuffles it, and then converts it back to a string.
When using shuffle_string_and_array.vim Malleable VI:
- For a one-dimensional array input, Branch 0 is used since shuffle_array.vim accepts one-dimensional arrays without syntax errors.
- For string input, Branch 0 will have syntax errors because shuffle_array.vim doesn't accept string data. LabVIEW then tries Branch 1, which successfully processes the string data, making it the adopted branch.
- For any other data type, both branches will show syntax errors, leading to syntax errors in the calling VI as well.
Although it's possible to implement many sets of code within a Malleable VI for various data types and algorithms, Malleable VIs are most effectively used for applying a single algorithm across different data types. Adding multiple code sets can reduce code readability. In scenarios requiring multiple sets of code, using polymorphic VIs or writing a class may be more appropriate.
Type Checking
In some scenarios, you might require a Malleable VI that accepts only a specific type of data, or perhaps you need a Malleable VI that explicitly rejects a particular data type. This can be achieved by utilizing various type comparison functions and VIs available under “Programming -> Comparison -> Assert Type” on the function palette. Among these, the most frequently used functions are “Assert Structural Type Match” and “Assert Structural Type Mismatch”. Each of these functions has two input parameters. If the data types of the two inputs match, the Assert Structural Type Match function will not trigger a syntax error; if the data types differ, it will trigger a syntax error. The Assert Structural Type Mismatch operates inversely, triggering a syntax error when the two input data types match.
For instance, suppose we're tasked with creating a Malleable VI that bundles two pieces of input data into a cluster for output. However, there's a stipulation that the types of the two inputs must match, such as both being integers or both being strings. Inputs like one Boolean and one string should trigger a syntax error. LabVIEW’s native cluster bundling function doesn’t require each input element to be of the same data type. Therefore, to enforce type checking in this scenario, we need to incorporate an Assert Structural Type Match in the Malleable VI to verify whether the two inputs share the same data type:
Upon testing, it's observable that if the two data types fed into the Malleable VI are identical, the wires connect correctly. If they differ, the connection breaks:
The use of Assert Structural Type Mismatch is strikingly similar. For example, if we need a Malleable VI that accepts all data types except for strings, this function can be utilized effectively:
Limitations of Malleable VIs
Malleable VIs can handle a vast majority of generic programming requirements. However, there are a few complex scenarios where Malleable VIs fall short.
Firstly, Malleable VIs are a kind of reentrant VI, and certain LabVIEW functionalities cannot be utilized within reentrant VIs, such as specific VI properties and methods. Additionally, unbundling private data of a class is not permissible within a reentrant VI, even if the Malleable VI is located in the same class.
Moreover, Malleable VIs are not suited for some more complex, type-related operations. For instance, if we need to write a sub VI that accepts any kind of cluster data, reverses the order of elements within the cluster, and outputs the new cluster data. Or, if we aim to develop a sub VI with a variable number of input parameters, akin to the cluster bundle function that allows adding more input parameters by extending the function. These requirements cannot currently be achieved using Malleable VIs. LabVIEW does offer alternative methods for these more intricate needs, which will be discussed in the next section on Xnodes.
Application Example - Generic Doubly Linked List Container
In our discussion on object-oriented application examples, we finalized a doubly linked list container which had a significant limitation: it could only accommodate a fixed type of data. For that demonstration, it was restricted to managing real number data. Suppose there's a new requirement for a doubly linked list container to store strings. In that case, we would need to redevelop a similar suite of classes and VIs for string data, despite the doubly linked list's algorithms being data type-agnostic.
This section aims to refine it into a generic container capable of supporting multiple data types, allowing users to employ the same suite of VIs to create and manage doubly linked lists storing real numbers, strings, or other data types.
So far, our examples have showcased individual generic VIs: a single VI capable of supporting multiple data types. However, for data containers, what's needed is a set of multiple related VIs. They comprise a generic class (or library), where the acceptable data types for different VIs are interrelated to the data types already accepted by other VIs. For example, although this generic doubly linked list container can support various data types, once instantiated, a specific list will only be able to store one type of data. Every method VI within the container needs to verify if the input data matches the data type selected upon creation. For instance, if a user creates a string list, then every insert method within the container must ensure that the inserted data is string-typed.
Achieving this requires a mechanism for passing data type information between VIs. LabVIEW's data wires possess this capability; they transmit not only data but also information about the data type. Unfortunately, we cannot utilize LvClass data wires to transmit potentially variable data types within a class, as LabVIEW classes are static and do not support generic classes. The data types within a class are fixed before runtime. However, we can still utilize other types of data wires for this purpose, such as cluster data types, which can be created at runtime.
Designing an entirely new generic doubly linked list from scratch could offer better runtime efficiency. However, since we've already developed a doubly linked list container class, to simplify the task, we will not redesign it but instead will make minor adjustments and wrap the existing class. The overarching strategy involves changing the data type in the existing doubly linked list container class from real numbers to variant types. This allows us to insert any data type into the container, achieving runtime support for any data type. Then, we'll wrap the methods in the original doubly linked list container class with Malleable VIs and add compile-time data type checking. This method of implementing generic programming shares similarities with how Java supports generic programming.
Utilizing Variants for Data Storage
The process of creating the doubly linked list class has been extensively detailed in the Object-Oriented Application Examples section, so it won't be elaborated upon here again. The modification that needs to be made involves changing the control type used for storing data in the list node class to a variant type:
Correspondingly, in all VIs related to reading and writing data, the parameters must be changed to variant types. For example, here is the data reading VI within the Iterator class:
This adjustment allows the nodes of the doubly linked list to store any type of data during runtime. Implementing support for any data type at runtime is relatively straightforward. Next, we will explore how to conduct compile-time data type checks to ensure the data types stored within a container are consistent.
Wrapping Methods for the Generic List
A library (LvLibrary) is used to encapsulate all the generic methods. The completed library structure is shown below:
All methods within this generic linked list container are Malleable VIs, not conventional VIs.
Method for Creating a New List
When using a linked list container with a fixed data type, a special initialization method isn't necessary, as there are no parameters that need to be set at the time of container creation. However, a generic linked list container must initialize one crucial parameter: the type of data the container will store. If the container is set to store integers, then it can only accept integers in subsequent uses; similarly, if set to store strings, then it can only accept strings thereafter.
We've developed a Malleable VI named new_list.vim for creating a new list container. It requires just one input parameter, “element data type,” to indicate the data type of elements in the new list. It's important to note that the data in this control is essentially a placeholder; we're only interested in the data type it represents. The actual data within the list will still be stored in the DoublyLinkedList object, as previously modified. Therefore, in this list creation method, we also need to generate a DoublyLinkedList object. The DoublyLinkedList object, along with the data type, is bundled into a cluster. This cluster is required by other methods of the generic linked list container, allowing those method VIs to identify the container where the data is stored and the type of data it contains.
Writing Data Method
Each method for writing data into the container is structured similarly, involving two main steps: 1. Verifying the data type of the input matches the expected type; 2. Utilizing the corresponding method from the DoublyLinkedList class to store the data. Let's examine one such method, illustrated by the program diagram for insert_before.vim:
The 'list in' input parameter represents the generic linked list cluster generated in new_list.vim. The program initially unbundles 'list in' to extract the DoublyLinkedList object and the data type specified for the container. It then uses the Assert Structural Type Match function to ensure the data type being inserted matches the container's designated type. A syntax error is triggered if the types do not align. If they do match, the program proceeds to invoke the insert_before method within the DoublyLinkedList class to insert the new data.
Reading Data Method
Since the DoublyLinkedList class stores data as variants, the generic container's methods must convert the data from the DoublyLinkedList object back into a specific type before output. The to_array method, shown below, retrieves all data from the list and returns it as an array:
Utilizing the Generic Linked List
Here is an example demonstrating how to use this generic linked list:
This same set of Malleable VIs enables the creation of linked lists for storing real numbers, strings, or any other type of data, showcasing the versatility of the generic doubly linked list.