Skip to content

Commit ee21ff4

Browse files
authored
Merge pull request #73 from kyouko-taiga/main
SIP-59 - Multiple assignments
2 parents b6041cb + 649ccc0 commit ee21ff4

File tree

1 file changed

+331
-0
lines changed

1 file changed

+331
-0
lines changed

content/multiple-assignments.md

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
---
2+
layout: sip
3+
permalink: /sips/:title.html
4+
stage: implementation
5+
status: waiting-for-implementation
6+
presip-thread: https://contributors.scala-lang.org/t/pre-sip-multiple-assignments/6425
7+
title: SIP-59 - Multiple Assignments
8+
---
9+
10+
**By: Dimi Racordon**
11+
12+
## History
13+
14+
| Date | Version |
15+
|---------------|--------------------|
16+
| Jan 17th 2024 | Initial Draft |
17+
18+
## Summary
19+
20+
This proposal discusses the syntax and semantics of a construct to assign multiple variables with a single expression.
21+
This feature would simplify the implementation of operations expressed in terms of relationships between multiple variables, such as [`std::swap`](https://en.cppreference.com/w/cpp/algorithm/swap) in C++.
22+
23+
## Motivation
24+
25+
It happens that one has to assign multiple variables "at once" in an algorithm.
26+
For example, let's consider the Fibonacci sequence:
27+
28+
```scala
29+
class FibonacciIterator() extends Iterator[Int]:
30+
31+
private var a: Int = 0
32+
private var b: Int = 1
33+
34+
def hasNext = true
35+
def next() =
36+
val r = a
37+
val n = a + b
38+
a = b
39+
b = n
40+
r
41+
```
42+
43+
The same iterator could be rewritten more concisely if we could assign multiple variables at once.
44+
For example, we can write the following in Swift:
45+
46+
```swift
47+
struct FibonacciIterator: IteratorProtocol {
48+
49+
private var a: Int = 0
50+
private var b: Int = 1
51+
init() {}
52+
53+
mutating func next() -> Int? {
54+
defer { (a, b) = (b, a + b) }
55+
return a
56+
}
57+
58+
}
59+
```
60+
61+
Though the differences may seem frivolous at first glance, they are in fact important.
62+
If we look at a formal definition of the Fibonacci sequence (e.g., on [Wikipedia](https://en.wikipedia.org/wiki/Fibonacci_sequence)), we might see something like:
63+
64+
> The Fibonacci sequence is given by *F(n) = F(n-1) + F(n+1)* where *F(0) = 0* and *F(1) = 1*.
65+
66+
Although this declarative description says nothing about an evaluation order, it becomes a concern in our Scala implementation as we must encode the relationship into multiple operational steps.
67+
This decomposition offers opportunities to get things wrong:
68+
69+
```scala
70+
def next() =
71+
val r = a
72+
a = b
73+
b = a + b // invalid semantics, the value of `a` changed "too early"
74+
r
75+
```
76+
77+
In contrast, our Swift implementation can remain closer to the formal definition and is therefore more legible and less error-prone.
78+
79+
Multiple assignments show up in many general-purpose algorithms (e.g., insertion sort, partition, min-max element, ...).
80+
But perhaps the most fundamental one is `swap`, which consists of exchanging two values.
81+
82+
We often swap values that are stored in some collection.
83+
In this particular case, all is well in Scala because we can ask the collection to swap elements at given positions:
84+
85+
```scala
86+
extension [T](self: mutable.ArrayBuffer[T])
87+
def swapAt(i: Int, j: Int) =
88+
val t = self(i)
89+
self(i) = self(j)
90+
self(j) = t
91+
92+
val a = mutable.ArrayBuffer(1, 2, 3)
93+
a.swapAt(0, 2)
94+
println(a) // ArrayBuffer(3, 2, 1)
95+
```
96+
97+
Sadly, one can't implement a generic swap method that wouldn't rely on the ability to index a container.
98+
The only way to express this operation in Scala is to "inline" the pattern implemented by `swapAt` every time we need to swap two values.
99+
100+
Having to rewrite this boilerplate is unfortunate.
101+
Here is an example in a realistic algorithm:
102+
103+
```scala
104+
extension [T](self: Seq[T])(using Ordering[T])
105+
def minMaxElements: Option[(T, T)] =
106+
import math.Ordering.Implicits.infixOrderingOps
107+
108+
// Return None for collections smaller than 2 elements.
109+
var i = self.iterator
110+
if (!i.hasNext) { return None }
111+
var l = i.next()
112+
if (!i.hasNext) { return None }
113+
var h = i.next()
114+
115+
// Confirm the initial bounds.
116+
if (h < l) { val t = l; l = h; h = l }
117+
118+
// Process the remaining elements.
119+
def loop(): Option[(T, T)] =
120+
if (i.hasNext) {
121+
val n = i.next()
122+
if (n < l) { l = n } else if (n > h) { h = n }
123+
loop()
124+
} else {
125+
Some((l, h))
126+
}
127+
loop()
128+
```
129+
130+
*Note: implementation shamelessly copied from [swift-algorithms](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/MinMax.swift).*
131+
132+
The swap occurs in the middle of the method with the sequence of expressions `val t = l; l = h; h = l`.
133+
To borrow from the words of Edgar Dijskstra [1, Chapter 11]:
134+
135+
> [that] is combersome and ugly compared with the [multiple] assignment.
136+
137+
While `swap` is a very common operation, it's only an instance of a more general class of operations that are expressed in terms of relationships between multiple variables.
138+
The definition of the Fibonacci sequence is another example.
139+
140+
## Proposed solution
141+
142+
The proposed solution is to add a language construct to assign multiple variables in a single expression.
143+
Using this construct, swapping two values can be written as follows:
144+
145+
```scala
146+
var a = 2
147+
var b = 4
148+
(a, b) = (b, a)
149+
println(s"$a$b") // 42
150+
```
151+
152+
The above Fibonacci iterator can be rewritten as follows:
153+
154+
```scala
155+
class FibonacciIterator() extends Iterator[Int]:
156+
157+
private var a: Int = 0
158+
private var b: Int = 1
159+
160+
def hasNext = true
161+
def next() =
162+
val r = a
163+
(a, b) = (b, a + b)
164+
r
165+
```
166+
167+
Multiple assignments also alleviate the need for a swap method on collections, as the same idiomatic pattern can be reused to exchange elements at given indices:
168+
169+
```scala
170+
val a = mutable.ArrayBuffer(1, 2, 3)
171+
(a(0), a(2)) = (a(2), a(0))
172+
println(a) // ArrayBuffer(3, 2, 1)
173+
```
174+
175+
### Specification
176+
177+
A multiple assignment is an expression of the form `AssignTarget ‘=’ Expr` where:
178+
179+
```
180+
AssignTarget ::= ‘(’ AssignTargetNode {‘,’ AssignTargetNode} ‘)’
181+
AssignTargetNode ::= Expr | AssignTarget
182+
```
183+
184+
An assignment target describes a structural pattern that can only be matched by a compatible composition of tuples.
185+
For example, the following program is legal.
186+
187+
```scala
188+
def f: (Boolean, Int) = (true, 42)
189+
val a = mutable.ArrayBuffer(1, 2, 3)
190+
def b = a
191+
var x = false
192+
193+
(x, a(0)) = (false, 1337)
194+
(x, a(1)) = f
195+
((x, a(1)), b(2)) = (f, 9000)
196+
(x) = Tuple1(false)
197+
```
198+
199+
A mismatch between the structure of a multiple assignment's target and the result of its RHS is a type error.
200+
It cannot be detected during parsing because at this stage the compiler would not be able to determine the shape of an arbitrary expression's result.
201+
For example, all multiple assignments in the following program are ill-typed:
202+
203+
```scala
204+
def f: (Boolean, Int) = (true, 42)
205+
val a = mutable.ArrayBuffer(1, 2, 3)
206+
def b = a
207+
var x = false
208+
209+
(a(1), x) = f // type mismatch
210+
(x, a(1), b(2)) = (f, 9000) // structural mismatch
211+
(x) = false // structural mismatch
212+
(x) = (1, 2) // structural mismatch
213+
```
214+
215+
Likewise, `(x) = Tuple1(false)` is _not_ equivalent to `x = Tuple1(false)`.
216+
The former is a multiple assignment while the latter is a regular assignment, as described by the [current grammar](https://docs.scala-lang.org/scala3/reference/syntax.html) (see `Expr1`).
217+
Though this distinction is subtle, multiple assignments involving unary tuples should be rare.
218+
219+
The operational semantics of multiple assignments (aka concurrent assignments) have been studied extensively in scienific literature (e.g., [1, 2]).
220+
A first intuition is that the most desirable semantics can be achieved by fully evaluating the RHS of the assignment before assigning any expression in the LHS [1].
221+
However, additional considerations must be given w.r.t. the independence of the variables on the LHS to guarantee deterministic results.
222+
For example, consider the following expression:
223+
224+
```scala
225+
(x, x) = (1, 2)
226+
```
227+
228+
While one may conclude that such an expression should be an error [1], it is in general difficult to guarantee value independence in a language with pervasive reference semantics.
229+
Further, it is desirable to write expressions of the form `(a(0), a(2)) = (a(2), a(0))`, as shown in the previous section.
230+
Another complication is that multiple assignments should uphold the general left-to-right evaluation semantics of the Scala language.
231+
For example, `a.b = c` requires `a` to be evaluated _before_ `c`.
232+
233+
Note that regular assignments desugar to function calls (e.g., `a(b) = c` is sugar for `a.update(b, c)`).
234+
One property of these desugarings is always the last expression being evaluated before the method performing the assignment is called.
235+
Given this observation, we address the abovementioned issues by defining the following algorithm:
236+
237+
1. Traverse the LHS structure in inorder and for each leaf:
238+
- Evaluate each outermost subexpression to its value
239+
- Form a closure capturing these values and accepting a single argument to perform the desugared assignment
240+
- Associate that closure to the leaf
241+
2. Compute the value of the RHS, which forms a tree
242+
3. Traverse the LHS and RHS structures pairwise in inorder and for each leaf:
243+
- Apply the closure formerly associated to the LHS on RHS value
244+
245+
For instance, consider the following definitions.
246+
247+
```scala
248+
def f: (Boolean, Int) = (true, 42)
249+
val a = mutable.ArrayBuffer(1, 2, 3)
250+
def b = a
251+
var x = false
252+
```
253+
254+
The evaluation of the expression `((x, a(a(0))), b(2)) = (f, 9000)` is as follows:
255+
256+
1. form a closure `f0 = (rhs) => x_=(rhs)`
257+
2. evaluate `a(0)`; result is `1`
258+
3. form a closure `f1 = (rhs) => a.update(1, rhs)`
259+
4. evaluate `b`; result is `a`
260+
5. evaluate `2`
261+
6. form a closure `f2 = (rhs) => a.update(2, rhs)`
262+
7. evaluate `(f, 9000)`; result is `((true, 42), 9000)`
263+
8. evaluate `f0(true)`
264+
9. evaluate `f1(42)`
265+
10. evaluate `f2(9000)`
266+
267+
After the assignment, `x == true` and `a == List(1, 42, 9000)`.
268+
269+
The compiler is allowed to ignore this procedure and generate different code for optimization purposes as long as it can guarantee that such a change is not observable.
270+
For example, given two local variables `x` and `y`, their assignments in `(x, y) = (1, 2)` can be reordered or even performed in parallel.
271+
272+
### Compatibility
273+
274+
This proposal is purely additive and have no backward binary or TASTy compatibility consequences.
275+
The semantics of the proposed new construct is fully expressible in terms of desugaring into current syntax, interpreteted with current semantics.
276+
277+
The proposed syntax is not currently legal Scala.
278+
Therefore no currently existing program could be interpreted with different semantics using a newer compiler version supporting multiple assignments.
279+
280+
### Other concerns
281+
282+
One understandable concern of the proposed syntax is that the semantics of multiple assignments resembles that of pattern matching, yet it has different semantics.
283+
For example:
284+
285+
```scala
286+
val (a(x), b) = (true, "!") // 1
287+
288+
(a(x), b) = (true, "!") // 2
289+
```
290+
291+
If `a` is instance of a type with a companion extractor object, the two lines above have completely different semantics.
292+
The first declares two local bindings `x` and `b`, applying pattern matching to determine their value from the tuple `(true, "!")`.
293+
The second is assigning `a(x)` and `b` to the values `true` and `"!"`, respectively.
294+
295+
Though possibly surprising, the difference in behavior is easy to explain.
296+
The first line applies pattern matching because it starts with `val`.
297+
The second doesn't because it involves no pattern matching introducer.
298+
Further, note that a similar situation can already be reproduced in current Scala:
299+
300+
```scala
301+
val a(x) = true // 1
302+
303+
a(x) = true // 2
304+
```
305+
306+
## Alternatives
307+
308+
The current proposal supports arbitrary tree structures on the LHS of the assignment.
309+
A simpler alternative would be to only support flat sequences, allowing the syntax to dispense with parentheses.
310+
311+
```scala
312+
a, b = b, a
313+
```
314+
315+
While this approach is more lightweight, the reduced expressiveness inhibits potentially interesting use cases.
316+
Further, consistently using tuple syntax on both sides of the equality operator clearly distinguishes regular and multiple assignments.
317+
318+
## Related work
319+
320+
A Pre-SIP discussion took place prior to this proposal (see [here](https://contributors.scala-lang.org/t/pre-sip-multiple-assignments/6425/1)).
321+
322+
Multiple assignments are present in many contemporary languages.
323+
This proposal already illustrated them in Swift, but they are also commonly used in Python.
324+
Multiple assigments have also been studied extensively in scienific literature (e.g., [1, 2]).
325+
326+
## FAQ
327+
328+
## References
329+
330+
1. Edsger W. Dijkstra: A Discipline of Programming. Prentice-Hall 1976, ISBN 013215871X
331+
2. Ralph-Johan Back, Joakim von Wright: Refinement Calculus - A Systematic Introduction. Graduate Texts in Computer Science, Springer 1998, ISBN 978-0-387-98417-9

0 commit comments

Comments
 (0)