[Pitch] InlineArray type sugar

Remember about types in expression context. Is [5-Int].self an InlineArray metatype, or an array literal with one element which is the result of calling the - operator on an integer and metatype? (You can define such an overload for yourself if you want.)

That’s certainly an esoteric case, but ambiguity generally makes error recovery and diagnostics more difficult.

3 Likes

Personally I like x the most, aesthetically, because it's immediately clear what a "[5 x Int]" is, in a way that isn't matched by any of the alternatives. Perhaps because of the resemblance with the "⨉" operator in matrix notation, I think I subconsciously read [5 x Int] as a 5 ⨉ 1 vector of Int-typed elements (which is exactly what it is), or 5 "dimensions" of Int.

However, I find it rather confusing when combined with the Future Direction of extending it to literal initialization, because I also inevitably see [5 x 99] as "a matrix of 5 rows and 99 columns", not as "a row of 5 values all being 99". I don't really see a way to use the same symbol to convey both dimensionality and repeated values, but x would be particularly confusing in that dual role.

IMO the type sugar is enough here:

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

And adding the extra sugar to elid repeating can only cause confusion. Also, I suspect initializing to zeros (or some other default value) is going to be by far the most popular thing to do, and I wonder if that case wouldn't be better served by a static extension:

let fiveZeros: [5 x Int] = .zero

Rather than needing to come up with a new syntax sugar for arbitrary repeated values that can be used everywhere.


Without considering that future direction, I like x (as proposed) the most, then or ; (* doesn't really suggest dimensionality to me, even though I see why some people prefer it over x).

However, if we absolutely must use the same symbol for both "meanings", then I'd suggest explicitly trying to drop that association with "times" or "many" mentioned in the Alternatives Considered as a desirable trait:

  • [5; Int] is what Rust uses, but appears to have little association with "times" or "many". Similarly other arbitrary punctuation e.g. , or / or #.

For which either , ; or of would be probably fine.

3 Likes

- can be an operator, but
can't, and neither can
.

I think is the best option next to -.
(Because it's only option-- instead of shift-option--, even though they render the same in monospace.)

[5-Int]
[5–Int]
[5—Int]

Close enough.

Non-ASCII syntax is one of those things that sounds good in theory and has been attempted over and over again, and it never works out very well (with the possible exception of APL, but that’s a special case because it essentially predates ASCII).

11 Likes

I agree. AFAIK we haven't found ourselves needing shorthand for arrays of repeated values either.

2 Likes

If this ever came up, I think it could be solved with an expression macro.

The other comparable notation comes from regex, where a count follows a pattern, using the same angle brackets.

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

This converts InlineArray<N, T> into [T]<N>, which seems like a sensible rule, particularly since types have pattern aspects.

I don't think we'd need macros for it. An expression macro would be exactly one character longer than a corresponding inlinable function that does exactly the same thing.

I like "of", i.e. [3 of Int]. It mirrors "in" in the statement "for x in array"

6 Likes

I guess it depends on if we want to guarantee that loops over known inline array counts are always unrolled in all cases, eg if you have a series of nested literals and calls to init(repeating:) with a more complex element type. Otherwise, your literal might require runtime initialization. (Ideally we should be able to fold this away in all cases, but parameter packs currently have this problem for example.) A developer might still prefer to roll a macro for this purpose to avoid depending on optimizer behavior.

2 Likes

I was picturing we'd use @const, actually.

2 Likes

It wouldn't be so bad for the common stuff like • if every OS supported easy modifiers for it like MacOS, but they don't. These symbols are a pain to type on anything not made by Apple, and unless a lot of work is put in to fix that, I don't see it being viable to have non-ASCII operators in the standard library anytime soon.

2 Likes

Count me in with both "hating the letter x here" and "why not just follow Rust" — [Int; 5] isn't spectacularly intuitive, but there's precedent. I also agree that if we're going to diverge from Rust, [Int * 5] or [5 * Int] or Int[5] are more intuitive spellings.

5 Likes

I like Int[5]. Possibly even allowing Int[] for [Int]

Int[_] could be used to represent an inline array with "derived" count:

let x: Int[_] = [1, 2, 3] // an inline array of 3 elements.

[Int x 5] or [5 x Int] probably not - we don't do this for multiplication so it would be inconsistent. [5 Int] could work if unambiguous.

4 Likes

From my point of view keyword-based approach would be better than using x or especially *.

2 Likes

Apologies if this has been suggested before. Syntactic sugar may not be warranted for this case, but if new syntax is desired, why not make this look more like other C-like languages and place the dimension in array brackets:

let fiveIntegers: InlineArray<Int>[5]
let fiveIntegers: InlineArray<Int> = [1,2,3,4,5]
let threeByThree: InlineArray<Int> = [[1,2,3], [4,5,6], [7,8,9]] 
let fiveByFive: InlineArray<Int>[5][5] = .init(repeating: .init(repeating: 99))

Dimension-like generic parameters could be defined with parameters of type "Dimension":

struct InlineArray<Element, D: Dimension>
 
InlineArray<Int>[5][5]		// converted to: InlineArray<InlineArray<Int, 5>, 5>

Multi-dimensioned generics could be handled similarly:

struct Matrix<Element, Y: Dimension, X: Dimension>
Matrix<Int>[4][4]   // converted to:  Matrix<Int, 4, 4>

Explicit "Dimension" generic parameters might bring other benefits as well. The added information could be used to support operators, for example.

Again, this may not be worth the added complexity and effort -- it may not "pull its weight".

To anyone suggesting a syntax such as Int[4], please be aware that this syntax already has the meaning of calling a static subscript. For instance:

extension Int {
	static subscript (count: Int) -> Array<Int> {
		Array(repeating: 0, count: count)
	}
}

let x = Int[4] // [0,0,0,0] per extension above

I suppose we could make it have a different meaning depending on if we're in a position where we are expecting a type or a value, but that'd still make things confusing.

12 Likes

C follows "declaration reflects use". A declaration is a primitive type followed by an expression of that type; for example, int *x means "*x is an int", and int (*x)[10] means "(*x)[10] is an int". "Declaration reflects use" has the problem that types do not nest normally, resulting in declarations that are hard to read. For example, it's difficult to realize that int (*x)[10] means that x is a pointer to an array of ints, instead of an array of int pointers. The same is true for arrays. int x[5] means that x is a 5-array of integers, so we could be led to believe that int x[5][10] is a 10-array of 5-arrays of integers, but it's actually a 5-array of 10-arrays of integers.[1]

Swift type sugaring follows "declaration reflects construction". Dictionaries are constructed with a [key: value] literal, so dictionary types are written [Key: Value]. Arrays are constructed with [element, element], so array types are written [Element]. Tuples are constructed with (a, b), so tuple types are written (A, B).[2]

Rust also follows "declaration reflects construction". Rust arrays (that repeat an element) are constructed with [value; N], so array types are written as [T; N]. References are constructed with &value and &mut value, so reference types are written as &T and &mut T. And so on.


  1. What I mean by "not nest normally" is that the normal rules of substitution do not apply. If we have typedef int T[10], then the type T[5] would be int[5][10], not int[10][5] like we would expect from normal rules of substitution. ↩︎

  2. The only exception is optional types, because optional values are not constructed with value?, but they are written as such in pattern matching ↩︎

8 Likes

That's a good observation. Although type couldn't be used there, e.g. this is syntax error:

let x = Int

to reference type you'd need to put let x = Int.self, so could be the case with let x = Int[4].self which would differ from the static subscript.

1 Like

Don't all types in Swift have an implicit member self? I remember playing around and realizing that self.self is, in at least some cases, legal (and equivalent to just self).