SE-0483: `InlineArray` Literal Syntax

I share that concern and wonder if, should the consensus of the community conclude that sugar for InlineArray is warranted, perhaps we should consider sugar that clearly communicates the important bit. Would a macro before an Array literal be appropriate?

e.g. an @Inline(3) [Int] that expands to InlineArray<3, Int> would still express the "inlineness" of the type, which I feel is important to communicate at the use site (and the community felt was important enough to change the name from Vector in the original pitch).

2 Likes

Yes! This communicates all the important things in a clean and concise way. If sugar happens, this is the way I hope it does. It's very clear at the use site that what you are creating is not a regular Array and inline plainly tells you what to expect for this type's storage.

1 Like

This makes it ambiguous as to the type a well, I read this as initializing a 2-dimensional matrix.

4 Likes

I'm overall +1 on adding sugar, and x looks fine (still not convinced though). But also feels like proposal is coming too early—people need to work/play a bit with InlineArray first.

As a general question, is it possible or likely that this syntax will become the literal for fixed size collections? Similar to [] for Set, would it be possible to simply make InlineArray the default inferred type for the literal, rather than the only type which can use it? Also, should we hedge against future fixed size dictionary-like collections and ensure the multiplier syntax works for both? e.g. [5 x String: String] or [5 x [String: String]] (neither are great there with the x operator).

2 Likes

This has been perhaps mentioned or eluded to before by others. I am not sure that a sugar for the InlineArray type is actually a good thing. As a matter of fact I think that it has potentials to be actively harmful to the intent that developers have when using it.

The common and expected use case for InlineArray is to store things inline inside a type, hence the discussion on the name of that type versus Vector. Particularly the name was chosen to express the "inline storage" nature of the type if I recall correctly (or at least that was the argument that was compelling for me...). The desire was to let developers know that perhaps passing these as arguments might not be ideal since there would be a copy that would be incurred.

How often do we find folks typing the signature of methods to have parameters of Array<T>? Instead we often type and find in code signatures of [T] - because the sugar is easy to understand. By having an easy to type signature of [5 x Int] or [5 of Int] etc, that will proliferate InlineArray as a type used as an argument; mainly because it looses the storage hint in the name. Which will by that additional usage, may obviate the performance gains that type is expected to grant. It will incur copy costs at the calls of those methods. Alternatively we should be encouraging folks to use Span or other such types as the parameter passing mechanism.

I find the concept of literal construction and repeating initializers very helpful and perhaps even required to use InlineArray. However, I don't believe that any cases of usages of this type would outweigh the costs from the existence of the shorthand.

9 Likes

While I'm not certain exactly what the 'right' level of callout for the inline-ness of InlineArray is, I do think that even with sugar in place we still derive substantial benefit from having the name of the type be InlineArray—it's the term that will be used to refer to this type (just as everyone still calls [Int] an "array of ints" or "int array"), it's the name that will show up in documentation, etc.

The choice of InlineArray for the name was never a decision that the expensive-to-copy nature of this type must necessarily be visible at the point-of-use, or in every interface. While sugar for this type might eschew the name a bit more, I don't think a decision to adopt a sugar which omits 'inline' contradicts the value of having InlineArray be the name (though I of course recognize the tension that reviewers are pointing to).

5 Likes

While I understand this sentiment, they don't have to be identical. We can have a declaration that conveys the vital implication that the type is inline (ideally by using n inline T in the definition) while having a different sugar at the call site (one that can be used for multiple contexts). I like the n of T as sugar for n comma-separated instances of T, and would like to see it available in many contexts such that all of these work:

let a: [Int] = [0, 0, 0] // Array<Int>
let a2: [Int] = [3 of 0] // equivalent to a
let b: [3 inline Int] = [0, 0, 0] // InlineArray<3, Int>
let b2: [3 inline Int] = [3 of 0] // equivalent to b
let c: (Int, Int, Int) = (0, 0, 0) // a three-arity tuple
let c2: (Int, Int, Int) = (3 of 0) // equivalent to c

I see the n of T to just be a shortcut to typing out T, T, T, ... out to n Ts, and I think the type inference should be the same as if you had typed it out in full, so if all type annotations were removed from the above code I'd expect the type of a, a2, b, and b2 to be Array<Int> and for the type of c and c2 to be (Int, Int, Int).

Despite the name of the topic, the literal is not what we are actually evaluating here, since that's relegated to the Future Directions section, so that can be saved for the inevitable future discussion. I just wanted to point out that the two do not have to be identical, and the priorities of characteristics for the two use cases are not the same. The type definition can be more explicit about the type's storage while the value sugar for the literal can be shorter and more flexibly applied across multiple types.

3 Likes

Right. The type is called InlineArray because the type needs a name and that name describes well what the type is. It is not there as a "here be dragons" warning.

As I described upthread, both Array and InlineArray have specific performance characteristics that are good or bad in different circumstances. This is not an "Array is fine and InlineArray is risky" situation, and as such it is a mistake to think that Array needs sugaring, but sugaring InlineArray is dangerous.

It is very common to see Array used for simple fixed-size values – for example let's say you're translating this code from the popular Ray Tracing in One Weekend tutorial:

class vec3 {
  public:
    double e[3];

    double x() const { return e[0]; }
    double y() const { return e[1]; }
    double z() const { return e[2]; }

    // etc
}

The way in which Swift privileges [Double] with sugar strongly implies you should us that in this translation.[1] This would be a performance disaster. Every access to those coordinate accessors will need to check the bounds and, in the case of mutation, uniqueness of the pointer. Every vec3 would start from the assumption of needing to allocate except when the compiler can do sufficient escape analysis to see it doesn't. Copying a vec3 type would also incur reference counting that the compiler won't always be able to eliminate. This type is the hottest thing in the entire program, so will lead to an implementation way slower than the C++ equivalent. InlineArray has none of these problems.

There is no way to name our way out of understanding the performance characteristics of these types. I do not think it would help to name Array to CopyOnWriteDynamicArray nor do I think removing sugar from it would be appropriate just because it has these downsides. The same is true of InlineArray. And remember, every other language with an inline representation has sugar for this, and people are expected to understand what that means (in fact, most other languages comparable to Swift sugar the inline version, and do not sugar their dynamic version).

What is important is that we help shift the Swift culture around writing performant code away from "arrays are free and easy, don't worry about it" and towards "performance needs care, consider how your underlying code works and what the right tool is".

Of course, this only matters when you are trying to write code that maximizes performance. But that is a really important use case for Swift. The goal for Swift is a language that is as safe and enjoyable to write as many high-level non-performant languages, but also can achieve peak performance when that is your goal. And the idea is that when you are targeting that level of performance, you don't have to go into "ugly, no longer nice swift" mode to do it. For these reasons, we should be considering InlineArray a peer of Array (even if the need for it is less common – just not "niche").


  1. of course, there are other options in this case, like SIMD3<Double>, but that isn't always an option for many use cases. ↩︎

25 Likes

The recent posts by @Jumhyn & @ben-cohen have made it clear to me that the expectation for InlineArray is that it will be used for more than just "backing" properties, which makes sugar desirable. They've also reminded me that I'm so used to Array that I don't consciously think about it's costs like I am with InlineArray. Hopefully between a wider adoption than I expected and good documentation/tutorials about it's usage & tradeoffs my concerns about a sugar hiding the Inline property won't be the problem I expected.

So while I'm now in favor of sugar I'm still not convinced that [5 x Int] or similar is the correct syntax. My biggest sticking point is that by adopting this syntax we will be favoring InlineArray over any future fixed size Array types. I understand we can't block the present on a theoretical future, but since this will potentially block sugar for future fixed size Array types I think we either need to pick a sugar that can support other fixed size Array types, or address how they would (not?) get sugar.

I also have minor issues with x specifically, but nothing that hasn't been said many times already.

1 Like

I 100% agree with Ben's two really good assessments (A, B) of Array and InlineArray as peers but different. And I'm going to repeat my hope that he'll make an explainer blog post when InlineArray hits.

They don't make me feel better about having ambiguous sugar, though.

It's not that I don't want this type to never have sugar. I just don't want its entrance into the language delayed by finding the right sugar. Nor do I want it saddled with not quite right sugar just to not delay its release.

People have been really quick to generalize of their favorite sugar flavor "Oh look we could use it in so many places!"

Which sends up flags to me.

So, for example, what types do folks see the following declarations being inferred as? Arrays or InlineArrays or...... ????

let inferred = [3 of 0]
//or say
let inferred2D = [5 x 3 x 0]

Would these raise compiler warnings about ambiguous type? In which case it isn't sugar for InlineArray at all, because you still have to put in the type. (which is why I put in a vote for rethinking [...])

Perhaps what we are really discussing here is sugar for repeated values of a single type in general. That could be a really handy language feature. A lot of really good thinking has gone on in this thread about that. But perhaps its pitch should be disentangled from InlineArray, which won't be the only customer?

1 Like

The one is simple. inferred is unambiguously a zero initialized InlineArray<3, Int>.
The second is a compile error, as the correct version would be written: [5 x [3 x 0]], which is unambiguously a zero initialized InlineArray<5, InlineArray<3, Int>>.

Literals have a default type, and the default type for a future [n x value] or [n of value] literal would naturally be InlineArray.

That’s interesting because I think I would be surprised by this. I would expect the value sugared [3 of 0] to be equivalent to typing out the repeated zero, as [0, 0, 0] which is clearly and unambiguously an Array<Int>.

I’d like the type sugar to be clear what it’s defining and the value repetition sugar to be more succinct and also more widely applicable. I’d like a static zero method on InlineArray to give you an instance initialized to all zeros when Element: Numeric to make getting a new zero-filled InlineArray as easy as

let inferred = [3 inline Int].zero

(I like n inline T for the type sugar and n of value for the value sugar).

3 Likes

Some folks here are speaking to the question about whether the InlineArray type should ever have syntactic sugar, while others have asked whether the type should now have syntactic sugar (versus possibly later).

I think all can appreciate the very concrete benefits for a decision where, if the InlineArray type needs syntactic sugar, its being delivered now means that introduction of the type and introduction of the sugar aren't fragmented over multiple releases. @scanon also makes a good point above (or has it been moved into the other thread about the review process?) that a decade has passed without terribly much in the way of new ideas, suggesting that waiting longer won't necessary get us better results.

However, your post here illustrates one question which we don't have a good answer to now, and which we will have a answer to (probably soon) after the feature ships: we will know (instead of having to expect) how the type will be used. It is noteworthy that there is a divergence of views currently on that point, and one perspective or other will inevitably be proven out in due course.

7 Likes

It's worth remembering a weakness that has affected previous discussions of a repeat n { ... } syntax, which is probably inevitable every time we try to quantify repeats:

"Repeat once" is usually (correctly) taken to mean "do twice," and so by induction "repeat 5 times" is ambiguous as to whether the total count is 5 or 6. Most other words (or even x) are unambiguous in that context.

7 Likes

It's also true, however, that syntax sugar can massively influence how the type is used. So there's a risk of creating a self-fulfilling prophecy: since InlineArray may not be a currency type, the sugar for it is put on hold, causing developers to avoid using InlineArray as a currency type.

I think in this respect it makes sense to look at how other languages are doing it. Are inline arrays used as currency types in other languages? Is there a reason —other than the sugar— why Swift developers would (overwhelmingly) prefer to use a wrapper type over InlineArray, for those same use cases?

In my particular bubble, the answer to that last question is "no". There are far too many examples in which I have wished for an InlineArray while writing performant(ish) code in Swift yet I'd have had no interest in wrapping it into a custom type.

2 Likes

Swift also has the repeat keyword for variadic generics. The syntax for those is already relatively confusing (I don’t struggle with most Swift syntax, but I get at least one thing wrong each time I use variadic generics). Introducing another way one of the keywords could be used in a non-variadic context would only make this worse.

3 Likes

(tldr: inline + repeat is viable and arguably preferable, and does not suffer from ambiguity of the count)

Let's be fair to the proposal for repeat. The phrase is "repeating {something} {some number of times}".

The current usage in Swift for initializers disambiguates the count explicitly: init(repeating: {something}, count: {total number})

The same is true for the proposal for repeat usage in InlineArray, where the requirement is to put the count type parameter first. (repeat is used here to distinguish the keyword form from the parameter name repeating.)

let ira: InlineArray<6, Int> = [6 repeat 7] // with count
let ira: InlineArray<6, Int> = [repeat 7]    // count elided

So the problem is not the ambiguity of the count.

The problem is that repeat alone does not indicate that this is an inline array.

By default we should ask the literal to work on its own, both in meaning and in understanding:

let ira = [6 repeat 7] // what kind of array is this?

Here the sugar is not clear to the uninitiated (though it is probably clearer than x or or). Most uninitiated readers would read this as the stdlib Array.

That leads to the other problem with literal sugar: distinguishing members of a family of types with such repeating literals.

That led to being able say inline for the InlineArray literal sugar, notwithstanding the duplication, and use "repeat" only for the thing being repeated (not the count), in place of a series where the count is known. That also means "repeat" would work for repeating series of other series-consuming types.

So what are the rules for inline and repeat? This proposal is constrained to say the InlineArray count always precedes the type. It's fair to say that bidirectional type inference (B) should work for both count and element type

That gives us the following:

Code Rule
IT inline is the distinguishing copula in the sugared type declaration
IL inline in the literal indicates this is an InlineArray type
BC count of the InlineArray can be inferred from the type declaration or the literal
BT The element type of the InlineArray can be inferred from the type declaration or the literal
SC the count of the InlineArray can be inferred from the count of a literal series
ST the type of the InlineArray can be inferred from the type of a literal series
RL repeat x in the literal indicates a series with a repeating literal where the count is otherwise given.
CP The count may be a type parameter from the scope
TP The type may be a type parameter from the scope

The type declaration is:

Type Rules
InlineArray<3, Int>
[3 inline Int] IT

Here are the key combinations of type declaration and literal (imagine let ira {: type} = {literal}):

Type Literal Rules
- [inline 0, 3, 5] IL, SC, ST
[inline] [0, 3, 5] IT, SC, ST
[inline] [3 repeat 7] IT, BC, RL, ST
[n inline] [repeat 7] IT, CP, RL, ST
- [inline 0, 3, 5] IL, SC, ST
- [n inline repeat 7] IL, RL, ST, CP

If we also permit expressions in the repeat position where the index is available as a parameter:

Code Rule
RE repeat e in the literal indicates a series with a repeating expression where the count is otherwise given.

Then we also get repeat expressions:

Type Literal Rules
- [n inline repeat { T($0) }] IL, RE, ST, TP, CP, BC
[n inline] [repeat { T($0) }] IT, RE, ST, TP, CP, BC

Some might argue that repeat or inline could be implied in some of these cases, but I believe that forecloses other types, and that the original stdlib Array should be the default.

Alongside inline, imagine using the same keyword-type pattern for the following related types:

  • buffer: an inline array which can be resized within bounds on copy (where the count parameter is a Range<Int>)
  • slots: inline storage of different types with the same shape accessible via Int index (where the type parameter can be a constrained tuple or parameter pack?)
  • and so on...

I believe this pattern would work for all such types.

I think both logic and discussion have shown "n x m" to be a defining example of ambiguity -- for the uninitiated. But x may indeed be preferable in a world where we can assume readers already understand the context and what x means. That could be fair: "Sugar" by definition is derivative, working as something easy to write for something otherwise hard to say. But the question is whether one must already know that association, and whether that particular brief expression scales across code.

Postfix '?', though considered sugar for Optional, is probably the conceptual form for readers, particularly given the corresponding infix ?? and postfix !. In that case, the sugar is primary or at least stands on its own. [ ] for Array is similar in this respect. Both are nicely orthogonal to the types they qualify and thus scale. But they might be rare if happy outliers.

I believe Swift should aspire to bidirectional inference between sugar and sugared, akin to what it does for types, and for the same reasons. Any proposal for a sugared form that does not clearly indicate the unsugared form (preferably without training) should have a higher bar for acceptance, minimally superiority in usage over alternatives. The loss of naive and local reasoning at scale is a burden.

I believe the combination of inline and repeat is an alternative, one not precluded by the question of count. The question (assuming it's still feasible) is whether its lack of brevity is made up for by the increase in clarity and teachability, the bi-directionality of understanding, and the ability to scale to related types.

2 Likes

I do, actually, think that 6 repeat 7 is ambiguous as to whether it means “6 repeated 7 times” or “6 repeats of 7”.

20 Likes

To paraphrase a portion of that impressive post[1]:

With all due respect, yikes! While I appreciate the thought that's gone into this, I'm of the opinion that using multiple distinct keywords for the new sugared syntax, and 9 (maybe 10) rules for applying them, has gone way too far into the weeds for this language we call Swift. I'm not sure if initializers even have that many rules and intricacies.

In contrast, I'd suggest that there isn't much justification for using anything but matching syntax for sugared types and values literals, ie. the same keyword or symbol for both [N blah Type] and [N blah value] as suggested in the proposal, even if it means less expressive power.

In any case, thinking in advance about these possibilities isn't a bad idea, but this review is specifically for "sugared version of the InlineArray type" and despite being named "literal syntax" as it is, leaves sugar for value literals as a future direction. There's been so much in this thread already I think expanding the discussion into fine details of that future direction is a mistake right now.

+1 for the proposal from me, even though I'd prefer keyword of instead of x.


  1. i'd quote the whole table but my markdown-fu is not that strong ↩︎

7 Likes