I propose we add a spread operator for lists and record literals. This is a rather large feature, but we can break this down into smaller features and implement them over time, depending on what's needed more.
Syntax
We can compare equivalent for the spread operator for various programming languages:
- JavaScript:
[1, 2, ...array]
{ a: 1, b: 2, ...object }
- Python
[1, 2, *lyst]
{ 'a': 1, 'b': 2, **dictionary }
(I think)
- Rust
MyStruct { ..MyStruct::default(), a: 1, b: 2 }
I believe Scheme uses .
for variable arguments, but that would not fit with N's syntax.
Behaviour
The spread operator in operator precedence could bind looser than prefix unary operators, so ..record.field
is allowed (syntax-wise, at least), as is ..cmd!
, , and ..function()
, but not ..3 ^ 2
.
List literals
For list literals, the spread operator can be used to splice a list amongst other items:
[1, 2, ..[3, 4, 5], 6, ..[], 7] = [1, 2, 3, 4, 5, 6, 7]
Obviously, the items in the inner list must match the type of the outer list.
Record literals
The spread operator also works for records.
let record1 = {
a: true
b: 2
}
{ ..record1; b: '2'; ..{ c: 3 } }
Notice that the b
field does not need to match the type of b
from record1
. Why? Record fields don't need to be the same type, and b
from record1
is guaranteed to be overwritten. Fields can overwrite each other; record fields listed earlier will be replaced by fields of the same name. The type checker can resolve the type of the above record as { a: bool; b: str; c: int }
.
This can be used for supplying the default values of a record. For example, { ..defaultOptions, someOption: true }
.
Record types
Similarly, the spread operator can be used for types.
alias record1 = {
a: bool
b: int
}
alias record2 = { ..record1, b: str, ..{ c: int } }
..{ c: int }
is pretty redundant, though, so we might not allow that.
This could also be used in combination with generic types ๐ฑ:
let myFunction = [[a] record: { ..a, requiredField: int }] -> ...
This can be used kind of like a psuedo-interface, where the record (whether it be a class instance, module, or plain record) must contain at least the specified fields and their types.
List destructuring
In if let
and match
(see #78), the spread operator can also be used in patterns to destructure the rest of a list:
if let [requiredFirstItem, requiredSecondItem, ..otherItems] = list {
The above conditional pattern requires there to be at least two items, and the rest of the items will be bound in a new list.
We might also allow using the spread operator for the beginning or even middle of the list:
if let [..items, a, b] = list { ... }
if let [a, b, ..items, c, d] = list { ... }
The type of items
will be the same as whatever list
is.
Record destructuring
Quite like a list, the spread operator can take the remaining fields of a record. Since record fields are guaranteed to exist, they can be used in normal let
and other places where variables are bound to a name, like function arguments:
let { field1, field2: newName, ..otherFields } = record
The type of otherFields
will be the same as record
but without field1
and field2
. This can also be used in tandem with generic types.
Unlike the record literal, I don't think the order or placement of the spread operator matters.
For both record and list destructuring, the rest of the items may not be of much use, so they can be discarded with .._
.
let { function1, variable1, .._ } = imp "./module.n"
Tuples
We might also allow the spread operator for tuples, both in their literals, types, and patterns.
The spread operator is forbidden elsewhere. This can be enforced either by the type checker or the syntax.