If these are the important use cases, how are you planning to cover the second use case (native machine-size vectors)? I don’t see anything in your design that exposes this concept.
I think the argument that Vector3<Float> is the ideal spelling is misguided. These aren't generalized vector/matrix types that can be parametrized with any numeric type – they're a limited set of built-in vector/matrix types with specific layouts and instructions. The accepted spelling for that in the standard library is to enumerate all the combinations: Int8, Int16, UInt8, UInt16 etc. If UInt8 was spelled Unsigned<Int8> the argument would hold water. Alas, that's not the case.
Also, from an ergonomics standpoint, it sounds like Vector3<Float> would have pretty bad discoverability. How would you know what's allowed between the brackets? Autocomplete generally doesn't suggest a list of allowed types for a type parameter. The spelling Vector3.Float on the other hand sounds like it would have great discoverability. Type Vector3. and you'll get a list of all Vector3 types. Simples!
But I don't think anyone would consider "vector" to be an adjective like "unsigned". UInt8 is a number that happens to have the trait of being unsigned, whereas Vector3<Float> actually is a vector containing Floats, and I think that is to different to support a similar spelling.
I think this would be a nice addition to the editor... but besides that, relying on autocompletion has also downsides:
You don't get any explanation for missing possibilities, whereas the compiler can generate a meaningful error if someone tries to use something like Vector3<NSNumber>.
I encountered similar situations with conditional conformances, and it can be quite confusing when you are sure that Array has a contains method that is simply missing in autocompletion when you want to use it...
I don't think this is misguided if we have the language support necessary. All we need to properly restrict the types is a closed SIMDScalar protocol (this name is placeholder for discussion, not an actual proposed name).
As an aside, the argument against closed protocols has always been "that's what enums are for". Whether we choose the generic spelling or not, I think this is a good example of how closed protocols are able to do things that enums are not.
Can you name another type where this is true? (Including the dot).
Seeing as Vec3 is a term of art, people may just type/autocomplete Vector3 and stop there, and wonder why they can't instantiate it. This idea of 'container/class of types (dot) contained type' is completely new to Swift and shouldn't be added lightly.
As far as I can tell, the main objection to the Vector3<Float> spelling is that the implementation is a bit inelegant... but the implementation can be swapped out behind the scenes as new features come online (without needing to change the spelling).
The idea that <> means you can put anything in them is a red herring, since we already have lots of cases where the generic parameter is constrained by protocols. That said, I would hope that fairly quickly, we would be able to have vectors containing pretty much anything numeric... with the special ones being accelerated under the hood. I think we will want this anyway, at least for numerics, since there will come a time in the future where one chip supports certain vectors and another doesn't. If we have a non-accelerated option, then it can fall back gracefully.
Formal proposal in development here: simd types for the standard library by stephentyrone · Pull Request #911 · apple/swift-evolution · GitHub
here’s the problem, you need to have a different protocol for each N in VectorN and explicitly conform the particular subset of relevant integer types to each one, since there are integer types that are okay for some Ns and not for others. Now, this would probably involve the same amount of standard library boilerplate as VectorN.T, except with the addition of 6 extra top level symbols for the protocols (so, a total of 12 new top level symbols instead of 6), but that also tells us the protocols would be no more expressive than just defining them as simple types namespaced with the .. This just isn’t the kind of thing protocols were designed for. I also think this would confuse a lot of beginners as we are blatantly abusing a language feature that is already notoriously hard to teach. If I saw VectorN<T> and didn’t know any better i would assume protocols and generics work differently than what I thought, and that there’s something in the generics system that lets us conditionally constrain by bitwidth or by static var or something when in reality it was just a hardcoded standard library hack.
can i also say I don’t think protocols would help us write better generic code using vectors either,, they just don’t work with BinaryInteger, FixedWidthInteger, etc. You would have to look at all the vectors you’re using in your function and then take the lowest common denominator among them, which, with the more different-length vectors you use, the fewer integer types it can be generic over. where constraints would end up looking like
where T:FixedWidthInteger & Vector4Compatible & Vector8Compatible & Vector16Compatible & ...
Now you could probably mitigate this by making some of the vector protocols inherit from one another since a lot of them overlap others completely but you would still have the problem that, all of a sudden, we can’t just colon FixedWidthInteger and call it a day; as soon as you decide you need a Vector16 and not a Vector8 in a leaf function you have to go all the way back up its caller chain and narrow all the generic constraints from Vector8Compatible to Vector16Compatible. this would happen a lot if you’re trying to write generic vector code, whereas this kind of thing just doesn’t happen that often in scalar generic integer code.
It's worth keeping in mind here that generic code is the exception, not the rule. Most people will be writing concrete code. The protocols and generics are mainly useful for implementing libraries like this one--the protocols I defined are precisely the ones that I needed to minimize my own boilerplate both for the current draft implementation and for the features that I plan to add in the future. So they at least work well for those purposes.
okay forget about Vector8.UInt16 if i had to choose a hill to die on this would be it
(swift) let x = Int8.Vector4(-1, 0, 3, 0)
// x : Int8.Vector4 = Vector4(-1, 0, 3, 0)
(swift) let y = x.replacing(with: 1, where: x == 0)
// y : Int8.Vector4 = Vector4(-1, 1, 3, 1)
i’m sorry but that where: x == 0 looks just like an autoclosure and if i hadn’t been following this proposal and saw this the first time i would be so sure it’s an autoclosure i wouldn’t even bother looking it up until bugs started appearing. okay i'm being dramatic but you get the idea maybe make it a named method instead of == ?
Right. My post wasn't intended to take a position on what syntax we should use right now (I think your rationale is sound). If we get closed protocols, the ability to take arbitrary constant values as generic arguments, and the ability to specialize layout on generic arguments then Vector<Float, 8> might becomes a very appealing design.
This goes back to a point I made earlier: masks:simd vectors::predicate functions:scalars. It's a different low-level computational model binding exactly the same conceptual idiom. We could make a source-language distinction between these, but I don't know that it's a useful distinction.
It is not an accident that they look similar, and it leads to very natural use:
func minimum<T: SIMDIntegerVector>(_ x: T, _ y: T) -> T {
return x.replacing(with: y, where: x > y)
}
idk what to say other than that’s not my first thought. like, by now the standard library has basically taught me that “where:” means function so i would look for curly braces and since there aren’t any, i’d look at the double equals sign, and be like you know where else I’ve seen that before? assert. maybe this thing is like assert.
Read SIMD masks as a "function" from vector index to Bool. It happens to be spelled subscript instead of func, because you can also view these things as vectors-of-Bool. But it doesn't matter, because you rarely call it explicitly, you just pass it as an argument like this.
But it's not like assert. It it was a function it would need to be a predicate which would need to take an argument. If it does take an argument it can't be an @autoclosure. An API that accepted a function labeled where that did not take an argument (and could therefore be @autoclosure) would be very strange.
i don’t think that makes it any better now you’re making it so that <, ==, and >, etc return a “function” instead of a Bool. it makes as much sense as
func excludeBelowThresholds(_ a:inout [Int], _ b:[Int])
{
a.removeAll(where: a < b)
}
hence, confusion. of course if you think about it you’ll realize something doesn’t add up here and it cannot be an autoclosure but if you had to stop and think about it like that then that means the code isn’t very readable
I had a thought on implementation, which has an extra hop from Brent's version (which will hopefully still collapse down), but in return it would allow us to have a simpler model overall (including non-accelerated vectors).
The unfortunate part is I still haven't figured out a way to get rid of the need for having protocols of each dimension. I really wish Swift had a built-in notion of dimension...
The idea is that @scanon's base types would be named with an underscore and conform to a VectorXStorage protocol, where X is the dimension. Then the VecorizableX protocols (again with X being the dimension) would be much simpler, basically just pointing to the storage using an associated type (and possibly a trampoline for special abilities of that type). Finally, we have the public VectorX<VecotrizableX> type which just calls the storage.
That may seem like more complication, but the end result is that we can add a base conformance of VectorizableX to FixedWidthInteger and BinaryFloatingPoint (or perhaps even Numeric) that calls off to generic storage. For example:
struct _GenericVector4Storage<Element: Numeric>:Vector4Storage {
var a:Element
var b:Element
var c:Element
var d:Element
//Implement Vector4Storage requirements here
}
Thus, when built-in types are available, we are just using those, but we are also able to add non-accelerated vectors when desired. Vectors work for anything that is Numeric.
We have a real/full Vector type, which gets hardware acceleration for free where available (and if new vectors become available on future chips, we just upgrade the implementation behind the scenes to take advantage of it).
Am I right in understanding that the equality operator is overloaded such that ‘x == 0’ doesn’t return a Bool, but rather a function that will wrap an intrinsic (or something similar) that will compare each element of the SIMD to zero?
There are two == operators. One returns a vector of Bools, where each element is true if and only if the corresponding elements of the left and right operators compare equal. The other, which is defined in terms of the first one, returns a Bool scalar, which is true if and if every lane of the first result is true (i.e. if every lane of the two vectors compares equal).
Generally speaking, we avoid return-type overloading in the stdlib, because it can be confusing, and this definitely sounds confusing when you write it out like this, but in practice, when you use it, everything works out very nicely and you rarely need to think about it at all. In the examples that I've been working on and in the implementation itself, I haven't yet needed to explicitly disambiguate an expression.
init(fromArray array: [Element])
Naming bikeshedding:
- The argument label
fromArray:is merely repeating the type information, so the "Array" suffix should be omitted IMO. - Initializer argument labels usually do not start with prepositions, so we don't need "from".
- This is a value-preserving initialization when the precondition (
array.countequals the vector size) is satisfied, so the label can be omitted.