hey Marco!
it’s really, really tough to get this one right and to explain properly what’s going on.
So first of all: VL is a .Net language in a sense that it inherits the characteristics of the .Net type system. It adds some ideas to that, but most importantly here: it inherits the pros and cons of the underlying system.
When .Net generics got introduced back in the days with .Net 2.0, they added a feature that let you reason a bit more about your types at compile time. So by using generics, a library developer could now trade runtime errors for compile-time errors, which basically means that the end user would stumble upon incorrect usage rather earlier. But it is worth noting that they didn’t add a feature that suddenly would enable more programs than by using .Net 1.0, it would allow less.
They did some little thing different than the to-be-copied Java and it’s deep in there in the .Net way of thinking generics: it is the way how they treat value types, like Float32, Integer64… Java just treated types like Foo<T>
in a way that all occurrences of T would reserve some memory for a proper whole object (which internally comes with some pointer to the runtime type in order to look up virtual methods or for support some runtime type checks on that object). If that Foo<T>
just comes with a field T MyField, well .Net just behaves exactly the same. For the runtime behavior, it is just like using an object MyField. (again: Using generics only additionally allows to reason a bit more at compile time (for all possible runs to come))
Soo, where again is the difference between .Net and Java in regards to generics?
Here it comes: Java really always made sure that any occurrence of T would behave like a proper object, even if you have an array of T inside your Foo, in which case it is like having an array of objects inside (with added compile time info that there are only Ts inside), which is pretty good from a theoretical position: Barbara Liskov - Wikipedia formalized these intuitions in her Liskov substitution principles (back then girls were the dominant gender in computer science), but it is bad if you want to care a lot about performance and memory. So in Java you basically get an object array internally with each small Float32 being boxed into an object (somewhere else in memory, not directly in the array memory itself that is), each of which comes with RuntimeType info…
.Net on the other hand treats a Foo<Float32>
special in a sense that it makes sure that any T array within Foo<T>
actually turns out to be a Float32 array, where this Float32 info is stored once for the whole array, and each float sits directly in the array memory. This might be genius from a performance and memory point of view, but it breaks some intuitions concerning subtyping:
A Float32 or Integer64 are told us to be objects. But actually they only behave as objects when you can get hold onto the instances themselves: You can run Object.ToString() on them or ask them for their type.
But as soon as you use them in generics, the .Net type system treats them as not being objects.
An example: Sequence<T>
is covariant, which is like saying: if Banana is a Fruit than a Sequence of Bananas is a Sequence of Fruits. Works.
However Float32 is claming to be an object. But a Sequence of Float32 is not a Sequence of Objects:
Actually, we probably could do this differently. Now that i explain this to me another time: Yeah, gosh, we probably could have our own notion of value tpyes, that are just boxed versions of the .Net value types. So we would have the same .Net typing system, but a VL user would never come in touch with the original .Net value types (like Float32). This however comes with the exact small issue that at some point we’d need to do the boxing and unboxing, when talking to SharpDX or whatever library to get the boxed value into a value type and back again.
I attach a small patch that shows what i mean with boxing.
boxingTests.vl (26.4 KB)
Note, that if you want to use a composed type like OuterType<InnerType>
and you want to use inheritance intuitions to work for the inner type, your outer type needs to be covariant, which the .Net type system only allows to be specified for interfaces (and delegates). See IEnumerable<T> Schnittstelle (System.Collections.Generic) | Microsoft Learn for an example (the generic Sequence). The type parameter comes with the keywork out, which says you can only read items with that interface, in which case it is save to say that a OuterType<A>
is an OuterType<B>
, if A is a B. Just saying. A Command<Float32>
would only be a Command<Object>
, if Command is covariant and Float32 is an object, which probably both is not the case for now.
Sooo. What to learn, what to do?
I wonder if it would be an option to just introduce a ungeneric interface ICommand, which is implemented by your generic command class. This ungeneric version then is perfect for putting all kinds of differently typed commands into one big spread. As this spread now contains all sorts of commands you’d at some time test for the actual type which you could do with a foreach and a cast where you try the different commands.
Not sure if this answers your question. Hoping i could shed some light onto subtyping, generics, covariance and the problem of value types in relation to the former features…
When using nested generics in the Cons node instead, it falls back to something called UnTyped (instead of Object), but that can be fixed with a CastAs.
What do you mean with “in the Cons node”?
Note: You probably talk about type Sequence (Untyped) which is basically the ungeneric IEnumerable from .Net. So it is a sequence that just expresses that is about a sequence, but just doesn’t care about the element type. If think about it this is a very elegant shortcut. Instead of talking about a generic Sequence where you use object as element type (by that trying to make subtyping work for the inner type), just don’t use the feature of generics in that case (then you only need to reason about the outer type and yes any Sequence of T is also a Sequence). Basically this is exactly what i proposed for your Command. Make it implement an ungeneric variant of it for the cases where you just want to think about commands, but not so much about what exactly they store.
Note that the Cons example works as the cons can get a grip onto the single elements themselves, box them and put them into a Sequence of objects…