[swift-evolution] [Pitch] Circling back to `with`

Matthew Johnson matthew at anandabits.com
Sat May 28 21:08:50 CDT 2016


> On May 27, 2016, at 7:19 PM, Brent Royal-Gordon <brent at architechies.com> wrote:
> 
>>> - A plain `with` whose closure parameter is not mutable and which is marked `@discardableResult`.
>> 
>> I would like to see this version restricted to AnyObject.  It has extremely limited utility with value types.  It would usually be a mistake to call it with a value type.
> 
> I would not. It gives you a way to give a value type a short, scoped, immutable alias:
> 
> 	with(RareMagicalDeviceOwner.shared.spimsterWickets[randomIndex]) {
> 		print($0.turns)
> 		print($0.turnSpeed)
> 	}
> 
> And in this form, there is no danger of mistakenly mutating the value type, because mutating methods would not be allowed:
> 
> 	with(RareMagicalDeviceOwner.shared.spimsterWickets[randomIndex]) {
> 		$0.turnRepeatedly(times: 3)	// Error: can't call mutating method on immutable parameter
> 	}
> 
> To be clear, I'm not convinced there's a need to make any change from the proposed version at all. I'm spitballing alternate designs here, trying to see if there might be something a little better out there. But so far, I think the proposal balances the feature size against strictness pretty well, whereas these stricter designs I'm discussing increase the surface of the feature more than they improve it. This is a small (but significant!) convenience, and I feel pretty strongly that it should have a small implementation.
> 
>> That said, I am not convinced these non-copying functions would be worth having after method cascades are introduced.  Are there any use cases left for them in that future?
> 
> Yes, absolutely. Method cascades have a narrow use case: methods on `self`. Not everything in Swift is a method, and not all methods are on `self`.
> 
> 	with(tableView.cellForRow(at: indexPath).myLabel) { label in
> 		print("Constraining label: \(label)")
> 		
> 		NSLayoutConstraint.activate(
> 			NSLayoutConstraint.withVisualFormat("|-[label]-|", options: [], metrics: [:], views: ["label": label]) +
> 			NSLayoutConstraint.withVisualFormat("V:|[label]|", options: [], metrics: [:], views: ["label": label])
> 		)
> 		
> 		constrainedLabels.append(label)
> 	}
> 
> None of the calls in that `with` block would benefit from method cascades, but they all benefit from `with`.
> 
>>> - A `withVar` whose parameter *is* mutable and which is *not* marked `@discardableResult`. (This would help with the fact that our use of `@discardableResult` is a little dangerous, in that people might expect mutations to affect the original variable even if it's a value type.)
>>> 
>>> `withVar` does, I think, make it pretty clear that you're working with a copy of the variable.
>> 
>> One thing to consider in choosing a name here is the cases where this function would still be useful in a future that includes method cascades.  The one thing this function does that method cascades don’t is make a copy of the value before operating on it and returning it.  
>> 
>> With that in mind, I think it is worthwhile to consider the name `withCopy` and make the closure argument optional.
> 
> I specifically considered and rejected `withCopy` because it only creates a copy of a value type, not a reference type. (Of course, it does create a copy of the reference itself, but that's a very subtle distinction.) I chose `withVar` to make it very clear that you're getting the same semantics as you would for a `var` temporary.
> 
>> public func withCopy<T>(_ item: T, update: (@noescape (inout T) throws -> Void)?) rethrows -> T {
>>   var this = item
>>   try update?(&this)
>>   return this
>> }
>> 
>> This function would be more clear and useful in conjunction with method cascades:
>> 
>> let bar = withCopy(foo)
>>   ..cascaded = “value"
>>   ..operations()
>>   ..onFoo()
> 
> Honestly, I'm not sure there's a coherent way to make method cascades work with your `withCopy` (or the `copy` function you mentioned upthread) at all.
> 
> Here's the problem. Suppose you have a property like this:
> 
> 	var array: [Int]
> 
> And then you write this:
> 
> 	array = [1, 2, 3]
> 	return array
> 			..remove(at: 1)
> 			..remove(at: 0)
> 
> I assume you think this should not only *return* `[3]`, but also *set* `array` to `[3]`. That's kind of implied by the fact that you think we need a `withCopy(array)` call to protect `array` from being affected by these calls.
> 
> But that means that in this version:
> 
> 	array = [1, 2, 3]
> 	return withCopy(array)
> 			..remove(at: 1)
> 			..remove(at: 0)
> 
> You are trying to call `mutating` methods on an *immutable* value, the return value of `withCopy`. Normally, the compiler would reject that.

You are right, there would need to be an exception for method cascades.  That might be a reasonable exception to make because we already know the temporary is not just the subject of mutation but also the result of the expression.  The method cascade just operates on the temporary in-place before being used in the surrounding expression or statement .

> 
> Perhaps you could say that method cascades operate on a copy if the receiver is immutable

This isn’t necessary with the previously mentioned exception for allowing mutating method cascades on temporaries.

> , but that makes code vague and its behavior subtle and easily changed by accident. For instance, if a property is `internal private(set)`, then moving a method cascade from code which can't see the setter to code which can would silently change the code from immutable to mutable. Similarly, adding the `private(set)` would not cause the code which previously modified it to produce an error; it would instead silently change to no longer mutate where it used to before. That's not acceptable behavior from a language feature.
> 
> About the only solution to this I can come up with is to make `withCopy` have an `inout` return. But this at best forms an attractive nuisance: If you use normal `mutating` method calls instead of method cascading, your changes are going to disappear into the ether. And depending on how `inout` returns are actually implemented, it could lead to worse misbehavior.
> 
> -- 
> Brent Royal-Gordon
> Architechies
> 



More information about the swift-evolution mailing list