[Pitch] InlineArray type sugar

Hi all. Below is a pitch to add sugar for the InlineArray type. The PR for the proposal can be found here, please direct typo/grammar feedback there. Let me know what you think!

InlineArray Literal Syntax

  • Proposal: SE-NNNN
  • Authors: Hamish Knight, Ben Cohen
  • Review Manager: TBD
  • Status: **Awaiting Review **
  • Experimental Feature Flag: InlineArrayTypeSugar

Introduction

We propose the introduction of type sugar for the InlineArray type, providing more succinct syntax for declaring an inline array.

Motivation

SE-0453 introduced a new type, InlineArray, which includes a size parameter as part of its type:

let fiveIntegers: InlineArray<5, Int> = [1,2,3,4,5]

Declaring this type is more cumbersome than its equivalent dynamicaly-sized array, which has sugar for the type syntax:

let fiveIntegers: [Int] = [1,2,3,4,5]

This becomes more pronounced when dealing with multiple dimensions:

let fiveByFive: InlineArray<5, InlineArray<5, Int>> = .init(repeating: .init(repeating: 99))

Proposed solution

A new sugared version of the InlineArray type is proposed:

let fiveIntegers: [5 x Int] = .init(repeating: 99)

Detailed design

The new syntax consists of the value for the integer generic paramter and the type of the element generic parameter, separated by x.

This will be added to the grammar alongside the current type sugar:

Grammar of a type
type → sized-array-type

Grammar of a sized array type
sized-array-type → [ expression|_ x type ]

Note that while the grammar allows for any expression, this is currently limited to only integer literals.

The new sugar is equivalent to declaring a type of InlineArray, so all rules that can be applied to the generic placeholders for the unsugared version also apply to the sugared version:


// Nesting
let fiveByFive: [5 x [5 x Int]] = .init(repeating: .init(repeating: 99))

// Inference from context:
let fiveIntegers: [5 x _] = .init(repeating: 99)
let fourBytes: [_ x Int8] = [1,2,3,4]
let fourIntegers: [_ x _] = [1,2,3,4]

// use on right-hand side
let fiveDoubles = [5 x _](repeating: 1.23)

The sugar can also be used in place of the unsugared type wherever it might appear:

[5 x Int](repeating: 99)
MemoryLayout<[5 x Int]>.size
unsafeBitCast((1,2,3), to: [3 x Int].self)

There must be whitespace on either side of the separator i.e. you cannot write [5x Int]. There are no requirements to balance whitespace, [5 x Int] is permitted. A new line can appear after the x but not before it, as while this is not ambiguous, this aids with the parser recovery logic, leading to better syntax error diagnostics.

Source Compatibility

Since it is not currently possible to write any form of the proposed syntax in Swift today, this proposal does not alter the meaning of any existing code.

Impact on ABI

This is purely compile-time sugar for the existing type. It is resolved at compile time, and does not appear in the ABI nor rely on any version of the runtime.

Future Directions

Analogous to arrays, there is an equivalent value sugar for literals of a specific size:

// type inferred to be [5 x Int]
let fiveInts = [5 x 99]
// type inferred to be [5 x [5 x Int]]
let fiveByFive = [5 x [5 x 99]]

Unlike the sugar for the type, this would also have applicability for existing types:

// equivalent to .init(repeating: 99, count: 5)
let dynamic: [Int] = [5 x 99]

This is a much bigger design space, potentially requiring a new expressible-by-literal protocol and a way to map the literal to an initializer. As such, it is left for a future proposal.

Alternatives Considered

The most obvious alternative here is the choice of separator. Other options include:

  • [5 * Int], using the standard ASCII symbol for multiplication.
  • [5 ⨉ Int], the Unicode n-ary times operator. This looks nice but is impactical as not keyboard-accessible.
  • [5; Int] is what Rust uses, but appears to have little association with "times" or "many". Similarly other arbitrary punctuation e.g. , or / or #.
  • : is of course ruled out as it is used for dictionary literals.

Note that * is an existing operator, and may lead to ambiguity in fuure when expressions can be used to determine the size: [5 * N * Int]. x is clearer in this case: [5 * N x Int]. It also avoids parsing ambiguity, as the grammar does not allow two identifiers in succession. But it would be less clear if x also appeared as an identifier: [5 * x x Int] (which is not yet permitted but may be in future use cases).

Another thing to consider is how that separator looks in the fully inferred version, which tend to start to look a little like ascii diagrams:

[_ x _]
[_ * _]
[_; _]

Beyond varying the separator, there may be other dramatically different syntax that moves further from the "like Array sugar, but with a size argument".

For multi-dimensional arrays, [5 x 5 x Int] was considered but introduces visual ambiguity without being a radical improvement.

The order of size first, then type is determined by the ordering of the unsugared type, and deviating from this for the sugared version is not an option.

In theory, when using integer literals or _ the whitespace could be omitted ([5x_] is unabiguously [5 x _]). However, special casing allowing whitespace omission is not desirable.

15 Likes

In previous threads where this syntax has come up, I have expressed some distaste for the use of x here. I feel fairly strongly that [5 * Int] would be better. I don't think [5 * N * Int] is actually ambiguous because the compiler knows if N is an integer literal or a type name, and since multiplication is commutative, there's no ambiguity caused by the ordering of the pseudo-operands.

I understand the parser might consider it ambiguous, but consider [5 x x x Int] where the middle-est x is an integer constant or integer generic. Oof, no?

16 Likes

Oof, indeed! I agree with @grynspan in preferring * on aesthetic grounds, but this is a particularly good argument with respect to a more objective readability issue.

In SE-0452, I believe the conclusion reached was that we recommend integer generic parameters to be named in lowercase, so the ambiguity in [5 * N * Int] would arise only if someone discarded that convention, whereas the ambiguity in [5 x x x Int] would arise if someone follows that convention.

(And, of course, we know that x is going to be used as a variable name :slight_smile:)

17 Likes

Would [5 Int] be ambiguous?

If necessary we could require parens for expressions with operators. For instance:

[5 Int]
[x Int]
[(5 * x) Int]
2 Likes

I can suggest two more alternatives to consider:

  • [5 • Int] - unicode bullet character (U+2022)- can be entered on Mac keyboard using ⌥8. I've used it as a dot product operator in raytracer code, and typing it was not an issue.
  • [5 of Int] - an explicit keyword.

A wish there would be comparable shortcut for a vector product.

6 Likes

I think the main thing that weighs against it is that it looks aesthetically unpleasant.

4 Likes

I am reminded of my Java days, where this type was spelled Int[5]. Or my QuickBASIC days (eep) where it was spelled… uh…

DIM myArray(5) AS INTEGER

(Let's not do that one.)

2 Likes

Kind of surprised that [5, Int] hasn't been "officially" considered:

let foo: [5, Int] = [1,2,3,4,5]

I think it reads just fine as not to be mixed up (too much) with dictionary sugar, but I think that's part of the danger with using sugar in the first place.

Or to be more visually distinct, to seem more "vector" like with 90-degree rotated vs:

let foo: <5, Int> = [1,2,3,4,5]

I lump in , with ; as not really expressing "times". However, , is also problematic because while it isn't ambiguous with existing syntax (you can't put a , in array square brackets today) it is very confusing that [x, y] as a value means an array of two elements, as a subscript means two arguments, and as a type would mean x elements of y. I think this pretty firmly puts it out of the running.

4 Likes

I'm not sure any syntax for this looks any better. I think this is one of those useful things that's just ugly (like Rust's turbofish).

[5 x Int](repeating: 99) is a particularly arresting formulation

True, but the comma is how it's expressed in the type signature, and times / multiplication might itself be misleading as a concept here.

Agreed. Did you consider including the type angle brackets?

let ra: [Int] = [1, 2, 3]
let ira: [<3, Int>] = [1, 2, 3]
let nest: [<5, [<3, Int>]>] = ...

Perhaps having exactly one way to say this awkward type parameter makes it clearer, particularly as the type gets more complex.

2 Likes

My motivation was that existing sugar pulls elements from a combination of non-sugar syntax, like @wes1 pointed out. As if using square brackets all around is a weak precedent or a Happy Accident. Dictionary is the better example:

let foo: Dictionary<Int, Int> = [1: 1, 2: 2]
// Int + Int + [ + : + ] =
let foo: [Int: Int] = ...

That might be because of this: [5, Int] reads 5 comma Int , where as [5 x Int] reads 5 times Int implying 5 Integers, with little cognitive burden.

2 Likes

I’m more opposed to x than the QBasic version because x is not an operator or ponctuation symbol.

It’s going to complicate tooling, it’s going to complicate manual parsing, I’m strongly against.

Because of my background I’d prefer the C syntax which is currently available in the parser, I believe without ambiguity, as it’s currently used to emit a diagnostic about transitioning the array literal syntax.

I’d in fact prefer no sugar to x, as InlineArray isn’t that long to type anyway.

6 Likes

I think this idea is growing on me. SE-0393: Value and Type Parameter Packs takes a similar route by using the keywords repeat and each when no good/obvious operators were available; that choice is elaborated on in the section "Syntax alternatives to repeat each". If the sugar for InlineArray is ever reused as sugar for homogenous parameter packs, then Variadic<10 of Int> or (10 of Int) would fit right at home with the existing keyword-based parameter pack syntax.

11 Likes

This is most ideal for me. As x looks like a variable, people seem to be against using a , which is my second choice. Using an operator seems out of place in Swift (to me).

2 Likes

This is too ambiguous. Remember that types can appear in expression context, and we have no way to distinguish this from an array literal where the first element is 5 and the second is a metatype.

5 Likes

From most to least preferred (though I don’t dislike any of these) I’d say:

  1. [_ * _] because of what @grynspan mentioned.
  2. [_ x _].
  3. [_ of _].

Though now I realize wouldn’t * block the future directions?

// this is [45]
let x = [5 * 9]

Edit: added a question.

I think * is a much better separator than x. Asterisks are the idiomatic way to express "times" in Swift. x just feels wrong and hacky.

I also find of acceptable.

Would it work if for the size, complex expressions (expressions beyond just an integer or identifier) were required to be put in parentheses?

E.g. [(5 * N) * Int]

1 Like

I admit that I am a big fan of using x.

Without my glasses on, I can see the *s less clearly in this:

[5 * [N * Int]]
[3 * [3 * [3 * Int]]]

than the xs in this:

[5 x [N x Int]]
[3 x [3 x [3 x Int]]]
1 Like