Syntactic Addition for Setting and Returning Values
In Swift, there are often times where one would like to return and set the same value. This is not good for a few significant reasons.
1. Firstly and most importantly, this situation can require additives that negatively impact performance:
The value to be read (an extra time unnecessarily).
extension Int {
mutating func square() -> Int {
self = self * self
return self
}
}
var num = 5
print(num.square()) //Prints 25
print(num) //Prints 25
The recalculation of a value (i.e. if the value it is being set to has a didSet block that changes the value). This can also require the creation of a separate variable or constant to store the value in the interim.
var n : Int = 5 {
didSet {
n = n + 1
}
}
//I want to return the actual square, not the value of x after it has been squared
func square(_ x: inout Int) -> Int {
let y = x * x
x = y
return y
}
print(square(&n)) //Prints 25
print(n) //Prints 26
2. This can also make for more redundant and less efficient code.
Less efficient code; poorer performance
Extra and unnecessary instances of values.
Can require a value to be read an extra time.
You have to use multiple lines of code to accomplish what – with this proposal's addition – can be done in only one line of code
Proposed Solution
Returning and setting a value in the same line, while enhancing readability, can speed up performance and reduce the amount of redundant code.
Solution:return variable = n
This would return n and set variables value to n at the same time. This decreases redundancy while simultaneously improving efficiency as there is no need to create separate variables/constants to hold values or read additional values.
Also, this would not affect code that has already been written without this addition as it does not change any of Swift's syntax, it adds a faster alternative for these situations.
Improvements for Aforementioned Situations Utilizing the Proposed return variable = n
The value to be read (an extra time unnecessarily).
extension Int {
mutating func square() -> Int {
self = self * self
return self
}
}
Improvements with return variable = n:
extension Int {
mutating func square() -> Int {
return self = self * self //Proposed syntactical addition
}
}
var num = 5
print(num.square()) //Prints 25
print(num) //Prints 25
The recalculation of a value (i.e. if the value it is being set to has a didSet block that changes the value). This can also require the creation of a separate variable or constant to store the value in the interim.
var n : Int = 5 {
didSet {
n = n + 1
}
}
//I want to return the actual square, not the value of x after it has been squared
func square(_ x: inout Int) -> Int {
let y = x * x
x = y
return y
}
print(square(&n)) //Prints 25
print(n) //Prints 26
Improvements with return variable = n:
var n : Int = 5 {
didSet {
n = n + 1
}
}
//I want to return the actual square, not the value of x after it has been squared
func square(_ x: inout Int) -> Int {
return x = x * x //Proposed syntactical addition
}
print(square(&n)) //Prints 25
print(n) //Prints 26
NamingÂą
Possible names for this syntactical addition:
Mutating return statements
Return setters
¹. If you have a good name, please write it in the comments and – assuming the name appropriately fits this syntactical proposal – I will include it in this list.
Alternative Syntax for return variable = n
variable = return n
This can provide more clarity regarding what value is actually being returned.
Final Words
All comments, additions, suggestions, criticism, and improvement are welcome. Thank you for taking the time to read my proposal for the evolution of Swift.
The capability you're interested in could also be achieved by simply making assignment an expression, rather than a statement, such that the value of the assignment expression is the value that was assigned. This is how C and some other C-derived languages treat it, which is why you can say things in those languages like
int x, y, z;
x = y = z = 0;
Given that Swift is syntactically like C in many ways, sometimes I've wondered why the core team opted to leave this out—maybe they feel that it's along the same lines as ++, --, and C-style for loops which were later removed, as features that aren't truly necessary and don't strictly improve the legibility of code.
It doesn't look like there's been much discussion about this on the forums that I can find, but @jrose has some interesting insight in this post about how it's not immediately obvious what the correct thing is to do when properties with possible side effects to assignment are involved.
This is a source breaking change since the magical assignment operator = returns Void already, in which case something like optionalValue.map { nonOptionalVariable = $0 } will start returning optional type of $0 while it should stay Void?.
In other cases you could already have written something like this:
func doImportantWork() -> Void { // I just want to highlight that the functions returns `Void`
[...]
guard hasNoError else {
[...]
return errorCode = errorXYZ
}
[...]
}
This will also break. Even though I do not recommend to write it like this if you're working with non-experts, it's still totally possible and if you know that = operator returns Void, then that code can make total sense in some situations.
You can imagine the assignment operator to be defined something like that (but not entirely):
func = <T>(lhs: inout T, rhs: T) -> Void
If we would add your rule it will return T instead and act like a weird overload.
I read in TSPL that the reason they didn't make assignment an expression was to avoid inadvertent = vs == flaws (that have had historically bad consequences):
if userIsRoot = true {
...
}
simply won't work if assignment isn't an expression.
Yeah, I don't think we want to make assignment a non-Void-returning expression. It's rare that you actually need this anyway. Assign-and-return could be separated out from that, so I won't comment on the main proposal…except to say "what about x += y; return x or x += y; return y?"
(Okay, I lied: I'll also mention that there's no efficiency gain here. The compiler is smart enough even at -Onone to not do the extra copy if you use let, and probably even if you use var and never modify it. It's definitely smart enough to do all that with -O.)
I thought one of the main design decisions of Swift was that expressions are limited. If we wanted to start relaxing this, then I would probably go even farther than this, and say that almost everything is an expression.
func doesThing() -> Int {
if thing == otherThing { 2 } else { 5 }
}
But of course, this is totally impossible at this point. So I don't think this proposal is really solving anything actively harmful.
If the assignment operator were effectively defined as @discardableResult func = <T> (lhs: inout T, rhs: T) -> T then we'd have to write a lot more semicolons, otherwise the resulting T would start changing inferred types everywhere.
It doesn't work even if assignment is an expression, because there's no implicit conversion to Bool anywhere. Having said that, if we ever get generic protocols, it would be nice in the future to have an ImplicitlyConvertibleTo<T> protocol. Because sometimes you actually do want conversion, you just don't want it everywhere.
Related to that: the modulo operation with Mersenne Primes can be written in a very efficient way. Instead of writting %, you use a shift operation and something else. Anyway.
I was implementing this in C, and when compiling with Clang, using % in my code was faster than using the shift trick. Turns out, the compiler knew about this property and could optimize my code using it. I could still use % instead of numeric trickery and it would be very fast (actually, faster) anyway bc I was using a Mersenne Prime.
LLVM backs swiftc and clang. The smarts of both are in many places the same. Thank god for LLVM which lets us avoid making optimizations by hand.
var n : Int = 5 {
didSet {
n = n + 1
}
}
//I want to return the actual square, not the value of x after it has been squared
func square(_ x: inout Int) -> Int {
x = x * x
return x
}
print(square(&n)) //Prints 25
print(n) //Prints 26
Also, when talking about performance, I highly recommend testing performance using a -O build before saying anything, what the optimizer does and doesn't do is pretty random and you can easily be surprised both ways. It's generally incredibly good at optimizations which could also apply to C (like this one), Swift-specific ones can be hit or miss though that's improving.