Although, that also works with the with free function syntax.
let urlSession = with(MockURLSession()) { session in
session.dataRequester = with(MockDataRequester()) { requester in
requester.resultValue = /* ... */
}
}
Although, that also works with the with free function syntax.
let urlSession = with(MockURLSession()) { session in
session.dataRequester = with(MockDataRequester()) { requester in
requester.resultValue = /* ... */
}
}
I found it easier to compare different options when putting them together back to back:
// Option 1 (current)
let urlSession = {
var session = MockURLSession()
session.dataRequester = {
var requester = MockDataRequester()
requester.resultValue = /* ...*/
return requester
}()
return session
}()
// Option 2 (proposed "v.with {...}")
let urlSession = MockURLSession().with {
$0.dataRequester = MockDataRequester().with {
$0.resultValue = /*... */
}
}
// Option 3 (proposed "with(v) { ... }")
let urlSession = with(MockURLSession()) {
$0.dataRequester = with(MockDataRequester()) {
$0.resultValue = /* ... */
}
}
// Option 4 (possible "v.with(...)")
let urlSession = MockURLSession().with(
dataRequester: MockDataRequester().with(
resultValue: /* ... */
)
)
Option 4 looks most compact with the two additional traits to it, one positive and one negative:
with is being called on (example below).struct Foo {
var value: Int
func bar(_ v: Int) {
print(v)
}
}
// compact form
Foo(value: 42).bar(theThing.value) // no way to do this
// have to do this instead:
var theThing = Foo(value: 42)
theThing.bar(theThing.value)
I wonder if there are precedents in other languages to allow the compact form from the example.
We already have the “compact form”: it is spelled $0, and to emphasize again, the language steering group is not looking to replace or come up with another alternative to it.
Foo(value: 42).with { $0.bar($0.value) }
This feels related to Lenses. There’s an interesting YouTube video on the topic, and it’s been discussed on the forum before.
In the video. Brandon Williams develops a technique for creating variants of immutable data with concise syntax. It's very old, and can probably be made nicer with key paths
I don't think this potential exists. If in Swift you want to represent a type that
then Swift has already a solution, and it's simply using a struct and making those properties var: this is how this concept is expressed in the language, var properties in a struct mean this, and this is actually a powerful feature that's not present in many other languages, that instead need specialized syntax to represent the simple "copy + update" behavior (while Swift allows for more sophisticated code thanks to the automatic enforcing of value semantics), so failing to understand this critical point will make one not use Swift to its full potential.
As a personal note, in my first years in Swift I completely missed this point, so I get when one doesn't understand it (or stubbornly refuses it, due to familiarity with other languages), but when I "discovered" this feature, I fully embraced it, and then mine and my team's code got better, clearer, safer, faster to write et cetera.
Again, this is a major downside, that should compel us to discard that option altogether, in favor of the closure + $0 form, which is idiomatic, embedded in the language from the very start, far more flexible, and certainly more appropriate when considering a general extension to the language. Closures + $0 are here to stay, and I think they should be embraced an leveraged, instead of inventing a new language with constructs that would open the doors to a long stream of sub-pitches to add more features to it in order to make it on-par with the actual existing language.
Circling around this point due to an "allergy" to $0 (that's not even required, one could just spell the closure input explicitly with a Kotlin-style it, for example, or this, or x...) will not help the discussion, so I'd suggest again to instead think about another pitch to replace $0 with something else (or add another option in case of a single parameter).
How would you do this example with vars?
I wouldn't, it's not a sufficiently general example (it's only valid for specific types with a specific initializer that can return nil), and there's no reason to just add this for everything. An opt-in macro would be a better option, and the pitch is about something else.
Also, that with method doesn't check the validity of names. So you could easily create a person with an invalid name.
Person(...)
.with(first: "X Æ", last: "A-12", age: -7)
Is that the typo you were referring to?
In my example ".with" would return "nil" on invalid input (whatever that means for the particular type). And instead of returning Optional both with and initialiser could be throwing.
Oh, sorry, I didn't see that. You're right.
As was mentioned above, SwiftProtobuf generates a with static factory method for each generated message object type. This approach provided a clean way to construct new, immutable instances that would insulate client code from changes to initializer arguments. This was especially important for SwiftProtobuf for two reasons:
The example from the SwiftProtobuf documentation:
let e = Example.with {
$0.field1 = 7
$0.field2 = ["foo", "bar"]
}
(As Tony Allevato pointed out, this could have been implemented as a closure-taking initializer just as well.)
Note that this version of with is explicitly a static factory method -- it can only be used to construct a new instance, it cannot be used to modify an existing instance. I'm not entirely convinced of the utility of the latter case. In particular, I notice almost all of the examples listed in this thread have been for configuring a new value. It also seems that if you need to modify an instance, you always have the option of making it mutable.
Update:
I revised the proposal to use a with method instead of a free function as that seemed like the more popular choice. Currently there is no implementation, but if anyone wants to help with the implementation, please reply to this message.
Thanks for updating the proposal! I'd love to see this happen.
I'm certainly interested in trying to implement this in the compiler. In the past I worked on SE-0345 and SE-0365 (plus some other proposals that didn't make it to the review stage) so hopefully have enough experience with the compiler codebase to get a proof of concept up and running.
If I find time to look in to this in, I can DM you with updates.
Maybe just make do an expression?
let widget = do {
var w = Widget()
w.identifier = newId
w.color = .red
w.width = 100
w.angle = 2
w
}
Potentially it could be enriched by an optional init keyword:
let widget = do init Widget {
self.init()
identifier = newId
color = .red
width = 100
angle = 2
}
which is in fact an inline version of init extension:
extension Widget {
init(identifier: Int, color: Color, width: CGFloat, angle: Double) {
self.init()
self.identifier = identifier
self.color = color
self.width = width
self.angle = angle
}
}
But why to stop on inits? We could support inline extensions in general:
do init Type { ... } // equivalent of extension init
do with existingValue { ... } // equivalent of extension func, the return type is inferred by the body
do mutating existingValue { ... } // equivalent of extension mutating func, the return type is inferred by the body
do consuming existingValue { ... } // equivalent of extension consuming func, the return type is inferred by the body
do modifying existingValue { ... } // equivalent of extension func that is forced to return self
Just surfacing this thought in case it hasn't been brought up yet - the with method/function works great with value types and in this case creates a new copy as expected but with reference types it implicitly mutates the original copy:
var body: some View {
VStack {
let viewModel = ReferenceTypeObservableViewModel()
let modifiedViewModel = viewModel.with { $0.property = "newValue" }
// Potential intuitive expectation that each row would have different data
Row(model: viewModel)
Row(model: modifiedViewModel)
}
}
I think there are several approaches here:
with on both value and reference types.with() for value types and .set() for reference types.with() to value typesThis is basically impossible to enforce, since a struct or enum can still have reference semantics. Also, that would require a protocol NOT constraint which is not currently possible in the language.
func with<T>(_ value: T, transform: (inout T) -> Void) -> T where T: Copyable, T: !AnyObject { ... }
I think like with most of swift you just need to know what you're using.
Since we're talking about having compiler support already, it might be reasonable to warn when the body of a with closure doesn't contain any actual mutating or inout operations that act on the parameter. Reference-semantics operations don't generally mutate the receiver's value, so that might be a good heuristic to catch that sort of situation.
Hmm.., maybe, but sometimes that can be useful, such as...
SKLabelNode().with {
$0.text = "Hello, World!"
$0.fontColor = .red
$0.fontSize = 25
}
In this example, no actual mutation is performed on the SKSpriteNode, but it's still useful, especially for, say, in an SKScene where you might want to initialize it in place.
class GameScene: SKScene {
let label = SKLabelNode().with { label in
label.text = "Hello, world!"
label.fontColor = .red
}
}
If there is a warning for that there should at least be a way to silence it if that is what you want to do.