SE-0483: `InlineArray` Literal Syntax

In the absence of this sugar, can the compiler still correctly type-check inline arrays initialized from array literals, including for their cardinality?

// The existence of this sugar indicates the compiler has special knowledge of InlineArray.
// So it will presumably refuse to compile this:
let a: [3 x Int] = [1, 2, 3, 4]

// But what about this?
let a2: InlineArray<3, Int> = [1, 2, 3, 4]

That is just as much an argument against using x, as an argument for using it. Other arguments against it:

  • Does not feel serious
  • Does not look like an operator at all
  • We are all used to seeing x as a variable name
  • Will not be understood as a keyword the first time you see it
  • Can potentially appear together with a variable x
3 Likes

The test/Sema/inlinearray.swift file contains examples of type-checking.

There was a recent discussion about non-literal integer generics.


LLVM arrays use a similar syntax.

6 Likes

But as you mentioned, those cases can be handled by further sugar, which I think is preferable anyway. So what was the issue with Int[6,3]?

Could you expand on that? Is that because [] associate differently in Swift compared to C?

Example:

int main(int argc, const char * argv[]) {
    char x[10][3] = {}; // InlineArray<10, InlineArray<3, Int>>, no??
    // i.e. it is "ten triples" not "3 10-ples".
    printf("%ld\n", sizeof(x[9])); // 3
    x[9][2]; // ok
    // let's set last element of the first triple:
    x[0][2] = 1;
    // this is how it looks in raw memory:
    char *y = x;
    for (int i = 0; i < 30; i++) {
        printf("%d ", y[i]);
    }
    // 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    printf("\n");
}

As one of the people who questioned the need for syntactic sugar for this type, at this time and whether it was needed, just wanted to summarise some of the counter arguments presented and comment a bit.

I think these are reasonable arguments for the desirability of syntactic sugar and would have been helpful to have in the Motivation section of the proposal (which right now was simply "arrays are simple, so therefore inline arrays should be").

Me too.

Agree the full code wouldn't make much sense, but it is generally helpful for readers (who also should be given the benefit of the doubt of good intent, we spend our time freely also trying to help make this better by giving feedback on the proposals to the best of our abilities) to show use sites that truly are problematic and how it helps.

There is among some of us a sentiment that there has been a lot of syntactic sugar added the last few years, which may be good in the smaller context, still heavily increases the cognitive load also especially for newer engineers, so unless the need is clear, it is probably healthy to have some pushback and at least question it? It doesn't mean we are against sugar in general, just that it's good to understand the need better.

It is also (not related to this pitch directly, but...) exacerbated by the fast that not all of the rationale or concrete things about the sugar that exists get into TSPL as far as I can tell, but sometimes lives on in pitches/evolution documents making it harder to navigate.

So (correct me if it's incorrect), the same comparison without sugar would be:

struct System {
#if INLINEARRAY_NO_SUGAR
    var mass: InlineArray<5, Precision>
    var position: InlineArray<5, SIMD4<Precision>>
    var velocity: InlineArray<5, SIMD4<Precision>>
    var force: InlineArray<10, SIMD4<Precision>> = .init(repeating: .zero)
#else
    var mass: [Precision]
    var position: [SIMD4<Precision>]
    var velocity: [SIMD4<Precision>]
    var force: [SIMD4<Precision>] = .init(repeating: .zero, count: 10)
#endif

I agree the sugar version looks nicer, but the question is if it is important enough -- the answer may well be yes, it's largely a more holistic question on overall complexity (by adding a 1000 small pieces of sugar) vs. the expected use cases - different people will clearly have different views there.

I think that perhaps a multi-dimensional example together with the argument above on parity with other languages could have preempted part of this discussion though.

That's also fair enough, I can't think of anything significantly better than "x" of the suggestions made, unless the alternative is "don't sugar".

4 Likes

Nice bullet list, thank you!

Probably this won't (significantly) matter in practice...

However it puts the number in brackets, which is a bit odd compared to normal array that puts type in brackets. (unless we also "back-port" and allow Int[] for normal arrays)

This might be factually wrong... please double check on that. cc @Ben_Cohen

Sigh, I was waiting to see your feedback on separator-less option like [4 Int] but it wasn't there. Is it dismissed on aesthetic grounds only? Looks totally alright to me, concise, follows / extends Swift's Array notation tradition and almost no "new syntax".

1 Like

Yeah personally, in general, I don't mind syntax sugar - but I want sugar that does not sacrifice clarity.

In my opinion, using [Int] to mean Array<Int> does not sacrifice clarity. It's a small additional thing to learn, but it's visually self-explanatory, mirrors array literal syntax, and Array is so frequently used that you'll be exposed to this very often. In many codebases, this will be the primary way you see array types written. And similarly for [String: Foo] dictionary type sugar. I'm happy with those.

Now, I think I've made my position on this clear already so I won't labour the point, but my concern is really whether "fixed-size" necessarily implies "inline storage". I suspect many developers would like fixed-size regular arrays to maintain program invariants and may not be aware of the other aspects of these types. When I consider the entire picture:

let _: [Int]             // Array<Int>
let _: [1024 x Int]      // InlineArray<1024, Int>
                         // ------      ----

In this case, given the context of what regular array literals mean, I think [N x T] does sacrifice clarity. The full name draws attention to 2 important aspects of the type, while the sugar only mentions one.

I will also say that I feel this was glossed over in the proposal text. Anybody who just read the proposal and didn't read every post of the pitch threads (most of which is debating the choice of operator) wouldn't be informed of this concern.

Swift is a language that makes implicit copies all over the place, and in fact the InlineArray's conformance to Sequence was dropped in the review after I pointed out the effects of this. The proposal authors hadn't spotted it in that instance, and even though I raised the issue of copy behaviour again during the pitch for this sugar, I don't feel the proposal authors have responded with a convincing reason why this is not a problem.

(By the way I did also advocate for InlineArray to be @noImplicitCopy for this reason. I still think we might regret not doing that)

10 Likes

But, I am sure there can be some room for compromise here. :slight_smile:

let u: [3]Int    // An array of 3 Ints

let v: [5][3]Int // 5 arrays, each 3 Ints long

That does not look bad at all, given that there is so much opposition to [m x T] or [m T] which I prefer.

3 Likes

Which I think is a feature, since this isn't multiplication, and therefore should use some non-multiplication symbol. It is a separator, that separates a count from a type. Any symbol that looks like a separator is preferable to any symbol that looks like a non-separator, imho.

1 Like

But it is very likely that many standard functions that accept arrays, will be overloaded to InlineArrays. What will happen in that case, if Array and InlineArray literals have the same form? ([a,...]). Won't it make the type checker confused sometimes, as well as introduce some other unexpected behaviors?

For example, if we overload + operator on InlineArrays with
func + (lhs: InlineArray<n, Element>, rhs: Array<m, Element>) -> Array<m + n, Element> (after the arithmetic in generic parameters gets implemented, or let's assume we've introduced these overloads for small values of n and m manually) - won't the code that worked fine when concatenating arrays suddenly start breaking every time there is some sort of ambiguity between Array and InlineArray? (overloading + was the first thing came to my mind, I am sure there are more interesting use cases).

I understand that, I was just pointing out that if we want to be "consistent", InlineArray typealias should "match" the syntax for InlineArray literals, the syntax for typealiases for Array and Dictionary matches the syntax for literals of these types. Otherwise it might create even more confusion. Some of my comments were under the condition that syntax for array literals might change.

Could you elaborate a bit? I am not sure I completely understand you. Does it mean that examples in SE-0453 like

let a: InlineArray<_, Int> = [1, 2, 3] // InlineArray<3, Int>
let b: InlineArray<3, _> = [1, 2, 3] // InlineArray<3, Int>
let c: InlineArray<_, _> = [1, 2, 3] // InlineArray<3, Int>
let d: InlineArray = [1, 2, 3] // InlineArray<3, Int>

func takesGenericInlineArray<let N: Int>(_: InlineArray<N, Int>) {}

takesGenericInlineArray([1, 2, 3]) // Ok, N is inferred to be '3'.

are not supposed to compile in the future, becuase InlineArray can't conform to ExpressibleByArrayLiteral, or they will compile despite [1, 2, 3] being an array literal, because the literal syntax is hacked in.?

I guess you've meant the "will compile despite..." (please correct me if my guess was wrong).
Assuming this is the case you still have a lot of expressions of form [a...] with ambiguous meaning. Also, if this is a "literal syntax hacked in" - are you certain it will not create more issues later, especially if ExpressibleByArrayLiteral can't be easily fixed to make InlineArray to confirm to it? What will happen if ExpressibleByInlineArrayLiteral gets introduced, and you want you a type to conform to both?
To put it short - won't a many expressions of form [// elements ] with ambiguous meaning cause unexpected problems even with old source code that worked fine?

I assume (I might be wrong again) that InlineArray will be built-in first types and not a part of some library. The problem is that the usages of these types have significant overlap (hence most of standard library functions that work with arrays are supposed to get overloaded for these as well and will become a part of the default namespace) and literals of these types look exactly the same. Not to mention it might create more confusion on the surface.

With new code - it might become even worse. What happens if you have an overloaded code for Array<T> and InlineArray<3, SomeType> (it is natural, since immutable Array of fixed size and immutable InlineArray of the same type are essentialy the "same thing" - differences are implementation details, and you'd like to have the logic to work for both.

When you call `someFunction([a, b, c]) which one will be called?

Let's assume that Array overload is called in this case. Logic is essentially the same and no damage done (probably).

What happens if one overloads a function or initializer for the case when the number of elements is known (And logic benefits from knowing it at compile time) and other for the case when it is not known, using completely different logic? What happens now?

May be I am late to the party (I didn't follow to SE-0453 discussion, and this discussion should continue there, even though it has been accepted) and may be I am just wrong. But even if we assume that there is no problem with type checking performance, which I believe requires further investigation (what happens if we introduce the "natural overloads and protocol conferences for InlineArray into the default envirounment namespace while still using [ //elements ] as the syntax both for Array and InlineArray literals" , the ambiguity of using the same literal syntax for different (as types) but very similar in their usage built-in types might cause a lot of unnecessary clashes in the future. I am sure that all the potential issues mentioned here might have (sometimes very easy) workarounds, but why not avoid them in first place by using (slightly) different syntax for Array and InlineArray literals?

May be we should take a step back, make sure that InlineArray elements don't require different syntax for their literals, change it if needed - and the come back to discussing the syntax for typealias?

1 Like

No.

Since this keeps being brought up I guess I might as well actually respond to the content of the proposal too. I am -1 on the proposal, at least at this time, because I don't agree that InlineArray is a core type, nor do I think it should be used outside of a handful of specialized domains by users who reach for it because it is the only tool for the job. It is not a general-purpose type, and I don't think we should be telling people to use it. It has a number of limitations that stem from the contexts it's meant to be used but are not the right tradeoffs for most users. It's not even a Collection, for heaven's sake! What fraction of Swift developers do we think could verbalize why not? Does the average user know why making an InlineArray of a million elements might be a bad idea?

This isn't a problem, of course, because Array exists and is the best choice for most uses. It's nice, it has a bunch of sugar, and it's what most people should reach for, most of the time. I'm not here to throw shade on other languages but I will note, semi-seriously, that languages with fixed-sized arrays typically use them for creating security vulnerabilities, writing safe wrappers around interfaces written in languages that have security vulnerabilities, or just…don't recommend their use anymore. I think if you asked a programmer who used those languages today if they would rather have sugar for dynamic arrays or fixed size ones in their language, they'd pick the former.

I think a good analogy for InlineArray is that it's like a slightly less esoteric StaticString. Nobody should be switching their default string type over to StaticString, even if they know the string will never be modified. StaticString is by all accounts a worse string except if you need the handful of things that it guarantees that String does not. I see InlineArray as the same kind of type. I don't think StaticString literals need sugar, and I feel InlineArray falls in a similar bucket.

I'm definitely open to changing my mind on this. But the best way to do that is to point to real code that demonstrates your point :)

11 Likes

I'm going to reiterate my point from the pitch thread as to why I prefer of over x:

I would like to see this addressed in the proposal, even if we end up doing x anyways. I'm actually perfectly fine with x as a symbol if it stays only in type position, I think it looks fine, I just know that it's going to get confusing in value position.

1 Like

(Review manager hat off, constraint system implementor hat on)

I'm not concerned about this, because Array and InlineArray "overloads" will nearly always have different base types because they don't share any common protocol hierarchy. The only risk of type checker stress in overload resolution is if we add global functions that are overloaded on an argument of Array vs InlineArray, which I think is unlikely except in this case:

Yes, operator overloading is a fair concern, but that is a concern for any new type that we might want to add operators for. The array literal sugar for InlineArray only makes that worse to the extent that it causes overload resolution to fail later, but literal types are already bound much later than other types because they're polymorphic. That said, adding any new operators on standard library types (which SE-0453 does not) must be done very judiciously and with extensive source compatibility testing for this reason. Separately, we're investigating more ways for unrelated operator overloads to not have this kind of impact on existing code.

Also, to be clear, this is not an issue with this proposal; the array value sugar for InlineArray was already accepted in SE-0453. I suggest we take further discussion about overload resolution performance to another thread. And for what it's worth, the implementation of SE-0453 has been tested extensively for source compatibility and I have not seen any type checker performance fallout due to the API surface and literal support in SE-0453.

7 Likes

Thank you (and others upthread) for pointing this out. When reviewing the pitch discussion, I was much more focused on making sure the choice of separator and other syntax suggestions were covered in the proposal's alternatives considered that I failed to make sure this concern was addressed in the proposal text. I agree that it needs to be, considering that the inline-ness of the storage was the primary consideration for choosing the name InlineArray over other alternatives in SE-0453. I've asked Ben to address this concern in the proposal text.

Holly Borla
Review Manager

14 Likes

In addition to the missing square brackets around each row that @Pippin pointed out, this code also suffers from a significant usability problem.

Namely, what do you want to do with this value?

It is evident that the intended use of op is to multiply it by a vector. And maybe add, subtract, or multiply it by other matrices.

But InlineArray is the wrong type for that. It has no arithmetic operators, it does not conform to any numeric protocols, and we all know that client code should not conform types it doesn’t own to protocols it doesn’t own.

• • •

The correct approach is to declare a custom type, perhaps like this:

struct Matrix<let rows: Int, let columns: Int, Element> {
  var elements: InlineArray<rows, InlineArray<columns, Element>>
}

extension Matrix: AdditiveArithmetic where Element: AdditiveArithmetic {
  static func + (lhs: Self, rhs: Self) -> Self { ... }
  ...
}

extension Matrix where Element: Numeric {
  static func * <let otherColumns: Int> (
    lhs: Self,
    rhs: Matrix<columns, otherColumns, Element>
  ) -> Matrix<rows, otherColumns> {
    ...
  }
}

There are other possibilities, such as making a SquareMatrix type, but the overall point remains the same: many common use-cases for InlineArray are not actually use-cases for InlineArray.

Instead they are use-cases for a domain-specific wrapper type around an InlineArray. That means all the usage sites will actually use the wrapper type, and there will only be one single line of code that declares an InlineArray property, as an implementation detail of the wrapper type.

• • •

The fact that so many of the provided examples for using InlineArray are actually examples for using a Vector or Matrix that wraps an InlineArray, makes me think that perhaps those linear-algebra types should be added to the standard library (or at least a core library). Otherwise the community is likely to see a vast proliferation of similar types in a multitude of disparate libraries, which are subtly incompatible with each other.

And if the motivation for syntactic sugar truly stems from these examples, then it follows that the sugar ought to be applied to the Vector type which will see widespread use, not to the implementation detail InlineArray which won’t.

11 Likes

As someone who's read most of the pitch & review threads for both this and 453 I'm feeling some cognitive dissonance between their discussions. My main takeaways from 453 was that InlineArray is a type that many developers shouldn't shouldn't be using very often, and that the name including Inline was important because of it's storage and associated hidden tradeoffs.

But adding this sugar will completely hide the name, and make it easy for developers to start using it without understanding the tradeoffs they are making. If I didn't read the proposals and just saw this sugar I would start using it in many places where I have fixed size data, and much of that would be wrong.

I expect that if this sugar is added it will cause a lot of misuse of InlineArray, or at minimum developer frustration when they try to use it and it doesn't work with the same APIs as Array

16 Likes

Best suggestion in the thread so far. The higher dimensional ones could be done with commas as well:

let v: [5, 3]Int

Though I still think it is better the way you had it.

I would point out that our precedents in Swift with respect to words that function as binary operators (as, is, in, etc.) are all multi-letter (and most if not all are two-letter words). *

That the Rust folks proposed [4 of Int] until it was realized that they had Rust-specific parsing issues with it gets us closest (I think) to something of a reassurance that it's not a bonkers spelling.

* And if we really want one alphabetic letter only, we can consider the delightful o'[4 o' Int] has a jolly lilt to it that rolls off the tongue :stuck_out_tongue:

19 Likes

I'm concerned this might conflict with a future direction for some of the recent Duration/Clock proposals — you may want 4 o' ContinuousClock

12 Likes