You mutated it when you assigned $0
. The warning I was proposing would fire if there were no mutating operations on the closure parameter in the body.
Exactly. It arguably reads a bit better, using this with
, but it first requires knowledge & understanding of this construct, so it makes the language / stdlib a little bit more complicated. Only a little, but non-zero.
It's also potentially less efficient than alternatives (e.g. initialisers). It feels a bit regressive in that sense, like we're going back to [[SKLabelNode alloc] init]
.
Another equivalent way to do this today is:
users[userId].properties["node"] = {
var node = SKLabelNode()
node.text = "Hello, world!"
node.fontColor = .red
node
}()
It also has a potentially unintuitive quirk (the semi-implicit return of node
), but overall it's not much different - in length, complexity, or functionality - to the with
version.
The above form is also more amenable to factoring out into a standalone function. Implicit in a lot of this discussion is a stylistic preference for heavily inlining things, which I actually kind of go along with, but it might be worth stopping for a second and considering that aspect of the motivation.
To reiterate, I'm not particularly against something like this "with" proposal even for these cases, I'm just not enthused by it. It feels like it's missing a point somewhere; that there might be a better solution.

You mutated it when you assigned
$0
. The warning I was proposing would fire if there were no mutating operations on the closure parameter in the body.
To be absolutely clear, did I understand the context of your proposal correctly, in that the warning would be emitted because SKNode().with { $0.fontColor = .red }
doesnât formally mutate $0
due to SKNode being a reference type?
The purpose of my reply was to illustrate how generalizing with
to a lens can enable a rewrite that not only avoids the warning but also reduces the number of exclusivity checks. Is that not a useful operation for reference types as well, to reduce ARC traffic?

To be absolutely clear, did I understand the context of your proposal correctly, in that the warning would be emitted because
SKNode().with { $0.fontColor = .red }
doesnât formally mutate$0
due to SKNode being a reference type?
Yeah, which I raised as a possible solution if people are concerned about confusion around whether with
here is modifying a copy of a value or operating on the same object referenced by the operand.

The purpose of my reply was to illustrate how generalizing
with
to a lens can enable a rewrite that not only avoids the warning but also reduces the number of exclusivity checks. Is that not a useful operation for reference types as well, to reduce ARC traffic?
Yes, but what you propose is formally mutating, so it wouldn't be impeded by such a warning if it were implemented.
I'd really love this alternative to be considered.
In Obj-C we were able to call setters two ways:
object.property = 42
[object setProperty:42]
The two were by and large equivalent. We can do a similar thing in Swift to allow fluent style code pattern without the pain of writing the needed boilerplate manually.
users[userId].properties["node"] = SKLabelNode()
.text("Hello, world!")
.fontColor(.red)
I believe this could work out of the box without any opting in (or with some minimal opt-in marker at the type level). If there's a user provided method with the matching signature â it will be called instead.
This seems to work equally well for both reference and value types.

We can do a similar thing in Swift to allow fluent style code pattern without the pain of writing the needed boilerplate manually.
users[userId].properties["node"] = SKLabelNode() .text("Hello, world!") .fontColor(.red)
I believe this could work out of the box without any opting in (or with some minimal opt-in marker at the type level). If there's a user provided method with the matching signature â it will be called instead.
This seems to work equally well for both reference and value types.
I resisted bringing this up earlier because it's a popular pattern in Java which makes me sad. However, the main problem with it there is all the boilerplate required - that is, all the manually-defined modifier functions for every property.
Iff a Swift version of this didn't require any such boilerplate, then I think it's attractive.
One would have to think through all the possibilities, though. e.g. what if SKLabelNode
actually has a func text(_ foo: String) {}
method? Does that block the use of this syntax for setting text
? Does an implicit modifier still get created, overloading the text
function? Etc.

I resisted bringing this up earlier because it's a popular pattern in Java which makes me sad.
I would think that inventing such a with
extension is exactly the opposite, i.e. not introducing a new pattern, but just something that comes in handy in some situations, maybe when you fetch something to put it into another structure and want to change it âinlineâ without much ceremony. So on the contrary introducing with
frees you from needing such a pattern.
I very well like the idea of with
(although I would use another name) and I think one should not fear a âmisuseâ of it in some strange patterns. (Many language features can be misused, I for example use classes too much instead of structs, because I come from the Java world , but still classes are a good thing.)
So personally, I would welcome a more âdown-to-earth debateâ about a practical tool.

I resisted bringing this up earlier because it's a popular pattern in Java which makes me sad. However, the main problem with it there is all the boilerplate required - that is, all the manually-defined modifier functions for every property.
Regardless of the niche but stubborn push for such "fluent" pattern, the latter has been already largely dismissed in the thread, that is instead focused on the semantics of the a function that takes a regular closure that allows mutating of the input, the naming, the possibility of it being an automatic member for all types instead of a free function, et cetera.

Granted I haven't read every post - this is a long thread! - but a lot of the example cases are basically initialising a struct. In those applications, this reminds me of Java's prevalent builder pattern.
I'd still suggest to read the whole thing
The "builder" pattern is a bad idea, it was conceived in the context of languages with limited features and no currying, and it shouldn't be supported by Swift with some esoteric language feature. This conversation is not really about that: it's instead about ergonomic mutation of types with var
properties and mutating
functions. It's exactly about the replacement of something like:
let newFoo: Foo = {
var m_foo = get.this.foo()
m_foo.bar = "yello"
m_foo.baz = 42
return m_foo
}()
with something like
let newFoo = get.this.foo().with {
$0.bar = "yello"
$0.baz = 42
}
that clearly has higher clarity, and it's focused on what actually matters, eliminating most of the noise and unnecessary syntax.
Just to be clear, the pitch we submitted uses a magic extension on Any, not a free function.
I'd be surprised if it's actually a show-stopper or anything like it, but nonetheless it would be good if someone did some analysis into the cost of an initialiser vs this with
(or similar) pattern, as part of this pitch.
Initialisers can be inlined today (as far as I've seen, at least for trivial value types), but for non-frozen types shouldn't be because it imposes a de facto freeze on the type. For resilient libraries, I mean - but that includes all of Apple's OS libraries.
So this with
pattern is going to incur the cost of actually initialising all the object's memory, and then reinitialise some portion of it, at extra cost.
For higher-level code (e.g. UIKit, SwiftUI) that's arguably insignificant, but for lower-level code, maybe not so much.
I'm not certain there's a performance difference - calling an initialiser with many arguments isn't free either - but it'd be interesting to see how it shakes out.
You seem to be making an implicit assumption that the compiler can expose arbitrary mutable stored properties to the caller, substituting the caller-provided value for that done by the initializer. For resilient types, this is certainly not possible for multiple reasons: the ABI does not expose stored properties any differently from computed ones, and the type may actually require default initialization to a certain value for correctness, even if the value is immediately replaced. For non-resilient types, making the initializer inlineable enables the optimizer to eliminate redundant initialization when safe.
In both cases, your proposal simply decays down to initializing the type as normal, and then assigning to its mutable properties.

You seem to be making an implicit assumption that the compiler can expose arbitrary mutable stored properties to the caller, substituting the caller-provided value for that done by the initializer.
Am I? I thought I was assuming the exact opposite.
Can you clarify what you mean, perhaps with example code?
Sorry, I must have been confused. I thought you were cosigning @dmtâs arbitrary-initializer idea.
I donât really find the arguments against with
being a free function compelling enough to introduce this magical method. One of the arguments is that itâs not idiomatic, but I donât see how that can be the case since the API guidelines explicitly recommends free functions for when the argument is an unbounded type. It also matches all the other withXXX functions and I canât think of any methods that start with with
; so for me itâs the more idiomatic choice.
i donât buy the fluency argument either as both read similarly to me.
The discoverability aspect makes sense, but it has issues too. It pollutes the autocomplete namespace with a method that for the most part will only be called at initialization. Itâs also hard to imagine a user discovering the method naturally when theyâre trying to initialize the type.

let newFoo = get.this.foo().with { $0.bar = "yello" $0.baz = 42 }
I would like to point out that when consume
is complete, one will be able to do the following, which has the same number of lines:
var fooCopy = get.this.foo()
fooCopy.bar = "yello"
fooCopy.baz = 42 // done modifying here
let newFoo = consume fooCopy // can't use fooCopy after this
To be fair this is not currently possible for non-bitwise-copyable types, but it will become possible before long; you can do it now with Array, as long as the var
is not a global variable. The one thing I see so far that is an improvement over the copy-then-consume pattern I suggest is Joe's suggestion of a warning when there is no mutation.

The one thing I see so far that is an improvement over the copy-then-consume pattern I suggest is Joe's suggestion of a warning when there is no mutation.
The other big advantage of with()
is that it avoids copy-on-write by taking exclusive borrow of the value, which can be a massive performance improvement.
IIRC in the fullness of time you could achieve this by making your initial binding an inout var
, but with()
does it all for you with familiar lexical scoping.
Edit: Actually, I donât think itâs as simple as inout var
. You need to move the value into mutable storage, since you canât mutably borrow from an immutable binding.
If you want to have exclusivity during mutation, then you can consume the original binding. But then all you would have done is changed the name for the value, after mutations.

one will be able to do the following, which has the same number of lines
What about this use case, a result builder:
let a = MyStuff() {
get.this.foo().with { $0.bar = "yello"; $0.baz = 42 }
get.another.foo()
}
or as an argument:
doIt(using: get.this.foo().with { $0.bar = "yello"; $0.baz = 42 })
Nice and short.

var fooCopy = get.this.foo() fooCopy.bar = "yello" fooCopy.baz = 42 // done modifying here let newFoo = consume fooCopy // can't use fooCopy after this
This would help to avoid using fooCopy
again after mutation, but it's still verbose, noisy, and one could easily forget to add consume
, so I don't see it as a good alternative to .with
.
I think it would be nice to consume mutable, but I think this pitch is about more about not breaking out of an expression. Especially in context of if/switch expressions, but it can also disrupt the flow of your functions if you need to do a lot of ad-hoc programming in expression-heavy code. I think reducing the number of lines of code is a non-goal.