Alternate Interfaces for VTK-m Filters
One of the challenges of the filter design is building a mechanism that manages the dynamic objects that comprise the input and allowing the filter implementation to respond to the different types of data with regard to the policy. In particular, the following features must be maintained.
- The execution method must have at least two overloads: one that specifies a policy and one that uses a default policy.
- There must be at least one non-templated definition of the execution method using the default policy that completely compiles and can be used from a library.
- Different filters will need different parts of a data set.
- Filters will need to know the concrete types of some inputs (arrays or cell sets).
- All casts, whether for the execute method or done within a worklet invocation, must respect the policy.
Getting the structure of filters to satisfy all of these criteria is challenging. In particular, casting dynamic types to concrete types is awkward, as each one generally requires a functor to call back into the class. Here are some proposed structures to filters, all of which are unsatisfactory in some way.
Pre-Specified Input Sets
In the pre-specified inputs approach, which is what is currently in the development branch, there is some set of filter superclasses, each with a pre-specified calling specification. Currently there are two such classes. There is `DataSetFilter`, which simply takes a data set, and there is `DataSetWithFieldFilter`, which takes a data set and a field (or coordinate system).
For any of those pre-specified input parameters, they are cast to a concrete type (with a CastAndCall according to the policy) and passed to an internal `DoExecute` method.
The best feature of pre-specified input sets is its simplicity. The calling specification is easy to specify and determine by the superclass. All these inputs are cast without further coding.
The problem with this approach is that it is not very expressive. Already with the small number of filters implemented we see a dichotomy between those inputs specified in the execution arguments and the catchall of what is pulled out of the data set. For example, the marching cubes filter inherits the data set with field filter. It gets the field for the isovalue, but it still has to get the cell set and coordinate system from the data set. It is easy to, for example, forget the policy. It also adds some strangeness to the way inputs are specified because the cell set and coordinate system are set indirectly through object state rather than passed in the execute method. Furthermore, if the filter needs to know anything about the type of these other arguments, it is jumping through hoops to do so.
User-Created Interface Methods
Another approach is to avoid providing interface (now called `Execute`) methods altogether and leave it up to the user to provide such a method. There is no real way to enforce the creation of such a method or the type of arguments it has. Thus, it would be up to convention to maintain many of the required features listed at the top of this document.
Since it is up to the filter developer to create any `Execute` methods, this setup should minimize the number of such methods that need to be specified. At a minimum there must be two: a non-templated version and a version templated on the policy. (The non-template version will almost certainly call a second version with the default policy.) It should be easy to construct a non-templated `Execute` method. This will require the introduction of some special classes such as `FieldOrCoordinateSystem` that will automatically cast from those two class types. This would allow accepting either one without templates or overloading the method.
This method is very expressive from the terms of the filter developer. Any combination of input parameters is specified as arguments to the `Execute` method.
Furthermore, the code created is about as easy to understand as can be. There are no automatically generated methods or general methods in superclasses. The method to call is properly documented in Doxygen without any further work.
The biggest issue with this approach is that it requires all filter developers to closely follow conventions that are easy to break. The easiest mistake to make is to forget to apply the policy correctly. Making a non-templated version of the `Execute` method could trip up some developers as well.
Another issue is that as this is specified so far, there is no good method for casting the arguments. Requiring filter developers to use the `CastAndCall` method of dynamic objects is a bad idea. The interface is just too unwieldy. Here are some potential solutions (which may or may not be bad).
- By convention, have the `Execute` method cast everything. Have the filter superclass contain a method named something like `CallDoExecute` that has variable arguments. The `Execute` method calls `CallDoExecute` method with all its arguments, which in turn calls `DoExecute` with arguments cast to static types. There are numerous problems though. First, it is unclear how we capture metadata with this approach. Second, it requires the code to jump into complicated template code that generates a deep call stack. Third, you cannot really express constructs like "these two fields have the same base type," and you have to deal with type combinations that you do not really support.
- Support the ability to "half-cast" cell sets and fields. Most often we probably do not care about the full static type but only parts of the type. For example, we might be concerned whether an array has Float32 or Float64 in it, but probably don't care if it is in a basic storage or an implicit uniform point coordinate storage. Likewise, we might care if a cell set is structured or explicit, but probably don't care what the underlying array types are if it is explicit. It would be good to be able to check this part of the type without specifying the full type. It would also be good to get a half-cast type that has part of the type statically known and the other still dynamic. This would probably translate to code developers are familiar with. Of course, we do not have such a representation yet, and it might result in a lot of if-then-else code in filters.
Meta-Template Programming with Signatures
A third approach would be to implement something similar to what we already have for worklets. The filter could have some type of `Signature` typedef that defines the arguments valid for a call to `Execute`.
This is a very expressive way to specify the interface for just about any filter. Plus, we already know how to implement such an interface. Also, since the `Execute` method is built with meta template programming, the generated code can ensure correctness; we do not have to rely on users following conventions.
I get the impression that VTK-m users are not enamored with our signature conventions for worklets. The interface is very contrary to the typical C++ programming experience, and it is very non-intuitive, at least at first, for new users. Thus, this might not be what we want to expose at the very highest level of the VTK-m interface.
There are other problems to work around. We continue to have problems expressing relationships between the arguments (such as, both arguments must have the same base type). Also, it is difficult to capture the metadata as types like fields are converted to static arrays.