In-place map for MutableCollection

In the adding toggle to bool thread, some good examples came up for why that is useful, and many of the same examples apply to this inout version as well. A big difference is that it allows you to not repeat your self. Here's a contrived example:

struct Favorite {
    var name: String
    var visible: Bool
}
var favorites: [String: [Favorite]] = [
    "john": [Favorite(name: "swift.org", visible: true), Favorite(name: "idris-lang.org", visible: false)],
    "edward": [Favorite(name: "haskell.org", visible: true)],
]

Let's say that we want to toggle all visible properties for the "john" key. Our new mapInPlace makes this quite easy.

favorites["john"]?.mapInPlace { $0.visible.toggle() }

The main difference in expressivity is that the inout allows you to both get and set a value. The way it composes is especially interesting: For example, in the closure body, $0 is inout, but because visible is a var, $0.visible is also inout. Likewise, the ? operator will also maintain the inout-ness of a value. (Technically, these are called l-values, not inouts).

This style allows you to reach "deep" into a data structure, and mutate it. In a functional style, mutating something that is slightly deeper into the data structure becomes very cumbersome, unless you use something like lenses. This deep mutation is something that comes up often in my code when dealing with data. We could also consider adding lenses to the Standard Library, but Swift already has strong support for inout throughout the language.

If you have a closure (A) -> A, you can always recover (inout A) -> () by prepending it with $0 = . If something like with would be in the Standard Library, the inout version could also be recovered from the functional version.

3 Likes

It's sometime hard to be understood. Of course your pitched method is useful. You don't need to be defensive, because no one is telling the opposite! Now please if you have five seconds, look at the other variant that does not mutate element, just in case you'd find it useful, too:

If you have a closure (A) -> A, you can always recover (inout A) -> () by prepending it with $0 =.

Meh. This sentence is short-sighted. It puts the focus on one side of the coin without telling why the other side of the coin does not deserve the slightest attention.

Now this is your pitch. I've tried hard enough to make it less focused on your use case. I won't repeat and repeat again :-)

I understand what you're saying. I'm sorry if I come off to you as defensive, I'm just trying to show examples of where I think inout is useful, and how it is different from (A) -> A.

Like I said, you can also recover the inout variant by using with. If we don't regard performance (something the compiler engineers should weigh in about), it means that it doesn't matter whether mapInPlace would use inout or not in the closure. I do have a preference for the inout version of the closure, but to me, that's not set in stone. I'm actually very happy to be convinced otherwise! Instead of calling each other defensive / not listening, let's focus on coming up with more examples of how the API differs at the site of usage.

Oh, and with regard to adding both variants: I think that's too much of an ask, whichever one we choose could easily be recovered from the other. And unless we give them distinct names, it might hurt type-inferencing.

I've been thinking about this a bit more. I might still misunderstand you. You're asking me to look at the other variant (with the (A) -> A) parameter. I did look at it (and thought about it a lot, even before the pitch). Do I understand correctly that you would like me to give more examples of when that is useful? Or do you want me to acknowledge that I have read what you wrote? Or do you want the proposal to include both variants? I'm not here to get into a fight, I'm honestly asking, and trying to understand your needs :slight_smile: .

Fair questions, Chris, thanks :-) All right:

I think that the eventual proposal derived from this pitch should:

  • Give at least one example of a.mapInPlace { $0 = ... }, very early in the proposal, in the motivation section.
  • Tell that a variant that takes a (A) -> A closure is not included in the proposal in order to 1. keep the proposal short (a single added method), and 2. focus on the method that provides the most benefits in terms of memory usage.

The reasons why I think it would improve your proposal is because { $0 = ... } is ergonomically sub-par, but the only available technique, in your pitch, for alterations that aren't supported by a mutating method (and there are many). I gave two trivial examples (integer multiplication, and string uppercasing), but there are tons of other alterations that are not supported by mutating methods. People should be prepared for { $0 = ... }.

Your current pitch shines because its examples use elements that have mutating methods. But there is a little dark corner, and I wouldn't like that it was hidden from the community sight during the review phase. Maybe the community would find a way to address this shortcoming. At the same time, I'm sure the community will not kill the proposal because of this shortcoming: you won't jeopardize the success of your excellent pitch.

Thank you, that clarifies a bit.

There is a third reason, besides keeping it short and efficiency, which I mentioned a few times: it allows you to write code differently. Look at the examples in the posts above to see. This is (for me) an even bigger reason to write it using inout.

This's very common scenario used in my apps. I'm two hands for this improvement.

updateAll is very similar to a C++ function called std::transform but it allows you to specify a different destination. To apply it in place, you can then use the destination as the source:

extension Collection {
    func transform<D: MutableCollection>(
        to destination: inout D,
        transform: (Element) -> D.Element
    ) {
        var i = startIndex
        var j = destination.startIndex
        
        while i != endIndex, j != destination.endIndex {
            destination[j] = transform(self[i])
            
            i = self.index(after: i)
            j = destination.index(after: j)
        }
    }
}

The source could be adapted to be a Sequence as well.

Just stumbled upon this thread having just encountered the need for mutating map.

Does anyone know the status on that very nice-to-have feature ?

1 Like