We both posted at the same time, I am curious to know what you think about what I said above.
x
is not an operator in this context; it is just a redundant delimiter.
[3 x T]
:=
3 values of type T
.
It can be elided, without any loss of meaning.
[3 T]
:=
3 values of type T
.
If you consider other use cases of syntactic repetition (repeated types and values in variadic parameter lists, tuples, ...), then this keyword is essential, without which a more coherent syntactic repetition feature cannot exist and the syntax sugar will be locked at the level of InlineArray
alone.
Thank you. How do you feel about the other possible (proposed in this forum) contexts that may or could evolve for example in the following
As you suggest we would have instead of
var x: [5 of Int] = [6, 3 4, 2]
func build() -> [some of some Type]
var x: [5 of Int] = [6, 3 of 4, 2]
have
var x: [5 Int] = [6, 3 4, 2]
func build() -> [some some Type]
var x: [5 of Int] = [6, 3 4, 2]
Do you feel in those contexts eliding the keyword, operator or delimiter is appropriate? Granted that they are only environments proposed in this thread, but not necessarily a part of the proposal.
I appreciate this review of the aspects, summarized:
(Interesting: when you said "length" I took it to mean the length of the inline array, not the sugar.)
I think array + inline + count defines the aspects of the family being named and distinguished from related types (the referent):
- Tuples are not arrays (not integer-indexable or same-type)
- stdlib array count/capacity is self-adjusting (with all the memory consequences thereof)
As far as I can see, the consensus on sugaring for inline array is mostly settled except for the inline aspect
Aspect | Solution | Rational |
---|---|---|
array | [...] : surround in brackets |
Broad convention |
count | [n ...] put numeral or parameter first |
literal stands on its own |
inline | x |
Implies dimensionality |
inline | of |
not x |
inline | inline |
Indicates underlying store semantics |
Looking at it this way, inline
also disambiguates the other parts of the sugared spelling, (1) distinguishing these arrays from reference/resizing arrays and (2) providing a context identifying the number as an SE-452 integer generic parameter.
I think of brevity (of the reference) not as the number of characters, but the effort to read and especially to disambiguate from other possible things.
Here I think the sugared form is the most readable and thinkable. InlineArray<n, T>
makes a very awkward sentence "an inline array of n elements of type T", which I think of as "an inline array of T"
But I read [n inline T]
as "an array of n inline T", which is how I think about it. Indeed, for now in discussions I would probably just say "inline Int" because the actual count is variable. The array is implied at least by the fact that InlineArray
is the only way we have inlined values now, but more by the storage being its key feature.
Put another way: the array-ness of it has two parts: (1) same type (unlike tuples), which is key to what it can represent or encode; and (2) access via integer indexing, which is mostly a convenience for developers but also an optimization for looping algorithms. Both make homogeneity a key part of array, which makes it useful for looping.
Inline array adds to this by saying all values are initialized and the count is fixed, unlocking loop and access optimizations. (While tuples and structs may be stored inline, there are few optimizations that can result, and array optimizations are only now being safely unlocked with span.) That makes inline
the key meaning.
Does inline
compose?
let cubeCorners: [8 x 3 x Float]
let cubeCorners: [8 of 3 of Float]
let cubeCorners: [8 inline 3 inline Float]
let cubeCorners: [8 inline (3 inline Float)]
As with the others, the leading number means you could disambiguate back-to-front (as in C one reads types inside-out), or make it more obvious with parentheses.
Unlike the others, inline
tells you what comes before and after.
Checking generics and typealias:
func bound<let n: Int, let dim: Int>() {
let polygonCorners: [n inline dim inline Float]
}
typealias RegularPolygonCorners<let n: Int> = [n inline 3 inline Float]
typealias CubeCorners<let dimension: Int> = [8 inline dimension inline Float]
typealias Matrix<let m: Int, let n: Int, T> = [m inline n inline T]
func dotProduct<let a: Int, let n: Int, let b: Int, I: BinaryInteger>(
lhs: Matrix<a, n, I>,
rhs: Matrix<n, b, I>
) -> Matrix<a, b, I> {...}
func dotProductInline<let a: Int, let n: Int, let b: Int, I: BinaryInteger>(
lhs: [a inline n inline I],
rhs: [n inline b inline I]
) -> [a inline b inline I] {...}
func dotProductInline<let a: Int, let n: Int, let b: Int, I: BinaryInteger>(
lhs: [a x n x I],
rhs: [n x b x I]
) -> [a x b x I] {...}
func dotProductInlineArray<let a: Int, let n: Int, let b: Int, I: BinaryInteger>(
lhs: InlineArray<a, InlineArray<n, I>>,
rhs: InlineArray<n, InlineArray<b, I>>
) -> InlineArray<a, InlineArray<b, I>> {...}
If/since this is the key issue, then the inline
infix helps the most out of the proposed sugars.
I have a lot of issues with inline
, although, I did like it at one point. Using my following post as a rubric for my rational,
I want to put forth the following:
Analysis of n inline T
• var a: [10 inline Int]
and a = [10 inline 10]
• var a: (5 inline Int)
for var a: (Int, Int, Int, Int, Int)
• a = (5 inline 2)
for a = (2,2,2,2,2)
• var x: [5 inline Int] = [6, 3 inline 4, 2]
for [6, 3, 3, 3, 3, 2]
• func build() -> [some inline some Type]
This list of qualities was compiled by looking at a lot of the scenarios and directions that people have hoped to see this feature expanded into. I think that looking at how inline
or x
operates in all of these scenarios is paramount. A guiding principle that I am trying to stick to is that whatever the keyword, operator or symbol is it needs to be useful elsewhere not just in the declaration site but also the call site… and take into consideration possible future directions.
In these scenarios, inline
appears to not work well is when constructing repeating values.
Additionally, inline
suffers for nearing what I might consider too verbose
for being better than using InlineArray<count, T>
which is part (not all) of the problem that the sugar is trying to solve. I do understand it is not quite as long.
Again, when inline
was first introduced, I did like it.
I believe that the keyword, operator or delimiter should convey "N units of type T" or "N units of value N" to the reader.
Thanks for clarification, I took this from Swift API design guidelines. Apparently this phrase is correct only when "clarity" and "brevity" are in conflict.
I think it would really help if the authors can mention this precedent use of x
in LLVM in their proposal.
I agree for values as opposed to types, and I would not use the same sugar for values.
By call site you mean the literal used as an initializer? I think it's confusing to conflate the two or to make them type sugar and value initializers look similar, particularly if/since this requires a numeral as a type parameter.
For values, if I had to have a literal, I would use repeating
, which also puts the n
count in the same leading orientation as the type. That's used and meaningful in initializers already (and indeed could be used in endless sequences), so the notion of value-repeating should not be confused with inline type layout.
Given the type sugar, is a T(repeating:)
initializer enough?
let cubeCorners: [8 inline 3 of Float] = [8 repeating 3 repeating 1.0]
let cubeCorners: [8 inline 3 of Float] = [8 inline](repeating: [3 inline](repeating: 1.0)]
(But I have trouble understanding the benefit of repeated values for InlineArray.)
I see. So in the pitch under "Future Direction" it mentions:
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]]
In your view, then, having the extra benefit of bing able to use the same keyword to construct repeating values is not beneficial. Understood.
I believe for most use cases it would just be to initialize the InlineArray, as I believe it needs to be populated at construction. I have a feeling that most instances of its use would be var a: [1000 of Int] = [1000 of 0]
Yes, call site as in the construction site (when the initializer is called). Is there a strong reason that their looking the same would be against the norms in Swift as it is today? I think it would be nice to have both the declaration and constructor match.
Lastly, xwu mentions:
which gives us the possibility of func build() -> [some inline some Type]
. Do you think in this possible future, inline
fits?
No one wants to read post #272 in this long, long thread. But I've reviewed the discussion pretty carefully, and I'd like to make an argument that I don't think has been raised yet.
Namely: this proposal really has nothing to do with InlineArray
per se. It is in fact an attempt to grapple with the consequences of SE-0452: Integer Generic Parameters.
Scalars can now be part of a type's definition. In an ideal world, Swift would address that fact robustly as a matter of syntax, typing, and type inference.
Let's leave sugaring aside for the moment. It seems to me rather telling that
let unitVector: InlineArray = .init(repeating: 1.0, count: 3)
not only isn't currently accepted, but really cannot possibly be within the current type inference system. Shouldn't that be addressed first and foremost?
But that's hard! There's a PhD thesis in there somewhere! I wouldn't want that dumped on my plate either.
I'd just like us to be clear that we're punting on this. We can follow up on SE-0452 and all its implications, or we can declare it to be a special-case, "pay no attention to that man behind the curtain" aberration. My preference, as a person who pays none of the cost, is that this would be done right. But heaven is high and the emperor is far away.
(That said: my knowledge of compiler internals is rudimentary at best. If I'm wrong about this, I'll be both embarrassed and happy!)
There's nothing special about InlineArray
. Nothing! I want my own Matrix
and Vector
APIs to benefit from every last pinch of syntactic sugar that becomes available to InlineArray
.
I find it stange that the proposal does not reference prior art in LLVM IR, which seems to me like the reason this is proposed in this current form in the first place, non?
With InlineArray
mostly just supporting swap
, I couldn't think of a use for one initialized with the same values (and I don't see retrofitting Array literals using x
or of
).
Hmm, I do see that when the literal stands alone, repeating
loses the inline implication (in4
below).
Perhaps repeating
should be used as a prefix keyword in the storage position only, based on pulling the count from the type parameter. It's more wordy, but makes the locution usable in related literals.
Here are variants of sugared types and repeating
as parameter or value literal (including future ranges with statically-determined counts).
// today
var to1 = InlineArray<100, Int> = .init(repeating: 0)
var to2 = InlineArray<3, Int> = [0, 1, 2]
var to2: InlineArray = [0, 1, 2]
// x
var x1: [100 x Int] = .init(repeating: 0)
var x2 = [100 x Int](repeating: 0)
var x3 = [100 x 0]
// inline with parameter
var in1 [100 inline Int] = .init(repeating: 0)
var in2 = [100 inline Int](repeating: 0)
// Does inline alone for literal imply repeating? Maybe?
var in3 = [100 inline 0]
// Using repeating alone for literals is not clearly inline.
// This below should instead be sugar for Array values, no?
var in4 = [100 repeating 0]
// Use `inline` to change default from Array to InlineArray
// with N and storage parameters inferred from literal series
var in6 = [inline 0, 1, 2] // InlineArray<3, Int>
// Then focus on literal value replacing type...
// (1) Using repeating
// Wordy here but perhaps this makes `repeating` broady usable
var in5 = [100 inline repeating 0]
// (2) With (concrete) Range<T>?
var in7 = [inline 0..<10] // with concrete range for literal
var in8 = [8 inline 0..<9] // error in count: report as such
// (3) Plus more inference combinations...
enum CardFace {
static let asInline: [13 inline Self] {...}
}
// Deck of cards with 4 series of 13 faces
// n from literal
// T from declaration or literal
// values from static property of T with known N parameter
var in9 = [4 inline CardFace.asInline]
var in9aka: [inline CardFace] = [4 inline .asInline]
btw, I earlier failed to highlight an assumption that the storage type could be elided if inferred from the parameter:
var inp1 = [100 inline Int](repeating: 0) // explicit
var inp2 = [100 inline](repeating: 0) // inferred
I also assumed that in the nested case somehow n inline m inline T
is parseable as the storage type, but the original proposal has proper nesting 2 x [4 x 10]
.
var inp3: [8 inline [3 inline Float]]
In the value context
var cubeCorners = [8 inline [3 inline repeat 0.0]]
Note that if there's some other direct indication of Inline, even x makes pretty good sense:
let ira : InlineArray = [3 x 0.0]
But then what indications would be enough?
// nesting?
let iira : InlineArray = [8 x [3 x 0.0]]
// aliasing?
typealias Coord<n> = ArrayInline<n, Float>
// later...
let cra = [8 x Coord<3>(repeating: 0.0)]
x
and of
can be used in more contexts because they mean more things, but that also means they require both context and training to understand. The code could be made clear as needed.
inline
and inline repeating
I believe are clear and easy to read even without context or training, but they are not as brief for the initiated.
But even the initiated might still prefer to distinguish inline from repeating, to better support orthogonal evolution (using an implied count, more inline types, and more places/ways to spell literal repeats or series).
(Sorry for the shifts and possible duplication.)
Responding here as well as some detail isn't relevant in the other thread, but LLVM IR as prior art comes up a few times if you search the thread for it. Here is one mention.
I don't think LLVM IR is a great example for what to use here though, as the two languages are structured so differently and come at things from totally different directions. In particular, LLVM IR spells 'multiply' as mul
, and 'variables' are named differently, so the overloading concerns of x
don't really apply.
Edit to add: Ah, you said proposal, not thread, my bad. My second point still stands though.
I agree. The fact that Swift uses LLVM under the hood is for me as a user of the Swift language just an implementation detail. It is off course prior art. But it is not more valuable prior art than for instance how Rust, C/C++ or any common language does.
My point was simply that this preexisting syntax seems to be familiar to Swift's compiler engineers, and perhaps why the syntax was proposed in the first place? If so, I think it perhaps should have been pointed out in the proposal text. Whether it should we be given much weight, I think is up to reviewers.
Personally, I didn't like it — and still don't — but at least I'm more sympathetic to it now.
¯\_(ツ)_/¯
Hi Svein,
Wasn't trying to imply this was your point. I saw the point made earlier: that the LLVM case would be a (more) compelling argument in favour of the x
style.
My apologies for the confusion
KR Maarten
I made a post about "prior art" in another thread that is relevant to this discussion, please take a look:
Cognitive load is complex to measure. People are complex. And software is complex. I would be skeptical of any blanket claims like this that aren't supported by evidence.
As somebody who's worked in HCI for a while now, albeit not in programming languages or software engineering, I could see a good argument for why [N inline T]
might be less cognitively demanding for many folks. Whether that difference is "significant" or not is a good question.
I'm a little worried at the number of folks pointing to LLVM as prior art. A big part of the appeal of Swift is that it's not LLVM. For example, from Swift.org:
Swift is a general-purpose programming language that’s approachable for newcomers and powerful for experts. It is fast, modern, safe, and a joy to write.
Swift has fundamentally diffferent goals than LLVM.
As someone who is somewhat familiar with LLVM IR and is also comfortable with the [n x T]
syntax proposed here, I also personally happen to agree that it doesn't make for good prior art.
LLVM IR isn't something I would expect most Swift developers to ever read, let alone write. Its human-readable syntax is optimized for completely different use cases than a high-level language and it mostly comes up in testing and debugging.
So while I'm comfortable with [n x T]
in a vacuum (but am open to other options as well), the fact that LLVM happens to use a similar syntax for this is wholly irrelevant for me. The syntax ought to be evaluated on its own without that being a factor.