[Pitch] `With` functions in the standard library

I think there's a real problem to solve here for more concise code. But I don't like the solution, I find it counter-intuitive having to reach into a global function called with. I'd much rather have a function called with that I can call on an instance of any type.

I actually defined a Withable protocol in my open-source library HandySwift here which looks like this:

public protocol Withable { /* no requirements */ }

extension Withable {
  public func with(_ config: (inout Self) throws -> Void) rethrows -> Self {
    var copy = self
    try config(&copy)
    return copy
  }
}

Usage is like this, which I find much more intuitive:

let components = URLComponents().with { components in
  components.path = "foo"
  components.password = "bar"
}

The only downside is, you have to make any type you want to call it on to conform to Withable like so:

extension URLComponents: Withable {}

It's easy but inconvenient. If baked into the language, the conformance could become unnecessary.

2 Likes

It's the only downside, but it's a very major one that ultimately lead me and my team to abandon this approach, and use an operator instead.

2 Likes

I mentioned this in the future directions.

1 Like

I like the general idea but not a fan of the name with. Even objective-c name translation rules have a rule about that particular function name and the like, calling it "absurdly vacuous":

  • Never transform a name into "get", "set", "with", "for", or "using", just to avoid creating absurdly vacuous names.

I don't really think that applies here. With is a vacuous name, but the function is also vacuous; it does a very simple thing implied by the name with. A longer name seems pointless and self-defeating.

withMutableCopy(of: 5) {
 $0 += 2
}

It does seem like a lot of people aren't happy with the name with. A few names have been proposed in this thread.

  • with
  • modify
  • modifying
  • mutated
  • modified
  • mutate
  • withMutableCopy

In addition, people have proposed various alternate solutions to this problem.

  • A global function
  • A magical extension on Any
  • An operator paired with dynamic member lookup and key paths
  • The builder pattern
  • An operator paired with a closure

It seems like most of the people in this thread can agree that this functionality could be useful, but not exactly how to do it. I picked a free function because, since it was a small change, it seemed more likely to pass through Swift Evolution. And I picked the name with because of its existing use in the standard library. However, all of these proposed solutions have merit; should we add more than one to the standard library? Otherwise, which one should we pick?

1 Like

I kinda think . would be a better operator and allowing the closure that follows to start its lookup by default on the object it is being run 'on'.

UIView().{
     frame = .init(x: 0, y: 0, width: 100, height: 100)
     backgroundColor = .blue
}
5 Likes

What was the reason using . would’t be possible? Does it cause some sort of unresolvable interference?

I understand it can’t be used as a custom operator because it’s reserved , but I think ultimately this should’t just be a function in the standard library but rather a whole new language feature.

Of course I haven’t put enough thought into it to realize any interference with current syntax.

1 Like

Not exactly. But with an operator, you can use a named closure.

URLComponents()…mutateURLComponents

func mutateURLComponents(_ components: inout URLComponents) {
  components.path = "foo"
  components.password = "bar"
}

Just . would require parentheses, because it's the same syntax that properties or methods-as-closures use. I.e.

URLComponents().(mutateURLComponents)

It was mentioned a few times but nonetheless I would like to emphasize the following:

You would also like to be able to change a value by appending something to the according expression.

Also you would like to do it in a way that cannot be confused with an if block, e.g. when using applying(…) instead of the &> operator:

if let x = myValue?.getOtherValueFromValue().applying({ otherValue in
    otherValue.property1 = propertyValue
}) {
   …
}

The with syntax would not allow an elegant chaining, and I am not sure about the block of the &> operator in the context of if.

As general remark I would be cautious to not only use one simple application as the only use case. It should be considered how it composes in different, also more complex situations.

1 Like

If extensions on Copyable are allowed, we could declare it that way; since the function involves a copy it wouldn't make sense for a non-copyable type.

extension Copyable {
  public func with(transform: (inout Self) throws -> Void) rethrows -> Self {
    var copy = self
    transform(&copy)
    return copy
  }
}
1 Like

Copyable is spelled Any, and extensions are not allowed.

I'm using this as just an overload on &&.

public func &&<T>(_ value: T, modifications: (inout T) throws -> Void) rethrows -> T {
    var value = value
    try modifications(&value)
    return value
}

and in use:

let spellOutNumberFormatter = NumberFormatter() && {
    $0.numberStyle = .spellOut
}
2 Likes

Adding an overload to such a commonly used operator might cause regressions in compile time, but it does remove the need to add a new operator.

1 Like

I see.

I think what I am suggesting is that we just introduce new syntax altogether. Something like . followed by a closure.

So instead of having to even define any of this:

extension UIView {
     @discardableResult func with(_ closure: (UIView) -> ()) {closure(self); return self}
}

let view = UIView().with({$0.backgroundColor = .red})

you could just write

let view = UIView().{
   backgroundColor = .red
}

Maybe .{ automatically calls some function called something like with(_ closure:) if you want to provide additional customization. Otherwise the compiler just synthesizes it for you.

Would that solve a lot of this?

The .{ syntax has some drawbacks compared to with:

  • The .{ syntax has to be implemented in the compiler, which makes it harder to understand than the with function. You can ask Xcode (or, I assume, other IDEs) to navigate to the with function's documentation or definition in a way that is not (currently) possible with syntax like .{. Furthermore, the with function is a short function, written in Swift, and uses only very old, mainstream parts of the language. IMHO, it should not take long for any new Swift programmer to understand all of the concepts involved and be able to understand the function's definition completely. Syntax built in to the compiler cannot be learned this way.

  • The .{ syntax, as suggested, either replaces self in the scope chain, or inserts a new scope in the scope chain. I'm not sure which is being suggested. Either way, it makes it more difficult to understand where any (leading) identifier used in the block is defined. An IDE can help here, but not needing to ask the IDE for help is even better. If it inserts a new scope in the scope chain, then it is (ironically) much like the deprecated, widely criticized JavaScript with statement.

  • The .{ syntax, as suggested, makes the properties of the receiver shadow names that would otherwise be in scope. Here's an example from one of my projects. I use the with function like this:

    public init(now: Date, calendar: Calendar) {
        ...
    
        self.yesterdayFormatter = with (DateFormatter()) {
            $0.locale = calendar.locale
            $0.calendar = calendar
            $0.formattingContext = .standalone
            $0.dateStyle = .none
            $0.timeStyle = .none
            $0.doesRelativeDateFormatting = false
            $0.setLocalizedDateFormatFromTemplate("hma")
        }
    

    But with the value.{ block } syntax, I cannot access init's calendar parameter inside the block:

    public init(now: Date, calendar: Calendar) {
        ...
    
        self.yesterdayFormatter = DateFormatter().{
            locale = calendar.locale          // <-- THIS DOESN'T WORK
            calendar = calendar               // <-- THIS DOESN'T WORK
            formattingContext = .standalone
            dateStyle = .none
            timeStyle = .none
            doesRelativeDateFormatting = false
            setLocalizedDateFormatFromTemplate("hma")
        }
    

    I would be forced to give the calendar parameter a different name to make it accessible inside the block:

    public init(now: Date, calendar cal: Calendar) {
        ...
    
        self.yesterdayFormatter = DateFormatter().{
            locale = cal.locale
            calendar = cal
            formattingContext = .standalone
            dateStyle = .none
            timeStyle = .none
            doesRelativeDateFormatting = false
            setLocalizedDateFormatFromTemplate("hma")
        }
    
  • You can pass a function that's not a literal to the with function. That is, if you already have a function mutations with signature (inout Value) -> Void, you can say with(x, update: mutations). There's no obvious way to do this with the .{ syntax; you can only use a closure literal. FWIW, of the dozens of calls to with I found in a few of my projects, only one passes a non-literal function, so this is a pretty minor complaint.

1 Like

I addressed this in my last post. This thread seems to be oversaturated and will therefore very sadly not go anywhere. Unsubscribing. :crying_cat_face::melting_face:

I think it would be pretty clear that at least left hand side identifiers are properties/functions of the object on which .{ is called since that is precisely what the purpose of the syntax is for.

In the same way that self.something is used to disambiguate similar situations, $0.something would be required to disambiguate in those conditions.

Also, all major coding fonts render this a right pointing triangle, here are some:

Language steering group guidance

As various ideas related to this topic have percolated since at least 2016, the language steering group discussed if any guidance might help the discussion to coalesce.

Consensus among the steering group is that we wouldn't entertain something like a non-semantic Withable protocol to circumvent the restriction against extending Any. While it's true that arbitrary members can't be added to all types via an extension in the standard library, a restriction we're comfortable maintaining, there is precedent for simulating such a member via built-in "magic": besides self, we've added the keyPath subscript in SE-0161. A with feature—if adopted—would likely become a part of idiomatic Swift usage; so, if a convincing case is made that the best design for this feature would have it spelled as a member, it'd be justifiable to take a similar implementation approach. We'd prefer a fair consideration of the merits of a free function versus a member here (whether spelled with or some other name) not to be clouded by this implementation detail.

As a group, we're not yet sure that we'd all reject an operator spelling off the bat, but a very convincing argument would have to be made for why it's clearly superior to alternative designs. Our concerns are several: (1) Existing standard library operators are largely well-known from C-family languages (or, in the case of &-prefixed operators, are deliberately more esoteric "flavors" of well-known operators). From that vantage, it's hard to imagine that any operator for this feature would have the readability of a spelled-out word. Indeed, lately, the direction of the language has favored pronounceable words (repeat each T) over punctuation (...T). (2) Any succinct enough sigil could start to look like line noise with broad adoption. Part of a convincing argument would, in our view, include a robust selection of realistic code examples that demonstrate how well a proposed operator spelling would perform as compared to a free function or method. (3) The addition of a single operator can immediately invite consideration of filling out a whole family of related operators, and we don't think that would be desirable here.

Could this be a language feature—a with statement, in other words? The steering group ultimately considered the implications of using compiler "magic" beyond simulating extension Any to be undesirable. In particular, we think any feature proposed should take a bona fide closure: we'd like to avoid inventing language rules that are unique in Swift to create some DSL-like block here, such as rebinding self as some of Kotlin's scope functions do. We understand that at least a contingent of developers who've used such features in other languages find them to be confusing sharp edges.

Lastly, a variation of this feature that takes its argument inout has also been proposed. We'd avoid this sort of design so as not to expose idiomatic ways to "hide" mutations in a chain of operations. The situation is analogous to assignment operators, which do not return a value in Swift (unlike most C-family languages). Moreover, any inout version of the proposed feature would require the user already to have a mutable binding "on hand" that they could mutate directly, and we expect inout bindings to address use cases where one might want to repeatedly mutate some deeply nested member.

— Xiaodi Wu, on behalf of the language steering group

14 Likes

So, basically...

// free function
with(NumberFormatter()) { formatter in
  formatter.numberStyle = .spellOut
}

// magical method
NumberFormatter().with { formatter in 
  formatter.numberStyle = .spellOut
}

// operator | not recommended without a convincing argument
NumberFormatter() &> { formatter in
  formatter.numberStyle = .spellOut
}

// inout version | not recommended
var formatter = NumberFormatter()
with(&formatter) { formatter in
  formatter.numberStyle = .spellOut
}

// statement | undesirable
NumberFormatter().{
  numberStyle = .spellOut
}
2 Likes