The "wrong" thing is that you should use var properties for structs that have a memberwise initializer (which is the case for the Person struct in your code, because it's generated automatically): please check the explanation that I posted earlier.
Then, you could consider using the operator that I proposed to do that copy-and-update automatically for all structs, instead of needing a with function specific for each type.
As I mentioned, Swift automatic enforcing of value semantics allows to create structs with mutable properties, that are actually immutable (because they're values) but that can worked with as if they're mutable: this is one of the most important Swift features, but it's often not well understood. Please PM me if you want to discuss this more, as not to derail the thread .
EDIT: in fact, another "wrong" thing, I think, is the fact that your with function could be mutating, instead of returning a new value. Structs with var properties and mutating functions are the foundational components that allow to leverage Swift enforcing of value semantics.
Well, this is different. In fact more powerful than what the pitch proposes. If this version of "with" is generated automatically (e.g. only when it is used) it could be a powerful feature as it handles "let's" as well. Among these:
This suffers all the same limitations of the builder pattern, with no way to call arbitrary mutating methods or derive one property from another property. I have used it for a UIColor extension that adjusts HSL values, and it's generally a good fit for types where there aren't mutable properties to begin with (although that could be argued to be an underlying API design problem)
Obviously the original. To refer to another just use "another.xxx".
Sometimes "less is more"... If I want to do arbitrary mutation I can always do it by normal means. Plus "you win some you lose" some - if that version of "with" indeed works for let properties that's more powerful (at least in that regards). No, I am not buying your argument that "properties should always be vars unless..." although you are fully entitled to that personal opinion.
Unless identifier is let of course... in which case $0.identifier += 1 just won't work.
the equivalent would be:
let components = another.with(
path: "some/long/path"
identifier: another.path.split(separator: "/").last!
)
nice and clean IMHO.
Edit: Aha, I now see what you are doing... changing path first and using it next. My version above is not quite equivalent... the equivalent would be:
let path = "some/long/path" // or some long expression here
let components = another.with(
path: path,
identifier: path.split(separator: "/").last!
)
And if you say: "but that's a whole extra line!"... so is this:
func foo(bar: Int) {
var bar = bar // this is also another line
}
This is also another line, that, IIRC we didn't have in Swift 1 (or 2?) which allowed var arguments... Sometimes "extra line" is not a deal breaker.
That would be a usage of self that is completely different from any other usage in Swift, and I doubt it could be even seriously considered in this pitch.
If you find $0 ugly, I'd suggest to scope that into a separate discussion, but I'm not sure that a pitch caused by a mere aesthetic preference would get much traction.
My argument is: it's pointless for them to be let, and it doesn't use actual useful Swift language features (foundational, I'd say). Not to mention that always using let will make code less maintainable because there will be no distinction from what should actually be let to enforce invariants, so it's also a point about good modeling.
I'm simply trying to build on top of the existing language, and compose its useful existing features.
I don't quite follow, what do you mean? Obviously in another.foo(self.bar) self refers to original, not another. That's always how it was and I am totally lost what do you mean.
Leaving that aside, what don't you like in this example:
let components = { var s0 = another
s0.path = "foo"
s0.password = "bar"
return s0
}()
refers to the type in which the scope that contains that code is, but what @jayrhynas was referring to, I think, was to use the original identifier value of the another instance to derive the final mutated value, so I assumed that self meant "the original instance (that is, another)" there: sorry if I was mistaken. The point remains though: it would be clearly more noise and more boilerplate to repeat another and the property name twice... it seems self-evident to me.
obviously this
let components = { var s0 = another
s0.path = "foo"
s0.password = "bar"
return s0
}()
I will not waste my time pointing out all the differences again (just to mention another one, type inference will be faster because .with returns the exact same type of the value it's applied to), and I feel like I made a sufficiently compelling case for adding some syntactic sugar on top of the (idiomatic) former code, if you use it a lot (like me and many others).
On a side note: not even the idiomatic code would work with var properties.
Assuming that the pack iteration pitch turns into an accepted proposal (and I understood the syntax well ), I think .with could be composed with a function like the following:
func setting<Root, each Value>(_ keyPathAndNewValue: repeat (WritableKeyPath<Root, each Value>, each Value)) -> (inout Root) -> Void {
{ value in
for keyPath, newValue in repeat (each keyPathAndNewValue) {
value[keyPath: keyPath] = newValue
}
}
}
to enforce simple setting or properties with no closure via key paths, so something like:
let components = another.with(setting(
(\.path, "foo"),
(\.password, "bar")
))
IMO: supporting ergonomic construction of new values with arbitrarily updated lets is an anti-goal here. Either the property is subject to important invariants (in which case type authors must carefully consider how the property is meant to be initialized) or it isn't and it should just be a var.
IMHO it could well be a goal. Expanding @LLC example:
struct Person {
let first: String
let last: String
let age: Int
init?(first: String, last: String, age: Int) {
guard isValidFirstName(first) else { return nil }
guard isValidLastName(first) else { return nil }
guard isValidAge(age) else { return nil }
self.first = first
self.last = last
self.age = age
}
func with(first: String? = nil, last: String? = nil, age: Int? = nil) -> Self? {
Self.init(
first: first ?? self.first,
last: first ?? self.first,
age: age ?? self.age
)
}
}
With enough fields writing this "with" manually results in quite large amount of boilerplate, besides it's error prone (there's a typo in the above implementation) and doesn't support overriding optional fields, something that built-in "with" could have provided:
let clonedPerson = person.with(
first: "another",
// last is not provided β taken from person.last
address: nil // provided β overrides address with nil
)
Basically you want to re-call the initializer with some new values and some values from the existing instance. While I can see this being valuable in some cases, it only works if the initializer has the same signature as the generated initializer (but potentially failable). IMO this would be better implemented as macro that you can opt into rather than automatically be on all types (since not all types can participate anyway).
Having this available after opting-in rather than available for all types looks good to me.
That's part of the issue... With a "built-in" with (done by compiler or a macro or whatever) that could be possible. Pseudo code:
// library type
enum Optional3<T> {
case absent
case none // nil
case some(T)
}
// compiler generated method
func with(first: Optional3(String) = .absent, last = Optional3(String) = .absent, age: Optional3(Int) = .absent) -> Self? {
Self.init(
first: first == .absent ? self.first : first,
last: first == .absent ? self.first : second,
age: age == .absent ? self.age : age
)
}
// user code
let clonedPerson = person.with(
first: "another", // literal, no need to say .some("another")
// last is not provided β absent β taken from person.last
address: nil, // or .none β provided β overrides address with nil
previousAddress: .absent // ideally should be an error, like "`absent` is not available"
)
The value of .with syntax shows itself when you want to chain multiple mutations on one complex instance.
Imagine trying to provide a Mock class to an object for testing:
let urlSession = MockURLSession().with {
$0.dataRequester = MockDataRequester().with {
$0.resultValue = /*...*/
}
}
let sut = SUT(urlSession: urlSession)
In this use-case it is very readable what we're doing... we're creating a MockURLSession with its dataRequester being mocked to return a specific resultValue. While doing it normally with closures add so much noise to the code:
let urlSession: URLSessionProtocol = {
var mockSession = MockURLSession()
session.dataRequester = {
var mockDataRequester = MockDataRequester()
mockDataRequester.resultValue = /*...*/
}()
return mockSession
}()
let sut = SUT(urlSession: urlSession)
I believe you can expand on this example and IMHO it will exponentially become unreadable.