[Pitch] `With` functions in the standard library

But also, even if an ideal Swift API would not require these things (which I'm not sure about) the fact is there are a lot of types where this kind of copy + mutation is beneficial.

I feel like the with name is already used in the Swift community and it does kind of make sense. "With" a copy of the value, perform changes". You could argue that it would make more sense for it to mean "With" the original value, do something, but for value types that's almost completely useless (and barely easier than just writing a closure and calling it immediately).

And while there are many reference types that could just be modified like this:

{ components in
  components.path = "forums.swift.org/"
  components.password = "fluffybunnies"
  return components
}(NSURLComponents())

IMO the code above is somewhat ugly and difficult to read (I agree with you there) and there should be a better way to do this! With with you can write it like this:

NSURLComponents().with {
  $0.path = "forums.swift.org/"
  $0.password = "fluffy bunnies"
}

Which is more readable for various reasons. Also unlike forEach I don't see any control flow that would be confusing in a with closure other than returning, maybe? Even then the compiler would throw an error since you can't return in a Void function.

Since the beginning of Swift, I think a lot of people have defaulted to let on properties when they should really prefer var. Probably a consequence of “immutable data can’t have races” and defaulting to let for local bindings.

2 Likes

One problem I have is that your example does in fact look to me like it’s mutating the reference type. And that is actually a useful operation, especially when trying to mutate something stored behind a long path of accessors.

jsonDict["jwtHeader"].with {
  $0["alg"] = "none"
  $0["typ"] = "jwt"
}

looks like a perfectly cromulent in-place mutation!

1 Like

But here's the thing: it is mutating the reference type!

Then you’re not talking about a copy-and-mutate operation, which is what everyone else is talking about.

1 Like

I am talking about a copy-and-mutate operation.

Look at this:

var ref = ReferenceType()
var copy = ref

copy.foo = bar

This is just how reference types work: modifying a copy will modify the original, so it is a copy-and mutate operation. It's just that copying a reference type just returns, well, a reference!

1 Like

To be fair, in this case the compiler would emit a “result of call to with is unused” warning.

1 Like

We could mark with as @discardableResult; in fact I think I originally wanted it to be I just forgot somewhere along the line why I wanted that.

However that might make it a little less clear what with does.

I don’t think we’d want with to be @discardableResult. That would encourage using it for its side-effects instead of as a pure “copy and modify” function, and would lose the valuable “unused result” warning above.

8 Likes

One doesn't simply put all imaginable semantics into a single function. You either "copy, mutate, return" or "borrow, mutate" or "consume, mutate, return" or "borrow, transform, return" or "consume, transform, return".

Ben, to what degree would you feel better about this if it were a free function instead of a magic method? We’d lose the nice interaction with optional chaining, but on the other hand, it would be clearer that it wasn’t a mutation of the original value, and it might make available some more suggestive names.

1 Like

I feel like this has gone full circle: In the original pitch thread the name was debated a lot and whether it should be an operator, free function or method was also, and it seemed like people couldn't agree on a better name and thought that it should be a method (at least in the poll I did)

Well, I’m not saying that’s the way to go; but even if we do end up back there, I think we’ve definitely explored the issues far better than we had before, which is the purpose of a pitch.

2 Likes

It shouldn't be a question about people's preferences, it should be about correctness. Correctness is not a debatable thing.

That addresses this use case:

Button("Duplicate") {
  let newItem = with(myModel.items[selectedRowIndex]) {
      $0.name += " copy"
  }
  myModel.insert(newItem)
}

But it doesn’t address this use case:

Button("Edit").sheet(…, onDismiss:
    let newItem = with(myModel.items[selectedRowIndex]) {
      $0.name += newName
      $0.unitPrice += newPrice
      $0.upc = newUPC
  }
  myModel.items[selectedRowIndex] = newItem
})

The second accesses myModel.items[selectedRowIndex] twice and makes an unnecessary copy. Is the first pattern really so much more common than the second?

This is very very different semantic. It's "mutate inplace", not "make a copy, mutate, and return". These two very distinct functions shouldn't share a name, whatever it will be.

2 Likes

We are not talking here about a function here that can mutate an l-value in-place. That would absolutely need to spelled differently from the (copy)-mutate-return operation. But if we did want to add such a function as a free function, it could simply take an inout argument, again modulo the interaction with optional chaining.

2 Likes

Correctness is not debatable but the name of a function definitely is

But with is a valid name for both operations, and in other languages such as Groovy and Kotlin it does mean in-place mutation. Is the prevalence of value types in Swift sufficient to explain this difference in semantics?

Well, yeah; As was brought up much earlier in the thread with is kind of a vacuous word and can mean a lot of things.