If the operation isn't mutating a copy, you don't really get anything from the with:
let label = SKLabelNode()
label.text = "Hello, world!"
label.fontColor = .red
If the operation isn't mutating a copy, you don't really get anything from the with:
let label = SKLabelNode()
label.text = "Hello, world!"
label.fontColor = .red
class GameScene: SKScene {
let label = SKLabelNode().with { label in
label.text = "Hello, world!"
label.fontColor = .red
}
}
In this example, without with you would need to move this logic to the initializer. Keeping it all together with the variable is nice, and in some cases it avoids you having to create a custom initializer.
Note it's not always that rosy:
users[userId].properties["node"] = SKLabelNode()
users[userId].properties["node"].text = "Hello, world!"
users[userId].properties["node"].fontColor = .red
which normally you'd compact using a separate variable:
let node = SKLabelNode()
node.text = "Hello, world!" // repeating "node" here
node.fontColor = .red // repeating "node" here
users[userId].properties["node"] = node // repeating "node" here
The alternative proposal results into slightly more compact code as you don't have to repeat the argument neither in the original nor in the $0 form:
users[userId].properties["node"] = SKLabelNode().with(
text: "Hello, world!"
fontColor: .red
)
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. That's not a compliment. Java [ab]uses that heavily because it's crippled by a lack of default function arguments, among other things. Swift doesn't have those limitations.
Point being, in all the 'init' examples I've seen, the main issue appears to be that the type being demonstrated is poorly designed - it doesn't have a suitable init method. URLComponents is perhaps the poster child for this, and the main example used in the SEP.
I'm yet to see anyone explain why the root problem is not simply the missing initialiser(s)…?
I get that there are other conveniences to a transient scope, but those can be achieved today using { … }() (or by supporting simple do expressions as discussed in Enhanced 'do' Block Syntax and Error Handling). A few more keystrokes, perhaps, but not a big deal.
I really enjoy not having Java-style builders everywhere, in Swift. I'd hate to see Swift devolve in that regard.
Update on this for folks following along: I put together a compiler implementation for this change (apple/swift#67768) and we submitted an updated version of the proposal (apple/swift-evolution#2312)
Apart from the question of usefulness (a +1 from me on that point), there already seems to be some “agreement” on the naming i.e. using with?
Because (as I already mentioned somewhere) I also find another extension for Any kind of useful, namely conformingTo which tests some condition and returns self if the condition is fullfilled, else nil (I like to use it with if let, maybe I‘ll write a pitch on this).
The name with seems to be easily confusable with this other extension, i.e. with could also be understood to make a test on its subject. In my own code I use the names applying (for the extension in this pitch) and the said conformingTo which I find more understandable, and I find those namings particularly well if you use both extensions as they are not easily confusable with each other.
Wade, in those examples it could equally be something unrelated to a newly initialised variable:
..... = otherVariable.with ....
Another alternative if sugar were on the table... I think access to properties in a constructor makes sense since it would be unifying the main ways instances are constructed. It would get around some sticky situations where you need a mutable type in the middle since the constructor could possibly remain mutable until properties are called.
// Key path labels in constructor.
let label = SKLabelNode(
\.text: "Hello, world!",
\.fontColor: .red
)
// is equivalent to:
let label = SKLabelNode()
label.text = "Hello, world!"
label.fontColor = .red
It would allow inline property initialization for DSLs like SwiftUI. It wouldn't make sense for all modifiers, but I think it makes sense for some types of styling.
var body: some View {
RoundedRectangle(
cornerRadius: 25,
\.fill: Color.blue,
\.frame: .init(width: 300, height: 300)
)
}
To expand on this further, it could be interesting for property wrappers and other contexts where it is hard to use properties.
// `.help` is specified using a property instead of an argument.
@Option(name: .shortAndLong, \.help: "The number of times to repeat 'phrase'.")
var count: Int? = nil
Possibly the slash could be dropped as an alternative syntax.
let label = SKLabelNode(
.text: "Hello, world!",
.fontColor: .red
)
It avoids repeating yourself between constructors/properties and having to document those properties in two places.
Right, and there may be merit in that sort of case (I personally don't have much demand for that pattern, although I do encounter it sometimes, but others can certainly have a different experience).
I just think it's very confusing the way this is currently been discussed (in large part) and especially how it's presented in the SEP. Either I'm missing something important as to why better init methods aren't a solution - in which case, my apologies but I'd be grateful if someone could spell it out for me - or the pitch would be much more persuasive if it focused on situations which are more lacking for existing alternatives.
Just yesterday I searched for a solution to create a copy of let value with 1 changed property, unfortunately, for now, just mutating the original value is practical without a boilerplate, but it's very unsafe.
I looked for Optics from GitHub - pointfreeco/swift-prelude: 🎶 A collection of types and functions that enhance the Swift language., which uses WritableKeyPath and |> operator, I liked this solution because it's very concise but it doesn't work when a property is let.
let updatedUser = user
|> \.name = "Jhon"
I also have a question about ownership, will it consume original value?
I think you mean to use a functional lens set operator .~. That isn't exactly the same as assignment. Not sure what you mean by consume, but it will create a new copy of user (assuming it is a struct) before setting it in updatedUser in this case. If it is a let property instead of a let value then it probably wouldn't have access to user.
let updatedUser = user
|> \.name .~ "Jhon"
I think this is a bit much for the standard library. Embracing operators this way is something Swift hasn't wanted to be opinionated on so far, but there are great libraries like swift-prelude if you prefer the functional approach.
About consume I mean [Accepted] SE-0366: `consume` operator to end the lifetime of a variable binding.
Yes, in the original code operator .~ is used. I just provided an example of how it can look if using |> and KeyPath approach inspired by the swift-prelude library.
The assignment operator can't be overloaded, so this wouldn't work. I think you would want a full set of lens operators if taking this approach anyway...
I hope we will allow this function to work with class instances, so we can use the proposed function to configure class instances like this:
class MyView: UIView {
let myChildView = UIView().with {
$0.background = .red
}
}
I think this is the most compelling use case:
This makes three independent exclusive borrows through two different collections. It’s not even clear that explicit borrowings would let you borrow users[userID].properties["node"], since it indexes via subscript. The borrow feature would need to be generalized to a lens. with can be the factory for that lens, in a far more approachable form.
Using the with combinator as proposed:
users[userId].properties["node"] = SKLabelNode().with { node in
node.text = "Hello, world!"
node.fontColor = .red
}
still doesn't really strike me as saving much in the way of tokens or even lines of code. I don't really have a strong opinion as to whether we should promote or discourage the use of it with reference-semantics operations, but if enough people are concerned about the possible confusion, then warning on the lack of formally mutating operations in the body would be the way to do it.
+1 to @Joe_Groff 's point about strictness of mutating semantics. That's why in my proposal there are 5 different types of scoped operations on a value. mutating should be used when the operation is mutating, consuming should be used when it's consuming, etc.
To be specific: I want with on ref-types to be borrowing.
But if you can use with on arbitrary mutable storage, that (mis)use can be transformed into something useful:
with(users[userId].properties["node"]) { (inout SKNode?) in
let node = SKNode()
node.text = "Hello, world!"
node.fontColor = .red
$0 = node
}
That would be fine even with the hypothetical warning since you assign to $0 in the end.
But I never mutated the reference type value (SKNode). So yes, the rephrasing avoids the error by making the SKNode not the subject of the with.