[Second review] SE-0483: `InlineArray` Type Sugar

This is a good point. It could be misinterpreted as let a: Int = 5. But it shouldn’t take them long to realize that Swift's variable declaration is always let <identifier>: <type>.

I am not against [] in general, as [] directly implicates indexability. But, it is not intuitive that of is part of [] in [5 of Int]. 5 of Int wants to become a separate expression. Consider expressions like [5 is Int], [5 as Int], all of these mean an array literal of something. It’s almost like we need to use [5, of: Int] to stress that they are together. Perhaps, it’s reasonable to do something like [5|Int], [5,Int], or [5’Int] to give the impression that the separator is part of the [], just like how : seperates dictionary literal [A:B]. 5[Int] could also work.

2 Likes

Same for me. The [N x T] felt so much clearer and provided a better path for future directions.
For me, [7 of 9] does not imply a multiplicity in any way and yes, in [7 x 9] you have to remember in which order to read it, but it's the same in InlineArray<N, T>.
I'm coming more from the embedded and systems programming background and was very fond of the first draft of this proposal.
I have the fear that we choose an inferior syntax, just because that first draft got so much backlash by a few loud people. Now everybody is tired fighting and just wants to move forward again.

So for this version of the proposal, I'm -1. The syntax [N of T] may be shorter but in my view it is inferior to writing the full InlineArray<N, T>.

4 Likes

I think that [N of T] is a very reasonable approach, and appreciate the effort to revise the initial proposal.

What I still struggle with as a non-professional academic tinkerer is tracking when and where CoW applies, since there's little (or no?) indication in the language. When Swift was first introduced CoW was a big feature — we don't need to worry about it, it's built in, and we can take advantage of it whenever we like.

Now that we're moving to performance sensitive code, CoW isn't an advantage, and it makes sense to introduce types that elegantly and concisely help us code in those scenarios. But this also feels like a trap with no warning to others. Why does CoW apply to [N] but not to [N by t], and how can I look at code and understand when and where it's being applied without spending a lot of time digging through the manual.

Part of me wonders if we might want to introduce a small annotation to help distinguish the inline-ness of the literal. But I also have no sense of future directions, and what other cases might benefit from this. e.g., I assume our thinking is that our most common cases are CoW + dynamic sized [N] and non-Cow and fixed size [N by T], but why those specifically?

Will we ever have non-CoW literals for [K: V], or other types? And do we want to consider how we would specify those now?

8 Likes

I know it's outside the scope of this discussion but it's tangental to it: When we do get a pitch for the value literal sugar, should we expect an ExpressibleByInlineArrayLiteral protocol to be inherent in that pitch or would we expect the sugar to be exclusive to InlineArray and "hard coded" to work with it?
Actually, is there a particular reason why the value literal is not part of this Proposal? It seems like the needs of each separate sugar inform the design considerations of the other.

8 Likes

I agree.

Following the discussion I constantly think about what would this argument mean for a future value literal syntax? How does that proposal fit with an ExpressibleByRepetitionLiteral (as I like to call it).

I‘m not sure the separation of type sugar and value literal really is helpful. It would be great if the design of both would work together intuitively.

On the other hand, it might broaden the discussion too much. There are at least two aspects to the literal to consider:

  1. The repetition literal should be designed so that other types can use it as well:
let inlineArray = [4 x 0]
let array = [4 x 42] as Array
let string = [4 x " "] as String
  1. InlineArray needs to support other literals, too. Repetition is just a special case of initialization.
let zeros = [8 x 0]
let fib = [1,1,2,3,5,8] as InlineArray
4 Likes

InlineArray can already be initialized by a regular array literal.

2 Likes

Which is already interesting because (at least according to the docs on developer.apple.com) InlineArray does not conform to ExpressibleByArrayLiteral and does not have an initializer that protocol requires, so I guess it’s already using compiler magic for that.

1 Like

This is discussed explicitly in SE-0453.

2 Likes

It makes sense to use compiler magic for literals anyway. It would be rather facepalm-y to have to allocate on the heap to initialize a stack-allocated type with a literal, though it seems like even that could be avoided while still allowing the ExpressibleByArrayLiteral conformance. Unless I'm remembering wrong, variadics in a function declaration are actually treated as Tuples "under the hood" so I'd expect Array literals to be able to be handled without heap allocation by the compiler (or am I also misunderstanding how Tuples are allocated?).

Aren't variadics treated as Arrays under the hood?

2 Likes

If I was remembering it backwards, that would explain why they did it the way they did.

Count me in the "x was better" camp. I'm fairly neutral on "of" - I guess it seems weird to me to put an actual word into a type sugar. "x" just feels more appropriate, especially given its use in llvm.

I get that [5 x 5] looks similar to "five times five", but the pitch is for the type sugar, not the value sugar. There's no reason the value sugar needs to use the same syntax, especially if it will also be applicable for other types. (That said, I'm not really concerned about using "x" for both)

I do agree we need some kind of type sugar though, and if "of" has to be the compromise then so be it.

5 Likes

One thing that popped in my mind recently was how introducing a value literal syntax with of is one step towards introducing Python-like list comprehension. Now, that's quite a large leap and I'm in no way saying it's a slippery slope, but I can see proponents of comprehension pointing to the value literal syntax as a foundational justification for it.

It would be great to take into account the 'syntax friendliness' of new Swift features with respect to 'find-and-replace' operations and 'regular expressions'. Since regular expressions are commonly used not only in linters but also for 'find operations in IDEs', It would be nice that new language features remain easy to search and refactor. This consideration could be part of the evaluation criteria for future Swift language proposals.

2 Likes

Kind of, but this feature is an many ways the opposite of a python list comprehension in terms of use-cases and flexibility (let alone the fact that it’s defining a type here and not a value).

That is a good thing for InlineArray (its strict use cases are useful and legitimate, also to me directly, FWIW). But another potential downside to this sugar proposal.

Where is that choice documented?

It is too disjointed that Apple is pushing both what you're saying, and the opposite, simultaneously. I just learned about Fixed-Size Arrays | Tour of WGSL today, from the the Unlock GPU computing with WebGPU WWDC video.

There's only one other mention of WGSL on this forum:

Personally, I would just switch it back to what everyone else is doing, which looks better with shorthand and doesn't matter in longhand.

Reordering the generic parameters is discussed in the InlineArray proposal's alternatives considered.

4 Likes

Late to this topic, so sorry if this was already discussed, but I didn't see this variation in the alternatives considered section of the proposal.

I agree with the sentiment that sugar would be nice for multi-dimensionality, but any sort of "a _ b" seems to invite confusion to me. Reading some of the user comments, it seems natural for people to express "x many things" using parenthesis. Obviously that's a bit overloaded with function calls and math operations. But since we're already used to array syntax using brackets [], why not just do a literal numeric prefix?

The examples then become:

// Nesting - 
let fiveByFive: InlineArray<5, InlineArray<5, Int>> = .init(repeating: .init(repeating: 99))
let fiveByFive: 5[5[Int]] = .init(repeating: .init(repeating: 99))

// Inferring size from context
let fourBytes: _[Int8] = [1,2,3,4]  // InlineArray<4, Int8>

// Inferring type from context
let fiveIntegers: 5[] = .init(repeating: 99) // InlineArray<5, Int>
let fiveDoubles = 5[](repeating: 1.23)  // InlineArray<5, Double>

// Inferring size and type from context
let fourIntegers: _[] = [1,2,3,4]  // InlineArray<4, Int>

Since the size is intended to be specified at compile-time, it must be an integer literal. And I don't think there's currently any legal way of using types inside of brackets [] other than for declaring an array type anyway, so doesn't seem like there should be any confusion to the compiler or the human.

Complex Types

Once you start getting into more verbose types, some people may desire parenthesizing the rhs of of to more explicitly call out the grouping... Imagine a fixed-size array of closures each returning a fixed-size array...

let fiveClosures: [5 of ((Int, String)->[2 of MyClass<String>])]

In this case, the brackets already give the separation, obviating that need:

let fiveClosures: 5[(Int, String)->2[MyClass<String>] ]

Mixing fixed and dynamic arrays

In the cases of mixing fixed size and dynamic arrays...

let fixedArrayOfDynamicArray: 5[[Int]]
let dynamicArrayOfFixedArray: [5[Int]]

versus

let fixedArrayOfDynamicArray: [5 of [Int]]
let dynamicArrayOfFixedArray: [[5 of Int]]

There's perhaps a danger of being too concise, but I feel like these are equally understandable (or not understandable, depending on your perspective :grin: )...

Additional feature?

Size inferred from context

There's also perhaps a minor benefit in the case of inferred sizes...

The example above showed using an underscore, following the example given in SE-0483:

let fourBytes: _[Int8] = [1,2,3,4]
let fourIntegers: _[] = [1,2,3,4]

Using this notation, we could perhaps drop the underscore and default to InlineArray for let cases.

let fourIntegers: [] = [1,2,3,4]   // equivalent to InlineArray<4, Int>

But I think this is only advantageous if the "upgrade" cost of switching from InlineArray to Array at point of need is negligible, but I'll defer to someone more knowledgable in that area:

var dynamicIntegers: [Int] = fourIntegers   // Create Array<Int> and assign contents of InlineArray<4, Int>

Arguably this automatic behavior could still be done with the of syntax, but because there's a strong correlation between of and InlineArray, I feel like it would be a much bigger surprise.

Crazy idea?

By extension, even in var cases:, perhaps the compiler could detect based on usage whether any dynamic features were actually used by the array and automatically switch to there's some things that could be done to dynamically determine whether InlineArray or Array is desirable?

func assignmentStaysAsInlineArray() {
    let fourIntegers: [] = [1,2,3,4]   // equivalent to InlineArray<4, Int>
    var anotherFourIntegers: [] = fourIntegers  // equivalent to InlineArray<4, Int> because size was never mutated
    
    anotherFourIntegers[2] = 12
    return
}
func assignmentUpgradesToArray() {
    let fourIntegers: [] = [1,2,3,4]   // equivalent to InlineArray<4, Int>
    var anotherFourIntegers: [] = fourIntegers  // upgrades to Array<Int> with content assigned from InlineArray<4, Int> because size is mutated
    
    anotherFourIntegers.append(12)

    return
}

Sounds hard to do to me, and not sure if it's worth the effort. But food for thought...

1 Like

While I realize this could be said of any type inference, this scenario seems much easier for the compiler to reason about than for the programmer. I feel like it might be past the line.

4 Likes

Thank you to everyone who participated in this review discussion! The proposal has been accepted:

Holly Borla
Review Manager

8 Likes