Generic interfaces

Hi,

I got a generic interface with 2 inputs and 1 output

and a few classes implementing the interface, eg Add, Multiply, etc.

For specific combinations of Inputs, which might come up during runtime, I got non-generic implementations

As expected, this works fine as long as I connect a specific type (of the types that are supported by my classes, eg Float32, Vector3, etc…)

However, while I do know all types which will get connected will be supported by my classes, I don’t know them upfront

What to do? Am I setting this up right?

(For completeness: this is a simplified version of my problem - what I’m trying to do is, during runtime, build a spread of instances of some class, where each instance of this class holds some modifiers, which operate on different types per instance. So one slice of the spread/instance of the class might operate on Float32, another on Vector2, another on a combination of them, etc…I know all possible type combinations and have implementations for all of them, I just don’t know which ones are going to come up…)

thank you!
GenericInterface.vl (24.0 KB)

a couple of things are happening here:

switch

did you check the output type of switch? my guess is, you assumed it would output different types depending on which input is selected (0: float, 1: vector3). In the demo case the resulting type is IFormattable, therefore Create and Multiply err on not finding an adaptive implementation for the given type (IFormattable) therefore the spread is filled with null, therefore the error on foreach and imodify

adaptive +/* and class<T>

the implemented classes Add and Multiply both constrain both inputs and the output to the same type. You can either see this by directly connecting Create to IModify and then connecting a Vector3 which will immediately result in the second input of IModify also being Vector3 and the instance being Add<Vector3>. or just check the resulting code, which shows the class as Add<T> instead of Add<T,T1,T2>.
the type narrowing from 2 inputs and one output to just one possible type happens due to the adaptive + and * nodes. They define the constraint. Thats why there is also * (Scale) which works with T on input1 and Float32 on input2. you therefore wont be able to provide the + (Adaptive) with an implementation for +<Vector3,Float32,Vector3> which i think was your intention with the AddVec3Float class.

adaptive: generic + non-generic implementation

guessing that you intention was to declare generic add class and having the AddVec3Float non generic class was to provide a concrete implementation as with + (adaptive). I’m not sure if this even works for classes. i only used it with operations. adaptiveness is actually a VL language feature and the same as generics in c#.

IModifier<U,V,W>

In order to make use of generics without adaptiveness you would actually have to provide all permutations of float,vector3, as class implementations, instantiate those and have a function choose the correct instance depending on the inputs. And even thinking textual c# i think you can not achieve that fully typed but need to use dynamic for runtime type switching.

1 Like

Hi @woei + thanks for your time! A lot of no, can’t do, hehe

I knew it would change type to the ‘first’ interface both implement, I did however assume it would somehow gracefully cast to what it actually is in the operation. I do see now that that’s not gonna happen, I didn’t see this before I’ve patched it/you’ve explained it.

I understand I can not make an implementation of +(Adaptive), which would accept two different types - I thought however I could make two types of classes, Add(Adaptive), which handles all cases where the input types are the same (so kind of a wrapper around +(Adaptive)), and eg AddVec3Float, with respective input types; both classes implementing the same interface.
To be clear: that’s what I did, I’m just stuck here.

Came across dynamic when googling this, but there’s no dynamic in VL, is there?

I don’t think what I’m trying to do is not possible in C#, so I assume I’m just doing it the wrong way.

Since I’m currently prototyping a few things for a VL rewrite of our live performance tool made in beta (you know it well @woei ;)), and as there’s channels now, I thought I’ll see if I could make a synthesizer-like UI, where I could hook up different ImGui widgets to each other (ie merge channels).

So I’m drawing these links using the middle mouse button. When linked, there’s a small popup menu, which should let you swizzle, map, invert, etc. Got all the adaptive implementations of this as well as the ones for combinations of Vector3 and Float; and well, silly me, I thought when I would now only create links between the same types or Float and Vector3, I should get this to work.

Now I feel my whole strategy of doing this is wrong…what’s my other options?

Kind of afraid of the answer, haha.

thank you

Does your modifier interface have to have three different type parameters? Or would two be enough, for example T1, T2 -> T1?

My actual case are two inputs, possibly of different type, so T1 and T2, and 2 outputs, of the same types, T1 and T2.

“Links” between ImGui widgets might run both ways. So if I want to merge 2 slider both ways, but want eg one of them to be always three times the other slider, I’m modifying both inputs separately (before I merge them back to their channels):

It sounds like you need runtime type checks (TypeSwitch), not compile time type checks (adaptive, generic).

The difference is that compile-time types are known when the program is compiled. But it seems like you will change types at runtime (dynamically link things while the app is running). For that, you need a common super type and then switch the code that you run at runtime with a TypeSwitch or some other kind of if/else logic depending on the input type of the object that is coming in.

1 Like

I sketched something up based on the assumption you have T1, T2 -> T1. It makes use of a newly introduced feature in 5.0 which allows to instantiate generic types (including their adaptive content) at runtime. Kairos is making heavy use of that feature btw. Let me try to explain what’s happening in that patch.

Our ingredients are the following:

  • We have a generic interface IModifier<T1, T2> with two implementations Add<T1, T2> and Multiply<T1, T2>
  • Both of these implementations make use of an adaptive node called Lift which essentially lifts from T1 to T2 allowing us to use the normal adaptive nodes like + or * on the result.
  • For Lift we provide implementations for the case TT, FloatVector3 and Vector3 -> Float - more could be added of course

So far we can now create different generic instances of Add or Multiply with different permutations of its type arguments (Float & Float, Vector3 & Float, Float & Vector3). By adding additional Lift operations this list would grow quickly.

In any case, like it was already pointed out in this thread, you will only know your type arguments at runtime. So basically we need to find a way to create those Add or Multiply modifiers at runtime. Here the before mentioned new feature comes to the rescue: it’s a node called MakeGenericVLTypeInfo. Combined with GetTypeByName and CreateInstance we can now already start to create modifiers at runtime based on runtime type information only. Nice, but we lack one central part still: we don’t know how to call Modify - remember it is part of a generic interface, so we’d need to know the exact type instantation at compile time, which we don’t.

The trick here is to introduce a non-generic interface which will act as our non-generic accessor.
Now I could’ve implemented that non-generic version on the original classes (Add, Multiply) but decided for a different pattern, where I implement the non-generic accessor on a separate class which I then make available trhough a node called DynamicModifier. That DynamicModifier also asks for a model which describes what modifier to apply. There are for sure multiple ways of doing this, but I took the path we also took in Kairos since it’s quite common that you might want to pass implementation specific arguments. You’ll again find usage of the adaptive feature here, this time it’s about creating the modifier based on the model and the type arguments. Like I said, we did it like that in Kairos and it served us well so far, so why not stick to that pattern.

GenericInterfaceWithDynamicAccess.vl (68.8 KB)

3 Likes

thanks @tonfilm + @Elias

Somehow I was under the impression that you could have different types as inputs when using typeswitches, but not different types as outputs. Since this is wrong it’s definitely an option.
I don’t understand the new technique yet, but it also looks very neat. I will give both options a try.

One thing I immediately ask myself though: in both options I end up with Object (or similar). In the next step I’m intending to merge this with Channel<T>, T being one of the 2 types I’m after, but similarly unknown at compile time. So how do I go from Object to the type(s) connected upstream?

thank you

You say only 2 types - is it only about floats and vec3s at the end?

Hm, I’m realizing now I’ve got another problem: the 2 eg sliders I’m trying to merge are fully typed in my model, but when I select their channel via their name, I don’t know their type (when I’m creating such a link, I atm only take note of their name/path within SomeModel)

I’m trying this for all types which have ImGui Widgets, so Integer32, Int2, Int3, Int4, Float32, Vector2, Vector3, Vector4, Boolean, String and RGBA. I have a maximum of 2 types out of these.
GenericInterfaceWithDynamicAccess (1).vl (85.2 KB)

Well yes you’ll need to store these types somewhere as string. Again GetTypeByName etc will be your friends here. I’d recommend you to clone the Kairos repo and have a look how things are done over there if you need inspiration. It needs to solve similar issues.

I thought this would be the case, so I got that, just didn’t figure out how to get to the needed type from the string…I’ll try GetTypeByName and have a look at Kairos if it doesn’t work! thank you

Ok. I had a look at Kairos, ie AlchemX and the Interpolator and created my own Modifier from scratch. I still don’t fully get it, but with your patch @Elias from above I was able to adapt it so it now has two inputs/outputs of different types, and these actually are the wanted types instead of Objects or some other interface…

Very cool.

I couldn’t find anything within Kairos which helps me with my additional problem though - the fact that I’m getting the values from Channel<MyModel>, but I don’t know their types:

In the first picture you can see the info seems to be there when I do GetType. So I do have the name of my type as string, but don’t know how to cast to it.
Stack overflow suggests ChangeType from System.Runtime, but I can’t get that to work…

edit: since this is a related but different question, I’ve opened another thread here

GenericModifier.vl (64.3 KB)

Channel has an non-generic accessor for the Value, named “Object”, “SetObject”.
Juggling with objects:
GenericInterfaceWithDynamicAccess_PlusChannels.vl (72.1 KB)

This patch is only working with objects. Everything type-related is postponed to runtime.

DynamicModifier also only takes objects and at runtime juggles with type parameters. But that’s details. To the outside it says: I could be capable of working with any object that may flow. Try me…

Regarding Channels:
Select (ByPath) gives us a Channel. But we use it in a way that tells the system: ok any object.
Every Channel is also inheriting from Channel (Ungeneric), which comes with the Object accessor.

1 Like

@digitalwannabe did that help? Happy?

@gregsn yes, thank you.
It took a while, I’m still very much processing all of this, that’s why I didn’t answer yet. This is related to my other question you already answered, and at the bottom of this is a misconception of T, Objects, Pads and compile/runtime. Or let’s say no proper concept at all. I simply thought I had to, at some point, get back to typed pads to be able to merge channels. But the patch you posted does the job perfectly, and I also get this part now, thanks a lot → happy!

I’m also still trying to understand the new technique @Elias explained here. On the one hand, I kind of get it, also had a look at Kairos. What I understand: we create an instance of the modifier class, which inherits from both the generic and the nongeneric interface. We use the nongeneric interface so we can pick a type at compile-time. At runtime, any time an input or the model type changes, we create a new modifier instance. Then somehow, at runtime, the nongeneric interface points to the generic interface/implementation. Which implementation is set via the model.
But then, trying to gain more insight into this, I tried to understand how this would look like if I don’t have different implementations, so not Add, Multiply, etc. but only a single MyFunc, which needs to work with types decided at runtime (no use case rn, just out of curiosity) - and I have no clue :D
So I guess my question for now on this is: is this design using two interfaces inherent to this technique, or is the technique just set up here (or in Kairos) like this, because we(I) need different modifiers (Add, Multiply, etc…). I assume this is obvious, but I can’t see through this…yet.

Thank you.

hey @digitalwannabe.

I tried to go through this step by step in this patch.

adaptive process node + dynamic process node.vl (54.5 KB)

(1) Adaptive Process Node:
There is this idea of having an adaptive process node, but there is no direct (easy) way of defining one in VL yet. We solve it by abstracting over the creation of the process node implementation with an adaptive Operation. We then just call into that implementation.

This one already helps us. It allows us to use this node with any type combination and the implementation will get picked at compile-time.

Again, it’s basically the adaptive system enhanced in a way that it feels like VL would have adaptive process nodes.

(2) Dynamic Process Node:
We add a non-generic way of accessing the process node.

The pipeline gets constructed at runtime, when and only when, the type of one of the inputs changes.
As soon as we have the modifier, it runs as fast as possible. There are no type checks at runtime.


not included in the patch:

(3) Abstract over the operation via TModel
This is what Elias showed you:
If you want to avoid building this kind of system for each operation over and over again, you could embrace one signature for two operands with one pin naming and abstract over the operation via a model. This one builds ontop of (1) and (2)

(4) Everything is an object
A completely dynamic system, where changing data types does not result in the instantiation of a new modifier. Such a system would not start by using the adaptive system, but would just look at the operands and just do whatever needs to be done at runtime. This is how dynamic languages do business. CastAs, GetType and TypeSwitch nodes help you with dealing with runtime types. You can do all of this without defining one process node or class. There is an operation “Multiply” that just checks the types just in time before calling the one or other primitive multiplication node.
Obviously, you pay for the type checks on each call.

3 Likes

@gregsn this is incredibly helpful and exactly what I was hoping to understand! Thank you very much!!

1 Like