map literals
This commit is contained in:
1 parent
cb9df79ce3
commit
d118d29429
16 files changed
+669
-78
No files matched your search
+73
-8
@@ -1,19 +1,22 @@
|
||||
# Map
|
||||
|
||||
Map is a mutable collection of key-value pars, where keys are unique. Maps could be created with
|
||||
constructor or `.toMap` methods. When constructing from a list, each list item must be a [Collection] with exactly 2 elements, for example, a [List].
|
||||
Map is a mutable collection of key-value pairs, where keys are unique. You can create maps in two ways:
|
||||
- with the constructor `Map(...)` or `.toMap()` helpers; and
|
||||
- with map literals using braces: `{ "key": value, id: expr, id: }`.
|
||||
|
||||
When constructing from a list, each list item must be a [Collection] with exactly 2 elements, for example, a [List].
|
||||
|
||||
Important thing is that maps can't contain `null`: it is used to return from missing elements.
|
||||
|
||||
Constructed map instance is of class `Map` and implements `Collection` (and therefore `Iterable`)
|
||||
Constructed map instance is of class `Map` and implements `Collection` (and therefore `Iterable`).
|
||||
|
||||
val map = Map( "foo" => 1, "bar" => "buzz" )
|
||||
assert(map is Map)
|
||||
assert(map.size == 2)
|
||||
assert(map is Iterable)
|
||||
val oldForm = Map( "foo" => 1, "bar" => "buzz" )
|
||||
assert(oldForm is Map)
|
||||
assert(oldForm.size == 2)
|
||||
assert(oldForm is Iterable)
|
||||
>>> void
|
||||
|
||||
Notice usage of the `=>` operator that creates `MapEntry`, which implements also [Collection] of
|
||||
Notice usage of the `=>` operator that creates `MapEntry`, which also implements [Collection] of
|
||||
two items, first, at index zero, is a key, second, at index 1, is the value. You can use lists too.
|
||||
Map keys could be any objects (hashable, e.g. with reasonable hashCode, most of standard types are). You can access elements with indexing operator:
|
||||
|
||||
@@ -29,6 +32,36 @@ Map keys could be any objects (hashable, e.g. with reasonable hashCode, most of
|
||||
assert( map["foo"] == -1)
|
||||
>>> void
|
||||
|
||||
## Map literals { ... }
|
||||
|
||||
Lyng supports JavaScript-like map literals. Keys can be string literals or identifiers, and there is a handy identifier shorthand:
|
||||
|
||||
- String key: `{ "a": 1 }`
|
||||
- Identifier key: `{ foo: 2 }` is the same as `{ "foo": 2 }`
|
||||
- Identifier shorthand: `{ foo: }` is the same as `{ "foo": foo }`
|
||||
|
||||
Access uses brackets: `m["a"]`.
|
||||
|
||||
val x = 10
|
||||
val y = 10
|
||||
val m = { "a": 1, x: x * 2, y: }
|
||||
assertEquals(1, m["a"]) // string-literal key
|
||||
assertEquals(20, m["x"]) // identifier key
|
||||
assertEquals(10, m["y"]) // identifier shorthand expands to y: y
|
||||
>>> void
|
||||
|
||||
Trailing commas are allowed for nicer diffs and multiline formatting:
|
||||
|
||||
val m = {
|
||||
"a": 1,
|
||||
b: 2,
|
||||
}
|
||||
assertEquals(1, m["a"])
|
||||
assertEquals(2, m["b"])
|
||||
>>> void
|
||||
|
||||
Empty `{}` is reserved for blocks/lambdas; use `Map()` for an empty map.
|
||||
|
||||
To remove item from the collection. use `remove`. It returns last removed item or null. Be careful if you
|
||||
hold nulls in the map - this is not a recommended practice when using `remove` returned value. `clear()`
|
||||
removes all.
|
||||
@@ -110,4 +143,36 @@ is equal.
|
||||
assert( m1 != m3 )
|
||||
>>> void
|
||||
|
||||
## Spreads and merging
|
||||
|
||||
Inside map literals you can spread another map with `...` and items will be merged left-to-right; rightmost wins:
|
||||
|
||||
val base = { a: 1, b: 2 }
|
||||
val m = { a: 0, ...base, b: 3, c: 4 }
|
||||
assertEquals(1, m["a"]) // base overwrites a:0
|
||||
assertEquals(3, m["b"]) // literal overwrites spread
|
||||
assertEquals(4, m["c"]) // new key
|
||||
>>> void
|
||||
|
||||
Maps and entries can also be merged with `+` and `+=`:
|
||||
|
||||
val m1 = ("x" => 1) + ("y" => 2)
|
||||
assertEquals(1, m1["x"])
|
||||
assertEquals(2, m1["y"])
|
||||
|
||||
val m2 = { "a": 10 } + ("b" => 20)
|
||||
assertEquals(10, m2["a"])
|
||||
assertEquals(20, m2["b"])
|
||||
|
||||
var m3 = { a: 1 }
|
||||
m3 += ("b" => 2)
|
||||
assertEquals(1, m3["a"])
|
||||
assertEquals(2, m3["b"])
|
||||
>>> void
|
||||
|
||||
Notes:
|
||||
- Map literals always use string keys (identifier keys are converted to strings).
|
||||
- Spreads inside map literals and `+`/`+=` merges require string keys on the right-hand side; this aligns with named-argument splats.
|
||||
- When you need computed or non-string keys, use the constructor form `Map(...)` or build entries with `=>` and then merge.
|
||||
|
||||
[Collection](Collection.md)
|
||||
@@ -126,6 +126,14 @@ Splat (`...`) of a Map provides named arguments to the call. Only string keys ar
|
||||
assertEquals(["A?","B!","c","D!"], r)
|
||||
```
|
||||
|
||||
The same with a map literal is often more concise. Define the literal, then splat the variable:
|
||||
|
||||
fun test(a="a", b="b", c="c", d="d") { [a, b, c, d] }
|
||||
val patch = { d: "D!", b: "B!" }
|
||||
val r = test("A?", ...patch)
|
||||
assertEquals(["A?","B!","c","D!"], r)
|
||||
>>> void
|
||||
|
||||
Constraints:
|
||||
|
||||
- Map splat keys must be strings; otherwise, a clean error is thrown.
|
||||
|
||||
@@ -1,82 +1,93 @@
|
||||
Note for AI. please ignore this file until this remark is removed.
|
||||
|
||||
# Map literals proposal
|
||||
# Map literals — refined proposal
|
||||
|
||||
Note to the AI: please ignore this file until this note is removed.
|
||||
Implement JavaScript-like literals for maps. The syntax and semantics align with named arguments in function calls, but map literals are expressions that construct `Map` values.
|
||||
|
||||
Implement javascript-like literals for maps. It uses the similar syntax already adopted for named arguments in function calls so it should be organic and familiar, as the purpose is similar.
|
||||
Keys can be either:
|
||||
- string literals: `{ "some key": value }`, or
|
||||
- identifiers: `{ name: expr }`, where the key becomes the string `"name"`.
|
||||
|
||||
String literal keys
|
||||
Identifier shorthand inside map literals is supported:
|
||||
- `{ name: }` desugars to `{ "name": name }`.
|
||||
|
||||
Property access sugar is not provided for maps: use bracket access only, e.g. `m["a"]`, not `m.a`.
|
||||
|
||||
Examples:
|
||||
|
||||
```lyng
|
||||
val m = { a: "foo", b: "bar" }
|
||||
assertEqual(m.a, "foo")
|
||||
assertEqual(m.b, "bar")
|
||||
val x = 2
|
||||
val m = { "a": 1, x: x*10, y: }
|
||||
assertEquals(1, m["a"]) // string-literal key
|
||||
assertEquals(20, m["x"]) // identifier key
|
||||
assertEquals(2, m["y"]) // identifier shorthand
|
||||
```
|
||||
|
||||
The map literal is a map with keys that are string literals only. This is important. In a relatively rare case when keys are calculated, or extravagant but still possible case when keys are of different types, literal could be combined with "=>":
|
||||
Spreads (splats) in map literals are allowed and merged left-to-right with “rightmost wins” semantics:
|
||||
|
||||
```lyng
|
||||
val k1 = "bar"
|
||||
val m = { "foo": 123 } + k1 => "buzz"
|
||||
// this is same as Map("foo" => 123) + Map("bar" => k2) but can be optimized by compiler
|
||||
assertEqual(m["foo"], 123)
|
||||
assertEqual(m["bar"], "buzz")
|
||||
val base = { a: 1, b: 2 }
|
||||
val m = { a: 0, ...base, b: 3, c: 4 }
|
||||
assertEquals(1, m["a"]) // base overwrites a:0
|
||||
assertEquals(3, m["b"]) // literal overwrites spread
|
||||
assertEquals(4, m["c"]) // new key
|
||||
```
|
||||
|
||||
The lambda syntax is different, it can start with the `map_lteral_start` above, it should produce compile time error, so we can add map literals of this sort.
|
||||
Trailing commas are allowed (optional):
|
||||
|
||||
Also, we will allow splats in map literals:
|
||||
|
||||
```
|
||||
val m = { foo: "bar", ...{bar: "buzz"} }
|
||||
assertEquals("bar",m["foo"])
|
||||
assertEquals("buzz", m["bar"])
|
||||
```lyng
|
||||
val m = {
|
||||
"a": 1,
|
||||
b: 2,
|
||||
...other,
|
||||
}
|
||||
```
|
||||
|
||||
When the literal argument and splats are used together, they must be evaluated left-to-right with allowed overwriting
|
||||
between named elements and splats, allowing any combination and multiple splats:
|
||||
Duplicate keys among literal entries (including identifier shorthand) are a compile-time error:
|
||||
|
||||
```
|
||||
val m = { foo: "bar", ...{bar: "buzz"}, ...{foo: "foobar"}, bar: "bar" }
|
||||
assertEquals("foobar",m["foo"])
|
||||
assertEquals("bar", m["bar"])
|
||||
```lyng
|
||||
{ foo: 1, "foo": 2 } // error: duplicate key "foo"
|
||||
{ foo:, foo: 2 } // error: duplicate key "foo"
|
||||
```
|
||||
|
||||
Still we disallow duplicating _string literals_:
|
||||
Spreads are evaluated at runtime. Overlaps from spreads are resolved by last write wins. If a spread is not a map, or yields a map with non-string keys, it’s a runtime error.
|
||||
|
||||
```
|
||||
// this is an compile-time exception:
|
||||
{ foo: 1, bar: 2, foo: 3 }
|
||||
Merging with `+`/`+=` and entries:
|
||||
|
||||
```lyng
|
||||
("1" => 10) + ("2" => 20) // Map("1"=>10, "2"=>20)
|
||||
{ "1": 10 } + ("2" => 20) // same
|
||||
{ "1": 10 } + { "2": 20 } // same
|
||||
|
||||
var m = { "a": 1 }
|
||||
m += ("b" => 2) // m = { "a":1, "b":2 }
|
||||
```
|
||||
|
||||
Special syntax allows to insert key-value pair from the variable which name should be the key, and content is value:
|
||||
Rightmost wins on duplicates consistently across spreads and merges. All map merging operations require string keys; encountering a non-string key during merge is a runtime error.
|
||||
|
||||
Empty map literal `{}` is not supported to avoid ambiguity with blocks/lambdas. Use `Map()` for an empty map.
|
||||
|
||||
Lambda disambiguation
|
||||
- A `{ ... }` with typed lambda parameters must have a top-level `->` in its header. The compiler disambiguates by looking for a top-level `->`. If none is found, it attempts to parse a map literal; if that fails, it is parsed as a lambda or block.
|
||||
|
||||
Grammar (EBNF)
|
||||
|
||||
```
|
||||
val foo = "bar"
|
||||
val bar = "buzz"
|
||||
assertEquals( {foo: "bar", bar: "buzz"}, { *foo, *bar } )
|
||||
ws = zero or more whitespace (incl. newline/comments)
|
||||
map_literal = '{' ws map_entries ws '}'
|
||||
map_entries = map_entry ( ws ',' ws map_entry )* ( ws ',' )?
|
||||
map_entry = map_key ws ':' ws map_value_opt
|
||||
| '...' ws expression
|
||||
map_key = string_literal | ID
|
||||
map_value_opt = expression | ε // ε allowed only if map_key is ID
|
||||
```
|
||||
|
||||
Question to the AI: maybe better syntax than asterisk for that case?
|
||||
Notes:
|
||||
- Identifier shorthand (`id:`) is allowed only for identifiers, not string-literal keys.
|
||||
- Spreads accept any expression; at runtime it must yield a `Map` with string keys.
|
||||
- Duplicate keys are detected at compile time among literal keys; spreads are merged at runtime with last-wins.
|
||||
|
||||
So, summarizing, overwriting/duplication rules are:
|
||||
|
||||
- string literals can't duplicate
|
||||
- splats add or update content, effectively overwrite preceding content,
|
||||
- string literals overwrite content received from preceding splats (as no duplication string literal keys allowed)
|
||||
- the priority and order is left-to-right, rightmost wins.
|
||||
- var inclusion is treated as form of the literal
|
||||
|
||||
This approach resolves the ambiguity from lambda syntax, as
|
||||
|
||||
```ebnf
|
||||
ws = zero or more whitespace characters including newline
|
||||
map_literal start = "{", ws, (s1 | s2 | s3)
|
||||
s1 = string_literal, ws, ":", ws, expression
|
||||
s2 = "...", string_literal
|
||||
s3 = "*", string_literal
|
||||
```
|
||||
|
||||
as we can see, `map_literal_start` is not a valid lambda beginning so it is not create ambiguity.
|
||||
Rationale
|
||||
- The `{ name: value }` style is familiar and ergonomic.
|
||||
- Disambiguation with lambdas leverages the required `->` in typed lambda headers.
|
||||
- Avoiding `m.a` sidesteps method/field shadowing and keeps semantics clear.
|
||||
|
||||
+54
-5
@@ -676,14 +676,63 @@ Please see [Set] for detailed description.
|
||||
|
||||
# Maps
|
||||
|
||||
Maps are unordered collection of key-value pairs, where keys are unique. See [Map] for details. Map also
|
||||
are [Iterable]:
|
||||
Maps are unordered collections of key-value pairs, where keys are unique. Maps are also [Iterable].
|
||||
|
||||
val m = Map( "foo" => 77, "bar" => "buzz" )
|
||||
assertEquals( m["foo"], 77 )
|
||||
You can create them either with the classic constructor (still supported):
|
||||
|
||||
val old = Map( "foo" => 77, "bar" => "buzz" )
|
||||
assertEquals( old["foo"], 77 )
|
||||
>>> void
|
||||
|
||||
Please see [Map] reference for detailed description on using Maps.
|
||||
…or with map literals, which are often more convenient:
|
||||
|
||||
val x = 10
|
||||
val y = 10
|
||||
val m = { "a": 1, x: x * 2, y: }
|
||||
// identifier keys become strings; `y:` is shorthand for y: y
|
||||
assertEquals(1, m["a"]) // string-literal key
|
||||
assertEquals(20, m["x"]) // identifier key
|
||||
assertEquals(10, m["y"]) // shorthand
|
||||
>>> void
|
||||
|
||||
Map literals support trailing commas for nicer diffs:
|
||||
|
||||
val m2 = {
|
||||
"a": 1,
|
||||
b: 2,
|
||||
}
|
||||
assertEquals(1, m2["a"])
|
||||
assertEquals(2, m2["b"])
|
||||
>>> void
|
||||
|
||||
You can spread other maps inside a literal with `...`. Items merge left-to-right and the rightmost value wins:
|
||||
|
||||
val base = { a: 1, b: 2 }
|
||||
val merged = { a: 0, ...base, b: 3, c: 4 }
|
||||
assertEquals(1, merged["a"]) // base overwrites a:0
|
||||
assertEquals(3, merged["b"]) // literal overwrites spread
|
||||
assertEquals(4, merged["c"]) // new key
|
||||
>>> void
|
||||
|
||||
Merging also works with `+` and `+=`, and you can combine maps and entries conveniently:
|
||||
|
||||
val m3 = { "a": 10 } + ("b" => 20)
|
||||
assertEquals(10, m3["a"])
|
||||
assertEquals(20, m3["b"])
|
||||
|
||||
var m4 = ("x" => 1) + ("y" => 2) // entry + entry → Map
|
||||
m4 += { z: 3 } // merge literal
|
||||
assertEquals(1, m4["x"])
|
||||
assertEquals(2, m4["y"])
|
||||
assertEquals(3, m4["z"])
|
||||
>>> void
|
||||
|
||||
Notes:
|
||||
- Access keys with brackets: `m["key"]`. There is no `m.key` sugar.
|
||||
- Empty `{}` remains a block/lambda; use `Map()` to create an empty map.
|
||||
- When you need computed (expression) keys or non-string keys, use `Map(...)` constructor with entries, e.g. `Map( ("a" + "b") => 1 )`, then merge with a literal if needed: `{ base: } + (computedKey => 42)`.
|
||||
|
||||
Please see the [Map] reference for a deeper guide.
|
||||
|
||||
# Flow control operators
|
||||
|
||||
|
||||
Reference in new issue
Block a user