Circling back to `with`

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.

I don't understand what you mean by "temporaryā€.

I mean a return value.

Are you saying that Swift should distinguish between things which could be mutable but don't happen to be (variables and subscripts), and things which cannot ever be mutable (functions and methods)?

No. Iā€™m saying that whether the return value temporary is immutable or mutable is something the language defines (not something the user declares anywhere). The language rules for method cascades could be defined such that they create a contextual type environment that allows the return value temporary to be mutable to allow the method cascade to operate on the temporary in place before it is used in the surrounding expression. This makes sense for method cascades because by definition they return the original value. It does not make sense for arbitrary operations because they will not return the original value.

Applying mutating method cascades to temporary value could be very handy. You have an expression that returns a value and mutate the result before storing it in a binding or returning it from your function. You can do this by binding the result to an explicit temporary and giving it a name. Allowing you to do it with method cascades on the implicit temporary is great syntactic sugar and a big argument for making them work well with value types, not just reference types IMO.

Will this distinction hold up when we get `inout`-returning functions?

What I describe above would mean return values behave like a `var` argument used to (before they were removed) when the return value has a method cascade attached to it. `inout` return values would work differently, writing back to the source storage.

Basically, what principled rule makes Swift treat this:

  immutableVariable..mutatingMethod()

And this:

  functionCall()..mutatingMethod()

Differently?

The rule is that it is safe to mutate a return value temporary copy in place without impacting the semantics of any code that relies on immutability. The mutation happens prior to any code getting a chance to do anything with the value. The mutation is really part of the expression itself and the method cascade provides a safe syntactic and semantic environment for allowing this mutation.

Or is the distinction between this:

  immutableVariable..mutatingMethod()

And this:

  mutableVariable..mutatingMethod()

And if so, how is that not going to lead to bugs when people move code around?

That distinction is absolutely critical. The former is not allowed. I am not suggesting allowing calling mutating methods on any `let` constants. What I am saying is that the semantics of method cascades allow them to be safely used with return value temporaries even when they mutate the temporary.

-Matthew

Ā·Ā·Ā·

On May 28, 2016, at 9:29 PM, Brent Royal-Gordon <brent@architechies.com> wrote:

--
Brent Royal-Gordon
Architechies

Can we bump this topic for Swift 5?

Similar to how KeyPaths have a language support by extending Any we could provide a with function to any type.

extension Any {
  @discardableResult
  public func with(update: (inout Self) throws -> Void) rethrows -> Self {
    var this = self
    try update(&this)
    return this
  }
}

If we had a distinction between AnyObject and (not existing) AnyValue we could extend those instead in favor of a inout free with function for classes and also excluding function reference types.

One advantage the extension of Any adds over the pitched global function is that you can use it in combination with optional chaining.

var text: String? = ...
label.text = text?.with { /* mutate non-optional string */ }
4 Likes

Yes please. I have something like this defined in all my projects and think itā€™s a good fit for the Standard Library.

2 Likes

I would like both method cascading and with.

Introducing with offers use-cases for both value and reference types.

  • For let value types, it allows you to copy and modify, returning a new derived version of the previous version.
  • For reference types, it's most useful in Cocoa initialization, enabling you to create a scoped set-up that is visually (syntactically) differentiated from the code that surrounds it. This enhances both reading and maintenance and avoids dense blocks of API set-up one following another.
6 Likes

Obligatory bumpification for Adrian @DevAndArtist -- E

Introducing with to the Standard Library

Introducing with to the Standard Library

Introduction

This proposal introduces a with function to the standard library. This function
simplifies the initialization of objects and modification of value types.

Swift-evolution thread:
What about a VBA style with Statement?

Motivation

When setting up or modifying an instance, developers sometimes use an
immediately-called closure to introduce a short alias for the instance
and group the modification code together. For example, they may
initialize and customize a Cocoa object:

let questionLabel: UILabel = {
    $0.textAlignment = .Center
    $0.font = UIFont(name: "DnealianManuscript", size: 72)
    $0.text = questionText
    $0.numberOfLines = 0
    mainView.addSubview($0)
    return $0
}(UILabel())

Or they may duplicate and modify a constant value-typed instance:

let john = Person(name: "John", favoriteColor: .blueColor())
let jane: Person = { (var copy) in
    copy.name = "Jane"
    return copy
}(john)

This technique has many drawbacks:

  • The compiler cannot infer the return type.
  • You must explicitly return the modified instance.
  • The instance being used comes after, not before, the code using it.

Nevertheless, developers have created many variations on this theme,
because they are drawn to its benefits:

  • The short, temporary name reduces noise compared to repeating a
    variable name like questionLabel.
  • The block groups together the initialization code.
  • The scope of mutability is limited.

SE-0003, which removes var parameters,
will make this situation even worse by requiring a second line of
boilerplate for value types. And yet developers will probably keep
using these sorts of tricks.

Fundamentally, this is a very simple and common pattern: creating a
temporary mutable variable confined to a short scope, whose value will
later be used immutably in a wider scope. Moreover, this pattern
shortens the scopes of mutable variables, so it is something we should
encourage. We believe it's worth codifying in the standard library.

Proposed Solution

We propose introducing a function with the following simplified signature:

func with<T>(_: T, update: (inout T -> Void)) -> T

with assigns the value to a new
variable, passes that variable as a parameter to the closure, and
then returns the potentially modified variable. That means:

  • When used with value types, the closure can modify a copy of the original
    value.
  • When used with reference types, the closure can substitute a different
    instance for the original, perhaps by calling copy() or some non-Cocoa
    equivalent.

The closure does not actually have to modify the parameter; it can
merely use it, or (for a reference type) modify the object without
changing the reference.

Examples

Initializing a Cocoa Object

Before:

let questionLabel: UILabel = {
    $0.textAlignment = .Center
    $0.font = UIFont(name: "DnealianManuscript", size: 72)
    $0.text = questionText
    $0.numberOfLines = 0
    mainView.addSubview($0)
    return $0
}(UILabel())

After:

let questionLabel = with(UILabel()) {
    $0.textAlignment = .Center
    $0.font = UIFont(name: "DnealianManuscript", size: 72)
    $0.text = questionText
    $0.numberOfLines = 0
    mainView.addSubview($0)
}

Using with here moves the UILabel() initialization to the top,
allows the type of questionLabel to be inferred, and removes the
return statement.

Copying and Modifying a Constant

Before (without var parameter):

let john = Person(name: "John", favoriteColor: .blueColor())
let jane: Person = {
    var copy = $0
    copy.name = "Jane"
    return copy
}(john)

After:

let john = Person(name: "John", favoriteColor: .blueColor())
let jane = with(john) {
    $0.name = "Jane"
}

In addition to the aforementioned benefits, with removes the
var copy line.

Treating a Mutable Method As a Copy-and-Return Method

You would like to write this:

let fewerFoos = foos.removing(at: i)

But there is only a remove(at:) mutating method. Using with, you can write:

let fewerFoos = with(foos) { $0.remove(at: i) }

Avoiding Mutable Shadowing

The standard library includes an operator
for concatenating two RangeReplaceableCollections with this implementation:

var lhs = lhs
// FIXME: what if lhs is a reference type?  This will mutate it.
lhs.reserveCapacity(lhs.count + numericCast(rhs.count))
lhs.append(contentsOf: rhs)
return lhs

Using with, you can eliminate the shadowing of lhs:

// FIXME: what if lhs is a reference type?  This will mutate it.
return with(lhs) {
  $0.reserveCapacity($0.count + numericCast(rhs.count))
  $0.append(contentsOf: rhs)
}

It's important to note that with does not resolve the "FIXME" comment.
Like the var lhs = lhs in the original code, with only copies value
types, not reference types. If RangeReplaceableCollection included a
Foundation-like copy() method that was guaranteed to return a copy
even if it was a reference type, with would work nicely with that
solution:

return with(lhs.copy()) {
  $0.reserveCapacity($0.count + numericCast(rhs.count))
  $0.append(contentsOf: rhs)
}

Inspecting an Intermediate Value

Suppose you want to inspect a value in the middle of a long method chain.
For instance, you're not sure this is retrieving the type of cell you expect:

let view = tableView.cellForRow(at: indexPath)?.contentView.withTag(42)

Currently, you would need to either split the statement in two so you
could capture the return value of cellForRow(at:) in a constant, or
insert a very clunky immediate-closure call in the middle of the
statement. Using with, you can stay close to the original expression:

let view = with(tableView.cellForRow(at: indexPath)) { print($0) }?.contentView.withTag(42)

Because the closure doesn't alter $0, the cell passes through the
with call unaltered, so it can be used by the rest of the method
chain.

Detailed Design

We propose adding the following free function to the standard library:

/// Returns `item` after calling `update` to inspect and possibly 
/// modify it.
/// 
/// If `T` is a value type, `update` uses an independent copy 
/// of `item`. If `T` is a reference type, `update` uses the 
/// same instance passed in, but it can substitute a different 
/// instance by setting its parameter to a new value.
@discardableResult
public func with<T>(_ item: T, update: @noescape (inout T) throws -> Void) rethrows -> T {
  var this = item
  try update(&this)
  return this
}

@discardableResult permits the use of with(_:update:) to create a scoped
temporary copy of the value with a shorter name.

Impact on Existing Code

This proposal is purely additive and has no impact on existing code.

Alternatives Considered

Doing nothing: with is a mere convenience; any code using it could be
written another way.
If rejected, users could continue to write code using the longhand form,
the various closure-based techniques, or homegrown versions of with.

Using method syntax: Some list members preferred a syntax
that looked more like a method call with a trailing closure:

let questionLabel = UILabel().with {
    $0.textAlignment = .Center
    $0.font = UIFont(name: "DnealianManuscript", size: 72)
    $0.numberOfLines = 0
    addSubview($0)
}

This would require a more drastic solution as it's not possible to add
methods to all Swift types. Nor does it match the existing
design of functions like withExtendedLifetime(_:_:), withUnsafePointer(_:_:),
and reflect(_:).

Adding self rebinding: Some list members wanted a way to bind
self to the passed argument, so that they can use implicit self to
eliminate $0.:

let supView = self
let questionLabel = with(UILabel()) { 
    self in
    textAlignment = .Center
    font = UIFont(name: "DnealianManuscript", size: 72)
    numberOfLines = 0
    supView.addSubview(self)
}

We do not believe this is practical to propose in the Swift 3 timeframe, and
we believe with would work well with this feature if it were added later.

Adding method cascades: A competing proposal was to introduce a way to
use several methods or properties on the same instance; Dart and Smalltalk
have features of this kind.

let questionLabel = UILabel()
    ..textAlignment = .Center
    ..font = UIFont(name: "DnealianManuscript", size: 72)
    ..numberOfLines = 0
addSubview(questionLabel)

Like rebinding self, we do not believe method cascades are practical
for the Swift 3 timeframe. We also believe that many of with's use
cases would not be subsumed by method cascades even if they were added.

2 Likes

@Erica_Sadun thank you Erica. What do you think about two other versions of the function.

  1. Extension on Any as presented above (it will work nicely with optional chaining but is similar to .dynamicType which we want to avoid)

  2. A left assignment operator instead of the with function:

precedencegroup LeftAssignmentPrecedence {
  associativity: left
  higherThan: BitwiseShiftPrecedence
  assignment: true
}

infix operator <- : LeftAssignmentPrecedence

@discardableResult
public func <- <T>(lhs: T, rhs: (inout T) throws -> Void) rethrows -> T {
  var value = lhs
  try rhs(&value)
  return value
}

According to this reply the operator can work with optional chaining.

Small example of it's usage (click to unfold)
topGradientView <- {
  $0.prepareForAutoLayout()
  $0.attach(to: pickerHost)
  NSLayoutConstraint.activate(
    $0.topAnchor.constraint(equalTo: pickerHost.topAnchor)
      <- { $0.priority = .defaultHigh },
    $0.bottomAnchor.constraint(equalTo: backgroundView.topAnchor)
      <- { $0.priority = .defaultLow },
    $0.leadingAnchor.constraint(equalTo: pickerHost.leadingAnchor),
    $0.trailingAnchor.constraint(equalTo: pickerHost.trailingAnchor),
    $0.heightAnchor.constraint(greaterThanOrEqualToConstant: 0),
    $0.heightAnchor.constraint(lessThanOrEqualToConstant: 30)
  )
}

I see the utility of this proposal, but I'm not sold on the name. In particular, you say that the design is intended to "match the existing
design of functions like withExtendedLifetime(_:_:), withUnsafePointer(_:_:)", but I'm not sure the comparison is apt. The other withā€¦ functions take a parameter, perform some operation on it, and don't return the value provided. This is different than with(_:update) as it's currently pitched, which returns the first parameter or a copy. Especially given the fact that this function is @discardableResult, it seems like it might be one of the "does something with memory management I guess? Maybe if I'm fiddling with object lifetimes I'll use this" but otherwise useless functions, since the following code is legal as far as I can tell:

// To a beginner, how is this:
let label = UILabel()
with(label) {
	label.text = "Hello, world!"
}
// different from this:
let label = UILabel()
label.text = "Hello, world!"

Speaking of "returns the first parameter or a copy", what exactly is its semantics with regards value types and reference types? I was thinking of renaming it to update(_:changes:) or withCopy(of:update:) to make it more clear as to what it's doing, but obviously these names only apply to whether we're updating something or whether we're creating a copy and mutating that.

1 Like

The don't return the value provided, but they do return the value returned from the closure, so there is some similarity.

i like this idea and i think it would be v useful

This is a difference as far as I'm concerned. There's no precedent for functions starting with with returning their first parameter; as far as I'm aware they always return whatever the user returns in the closure (if I'm overlooking more than one or two functions, please let me know!). Keeping this in mind, why wouldn't users try to return the value they're operating on from the closure?

1 Like

I'd rather we pass on this one, to get the version with self rebinding.

Is the self rebinding version practical for Swift 4?

I'm not sure about the specifics but I would definitely be happy to have something like this in Swift. In Kotlin they have many "scoping functions" and they are really used while developing.

1 Like

If we want something like Kotlin with, then IMO it should be a first-class keyword that rebinds self, such that member access can omit even the member lookup dot:

var x = [1, 2, 3]
with x {
  append(contentsOf: [4, 5, 6])
}
print(x) // [1, 2, 3, 4, 5, 6]

Anything less seems a half-measure.

14 Likes

Interesting idea. Do you imagine it more like if x { ... } or a first class feature that uses a closure which can also have a reference capturing list?

Would the following be allowed?

var x = [1, 2, 3]
let y = with x {
  append(contentsOf: [4, 5, 6])
}

let v: [Int]? = nil
let w: [Int]? = with v? {
  /* executes only when non-optional */
}

Why would you use that over plain member lookup with optional chaining?

Here are two quick examples using the <- operator from above.

class ViewController : UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    view? <- { // view is `UIView!`, alternative is `(view as UIView) <- { ... }`
      $0.backgroudColor = .red
      $0.tag = 42
    }
  }
}

@UIApplicationMain
final class AppDelegate : UIResponder, UIApplicationDelegate {

  var window: UIWindow?

  func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions:
      [UIApplicationLaunchOptionsKey: Any]?
  ) -> Bool {
    window = UIWindow() <- {
      $0.frame = UIScreen.main.bounds
      $0.rootViewController = ...
      $0.makeKeyAndVisible()
    }
    return true
  }
}

Iā€™m afraid I donā€™t understand. Neither of those examples require with to be an expression instead of a statement.

Let me put it in different words.

  • Does the first class with return a value like the <- operator function or not?

  • Does it support optional chaining so that between {} the value is non-optional?

+1 on the general idea. I've used with in other languages and really liked it.

I do like the convention of automatically returning self or an identically typed entity. And the proposed implementation of that here is quite clever and transparent.

Overall, though, I'm not so sure about this specific proposal. Encouraging developers to replace variable names with $0 doesn't seem like an improvement, and the proposal punts on the most valuable part of with, namely, self rebinding. That's the thing that would really clean up code and add expressive possibilities.

It's kind of implicit in this proposal that doing the right thing would be too hard or too controversial, so we should just content ourselves with putting in some kind of half-baked patch to ease a few common pain points. I'm all for clever hacks, but why not just let people copy the implementation into their own projects? It seems like too much of a stopgap solution to be worthy of being blessed as official Swift stdlib API.

Maybe it really is too hard to implement self rebinding anywhere in the foreseeable future. It appears (to my unsophisticated eye) analogous to defining and then calling a type-specific extension, but currently that is not possible within the context of a function. I don't know the implementation history behind that restriction; perhaps there's a very good reason why it can't be done. If so, oh well; Swift doesn't have with.

This would be really nice. I'd suggest that with should be infix so it reads more naturally.

let translated = point with {
    x += 10
}