SE-0483: `InlineArray` Literal Syntax

You make a great point. I also see now how [ ] might be necessary if we wanted to allow of to be as expressive as you pointed out.

Previously I have been team no [ ] as in my view the of already indicated that we are constructing an InlineArray.

If we do not intend to have of meaning slightly different things in different contexts (i.e InlineArray or Tuple arguments) then I think that the [ ] is still unnecessary. I am looking forward to more justification of the [ ] as a way to disambiguate it from other potential uses of of

For example (5 of Int) would expand to (Int, Int, Int, Int, Int). In which case, that is reason enough to require the [].

Thank you for that insight.

4 Likes

I was also team "no brackets" at first, but after imagining a syntactic repetition meta-feature (it would probably get its own Syntax type in swift-syntax) I realized that eliding brackets would lock us out of such a feature. :slightly_smiling_face:

So, given all the rationale I've provided so far, I think [count of MyType] is the best spelling for the proposed syntactic sugar.

2 Likes

Given that there is so much disagreement over this sugar, it seems like (if there is to be sugar at all), Swift should borrow Rust’s syntax: [Int; 5].

It has its own problems, but at least there is prior justification for it. Consistency with another language (which Swift already shares many syntactical and semantic similarities with), is at least a reasonably objective justification. Going with something novel leaves it entirely subjective and no one will be happy.

Regarding the performance of InlineArray: I've seen a recurring framing in this thread that characterizes InlineArray as a risky, niche type best used only in specific situations for targeted performance. Meanwhile, Array is portrayed as the safer, default option, and this framing has led some to suggest withholding syntactic sugar for InlineArray: to "force" developers to confront its supposed danger by writing out the full type name.

I believe this is a mischaracterization. In practice, the performance trade-offs between Array and InlineArray are far more nuanced.

In practice, regular Array is one of the most common sources of performance problems in high-performance Swift. If I take a Swift project off the shelf that is supposed to be fast (and by this, I mean really fast – comparably as fast as a well-written C program would be), and profile it, misuse of Array is by far the most common reason why it isn't.[1] Usually this comes in the form of copying the array, and then later mutating it triggering copy-on-write. This can easily turn linear operations quadratic, and of course if the array contains reference types it will be even more expensive. If you're lucky this problem will be locally evident. What's insidious about this is that the performance symptoms (of triggering a copy of the buffer) often often appear far away from the cause (the copy of the array value), and tracking down that root cause can be really tough, similar to finding the source of a memory corruption bug.

Effectively CoW defeats local reasoning about performance. Unpicking this can often be quite painful.[2] For example you might need to start swapping arrays with empty ones, in order to move the buffer between two locations – but this means tracking down all the places in your code that are assuming "these values will be here at this point". If you don't have good unit test coverage, this can be really hard.

On top of this, Array has a number of lesser performance challenges:

  • Array's CoW feature means mutation often requires an additional check for uniquness, which is a tax like reference counting.
  • It's easy to forget to reserve space up front, causing multiple reallocations. Yes exponential growth means these are amortized, but they still cost a lot in a hot loop.
  • The dynamic size means the compiler does not always emit optimal code when looping over the array, even if in theory the size is statically known at compile time.
  • Two dimensional arrays are syntactically appealing i.e. [[Int]] but almost never a good idea.

By contrast, InlineArray's one specific performance challenge – that it costs to make a copy – is at least something you can reason about locally. The perf hit will show up in the trace exactly where it is caused, and so is easily resolved. To me, the naming of InlineArray is not "a warning". It's just the best name to describe what this type is: it's an array type that holds all its elements inline. The fixed size nature of it falls out of that property.

The name Array is the anomoly here, because it makes no effort to be more descriptive of its characteristics. I expect if we had a naming thread about it today, there would be a large constituency for calling it COWArray or DynamicArray or even GrowableCopyOnWriteArray.[3] However, I do not believe such naming would help fix the performance troubles folks hit while using it, nor should it mean Array shouldn't have sugar. And the same goes for InlineArray.

This is not a "this thing is bad, therefore we should allow this other thing to be bad" argument. It is more that there is far more to writing high performance code than just being prompted by some naming. If you want your code to be fast, you really need to understand the characteristics of the types you are using, and the trade-offs they bring. I do not think sugaring the syntax of this type changes that calculus either way.


  1. Misuse of classes is the second most common. In fact, these two frequently combine together and compound each other. ↩︎

  2. Much like if you've baked reference semantics into your code by using classes, only to find the classes should have been structs for performance. ↩︎

  3. We will experience this fun soon, since I believe it's important to add a noncopyable growable array type - one that prevents accidental sharing and does not require refcounting or checks for uniqueness. ↩︎

27 Likes

This also comes up a few times in this thread, and generally is cited in many other review threads. The problem is always that it characterizes clarity and brevity as being two ends of a slider: that if you make code more terse, it makes it less readable, and vice versa.

This isn't true though. Swift code is more readable because of its terse syntax. A syntax that keeps out of the way, allowing you get to the heart of what business logic is actually doing, is immensely valuable. There are numerous examples: 0..<5 is more easily readable than Range(0,5), struct S: P is more readable than structure S implements P, and so on. Whenever I see C or Objective-C or Java side by side with it rewritten in Swift it is always striking to me how much shorter and clearer the code has become. What the code is doing, not the ceremony of the language, is what is forefront.

This is of course subjective. Many do feel for example that Objective-C's verbosity aids rather than hinders clarity. It is not, though, a belief that tends to drive the future direction of Swift.

14 Likes

I am sympathetic to this since it is prior art. I am not an avid user of Rust; however, from what I understand, this delimiter ; can be used in both the declaration and for creating a repeating entries in an array (similar to what we want).

let a: [i32; 5] = [1, 2, 3, 4, 5]

and

let a = [0; 5];

I am not sure how this could be used to accomplish what was suggested below of the operator of being used to express regions of repeated values.

Using a ; this would become

let a: [5 of Int] = [1, 3 ; 10, 3] // equivalent to [1, 10, 10, 10, 3]

which I believe looks out of place. It kinda messes with how one thinks the parts are grouped. ; feels like it takes precedence over ,.


Use of x

I also want to point out that the use of x here would be inferior to of

let a: [5 x Int] = [1, 3 x 10, 3] // equivalent to [1, 10, 10, 10, 3]

as it almost appears like multiplication (which is, I only believe, what most people would associate x with, maybe…)

3 Likes

This is the best argument against x I've seen so far in context of the proposed syntax sugar specifically, thank you!

Even though the "multiplication" aspect of it may feel at home when it comes to describing a repetition of a type as a way to refer to an InlineArray, it doesn't carry over to other potential use cases of syntactic repetition (like repeating a number in a list of numbers), where it will conflict with numeric multiplication.

1 Like

As we a probably deep in the territory of "I only read a fresh post when its author is prominent" :joy:, I might as well add the results of my investigation for the actual reason why I (and maybe others) am so firmly against using the letter "x" for syntax.

X is not a multiplication sign, it is literally the symbol of arbitrariness.

Besides the famous Mr. X, we have

  • "The X marks the spot" (location on a map)
  • XL (size of a T-Shirt)
  • f(x) = x (equation)
  • Xmas (quite similar to the use discussed here, probably a case of "it looks like")
  • 2x4 lumber (multiplication of dimensions)
  • 4x4 drive (really, what should this mean? A car with four wheels, and each wheel has four ??)

Oh, and not to forget: Forbidden words, where each letter is replaced with an x.

Wheres one of those many different meaning is actually discouraged in formal writing, we are about to introduce it in an even more formal context.
Yes, the parser can handle it, and probably most humans as well - but there are lots of alternatives which are as well suited in this case (and not in half a dozen completely different cases). There is no pressure to use a letter that happens to look similar to an established symbol.

Maybe it helps to extend the logic for choosing x to other examples:
There are some useful operations on sets which could be written in a very concise way — but still, I don't think there would be much support for a proposal to allow code like
if id E allowedIds, if selection c validChoices or let aboveAverage = fiveStars U fourStars.

4 Likes

It means power is delivered to four of the four wheels, unlike a front-wheel drive car where power goes to two of four wheels, a truck that has power to six of six wheels, or a motorbike where power goes to one of two wheels.

5 Likes

I know — although I'm really not interested in cars. The point is that this has nothing to do with multiplication (or my knowledge of automobiles is really terrible).
Imo it would be way more logical to write 4/4 instead (four of four wheels are powered).

3 Likes

In Rust this syntax is reversed, [10; 3] gets you [10, 10, 10], and as far as I can tell they don’t allow mixing it with other comma separated expressions. If that were to be supported it feels like it would be better to require parentheses, or even [1] + [10; 3] + [3], otherwise it gets hard to read regardless of the syntax sugar used.

Good catch. I wasn't taking care of noticing the reversal from the proposed Swift syntax -> [N by Type] vs [type ; N].

I would also argue that there is a value in preserving the order that it is in the declaration. That is that the count comes before the Type as in InlineArray<let count: Int, Element>. Which for me further disqualifies the Rust syntax.

1 Like

What about inline for x or of?

let xyz: [3 inline Int] = [0, 1, 2]

(I thought, "both a symbol and an operator"!)

The target meaning also has many confusingly similar variants/contexts -- nested/multidimensional context (NM); declaring types vs. literals (TL); different concrete types in the array family (CF). So the target meaning is some point in a field of meaning with an "order" combinations of [NM, TL, CF]. The need for specificity is thus great.

Assuming the task of recognition is a combination of term disambiguation and meaning-binding, the sugar for inline array presents a very high dimensional problem.

Term recognition is also a problem that's difficult to talk about. The ambiguity of x is very hard to appreciate for those who recognize the term in a related context (math, types), or for those who've learned it for this usage. It's neurologically impossible to unsee a pattern one has seen, and language is all about this kind of sub-awareness leap of recognition. Teachers and experts need to take the most care when working with students and lay people on the other side of recognition.

Difficult? yes. Possible? yes - A good heuristic in these situations is to be as literal as you can - hence inline.

The term "inline array" is great because that it's weird enough to distinguish it and yet indicates the essential difference in behavior. It offers both arousal and direction.


This to me is the entire motivation behind inline array: it makes the performance issue a matter of local reasoning. That should be placed in all caps at the beginning when describing what it is and why one would use it.

Being inline makes it easy to talk about values being on the stack, and possible to do/avoid complex memory management -- all while avoiding the action-at-a-distance of reference values. It's very Swifty.

That's also what makes inline great sugar.

I don't fully disagree. There is something really nice about inline, so I would like to explore the wider usages.

In one of my previous posts and technogen's we noted the usefulness of re-using the operator as well for expressing regions in Arrays, InlineArrays and Tuples. I am quoting a couple regions below so you can reference those posts.

As well as building on xwu's idea that x doesn't lend itself to syntax that involves some

How do you feel about the following:

✅ [5 inline Int] // becomes InlineArray<5, Int> or Inline [Int, Int, Int, Int, Int]
❌ (5 inline Int) // becomes (Int, Int, Int, Int, Int)` 

✅ var x = [5 inline 6] // becomes .repeating(x, count: 5) or Inline [6, 6, 6, 6, 6]
❌ var y = (5 inline 6) // becomes the tuple value (6,6,6,6,6)

✅func build() -> (some inline some BinaryInteger) {}

Where I've placed an :cross_mark:, it should be noted that inline can't be used there as it doesn't make sense for use within tuples.

As for (some inline some BinaryInteger), I think inline works here, but it's not as cleaer (to me) as of.

Again, we don't HAVE to make this useable for tuples, but it would be nice.

NOTE:

When writing this I also realize that we'd have to also decide if [5 of 6] would be assumed to be an Array or an InlineArray as theoretically it could be used for both. So, for example

var x = [5 of 6] //is x an array here or an inline array? 

Would we then require the type to be explicit in these cases?

var x: [Int] = [5 of 6]

Would the rule be 'in the presence of of, the Array literal is assume to be an InlineArray?'

1 Like

Without having much thought put into it, I'd assume that this would not affect the literal type inference logic at all, if the intended effect of this syntax would truly be to mimic explicit repetition.

I suppose since the assumption is that InlineArray adopts ExpressibleByArrayLiteral, regardless of of it is always going to be an array declaration.

But maybe some exceptions are possible? Examine the following:

var a = [1,2,3,4,5] // a is Array
var b = [2 of 3] // b is Array or assumed to be InlineArray(?)
var c = [1, 2 of 3, 5] // c is Array or assumed to be InlineArray(?)

In the case of b and c would the compiler infer that the literals are Array or InlineArray giving special treatment to the presence of of allowing for us to elide the Type in var declarations for InlineArrays all together.

therefore

var b = [2 of 3] // b is inferred to be InlineArray as of is present. 
var c = [1, 2 of 3, 5] // c is inferred to be InlineArray as of is present. 

I am assuming that we'd still allow of in the regular Array literals when Array is explicitly named.

var a: [Int] = [1, 2 of 3, 5]

I personally don't have enough hands-on experience with using InlineArray, so I don't think I can provide useful information on this, but I think taking language change one step at a time may be a good way to go, so that we can utilize hands-on experience in reasoning one way or another.

1 Like

It seems to me that every proposed syntax here falls into one of three categories:

  1. Syntaxes that are short and emphasize the "array" aspect of InlineArray, but don't visually indicate the "inline" aspect
    • E.g. [n x T], [n of T], [n * T], [n # T], T[n], [n]T, [n; T], [n T]
  2. Syntaxes that are short and emphasize the "inline" aspect of InlineArray, but don't imply the "array" aspect as strongly
    • E.g. n of T, n x T, (n of T), (n x T)
  3. Syntaxes that emphasize both the "inline" and "array" aspects of InlineArray, but are verbose
    • E.g. [n inline T], n[inline T]

Whichever syntax we pick, it seems we'll need to compromise on either length, the emphasis of "array-ness", or the emphasis of "inline-ness".

If we compromise on length, that negates the point of having syntax sugar in the first place. [n inline T] is just 5 characters shorter than InlineArray<n, T>, and it doesn't significantly decrease cognitive load.

If we compromise on expressing "array-ness", it could lead to confusion for people encountering the type sugar for the first time, since there's no indication of how to use it. That being said, most programmers are adept at looking up information on things they don't understand, and they'll often notice it's treated like an array in the surrounding code. Considering those factors and Swift's strong type system, it appears this compromise is unlikely to introduce bugs into programs.

Another argument against a compromise on expressing "array-ness" is that the both the current type sugars for collection types are wrapped in brackets. Surrounding the InlineArray sugar with brackets would fit this pattern.

If we compromise on expressing "inline-ness", it could lead to unexpected performance problems. This compromise wouldn't just cause confusion but also potentially mislead the programmer, who might expect Swift's usual copy-on-write semantics to apply to the type. This seems likely to cause performance issues, which are already a major pain point for Swift.

2 Likes

Considering the following bare options:

n x T , n of T , n * T , n # T , T[n] , [n]T , [n; T] , n T, n inline T

I think that we need to evaluate the following.

• Does the keyword, operator or delimiter help convey "N units of type T" or "N units of value N" to the reader.

• Does the keyword, operator or delimiter have the flexibility to work at the declaration site as well as the use site.

ex: var a: [10 of Int] and a = [10 of 10]

• Can the keyword be expanded into other environments (tuples).

ex: var a: (5 of Int) is the same as var a: (Int, Int, Int, Int, Int)
ex: a = (5 of 2) is the same as a = (2,2,2,2,2)

• How does it look and work in the context of opaque types.

ex: func build() -> [some of some Type]

• How does it look with (I forget the name but _ implied elements )

• Can the keyword be used in other ways.

ex: var x: [5 of Int] = [6, 3 of 4, 2] for var x: [5 of Int] = [6,4, 4, 4, 2]

The purpose of including some of these not yet proposed features is to examine how useful a new keyword can be, and allowing us to think forward.


Putting it to the test

Let's try n # T, n x T and n T as an example.

n # T

var a: [10 # Int] and a = [10 # 10]
var a: (5 # Int) for var a: (Int, Int, Int, Int, Int)
a = (5 # 2) for a = (2,2,2,2,2)
var x: [5 # Int] = [6, 3 # 3, 2]
var x: [6 # Int] = [1 , 4 # 4 * 2, 3] // [1 , 8, 8, 8, 8, 3]
func build() -> [some # some Type]

For me, # just doesn't convey the message "N units of type T" or "N units of value N." Outside of that I don't have much against it. It technically works. I suppose it's strong association with Macros and #if statements disqualifies it for me.

n x T

var a: [10 x Int] and a = [10 x 10]
var a: (5 x Int) for var a: (Int, Int, Int, Int, Int)
a = (5 x 2) for a = (2,2,2,2,2)
var x: [5 x Int] = [6, 3 x 3, 2]
var x: [6 x Int] = [1 , 4 x 4 * 2 , 3] // [1 , 8, 8, 8, 8, 3]
func build() -> [some x some Type]

My biggest grip with x is that it really just looks and feels like multiplication. Even if I got past that -- which we all can, we're all smart-- it feels wrong here [6, 3 x 4, 2]. That looks absolutely like multiplication. This is more difficult to mentally parse in [1 , 4 * 2 x 4, 3]. Some may wonder if someone would even do this, regardless, it is possible. It's hard to see that 4 * 2 and 3 are separated by x.

n T

var a: [10 Int] and a = [10 10]
var a: (5 Int) for var a: (Int, Int, Int, Int, Int)
a = (5 2) for a = (2,2,2,2,2)
var x: [5 Int] = [6, 3 3, 2]
var x: [6 Int] = [1 , 4 4 * 2, 3] // [1 , 8, 8, 8, 8, 3]
func build() -> [some some Type]

This falls apart when we try to re-use for [10 10] or [some some Type]. I don't think that is acceptable.

n ; T

var a: [10; Int] and a = [10 10]
var a: (5; Int) for var a: (Int, Int, Int, Int, Int)
a = (5; 2) for a = (2,2,2,2,2)
var x: [5; Int] = [6, 3 ; 3, 2]
var x: [6 ; Int] = [1 , 4 ; 4 * 2, 3] // [1 , 8, 8, 8, 8, 3]
func build() -> [some ; some Type]

This one looks OK for the declaration [10; Int] but feels wrong for [some ; some Type] It looks the most unnatural. This doesn't seem to fit Swift as well. ; is an expression and statement delimiter. I know we can have a special meaning within [ ]and ( ) but that seems awkward. I also want to note that this is the reversed to Rust (I believe).

2 Likes

To me of is just noise, just like x, so I would drop them entirely.

[count MyType]