In-place map for MutableCollection

In Sequence/Collection Enhancements, the possibility of an in-place map was discussed. I found myself needing this today, and I figured (similar to toggle on Bool) it might be a useful addition to the standard library. Here's my implementation:

extension MutableCollection {
    mutating func mapInPlace(_ x: (inout Element) -> ()) {
        for i in indices {
            x(&self[i])
        }
    }
}

This is handy when you want to mutate some type in-place, rather than creating an entirely new value. Consider the following boiled-down example from Point Free:

struct Food {
    var name: String
}

struct User {
    var foods: [Food]
    var name: String
}

If we want to change all the food elements a user has (for example, by appending: "& Salad"), we could write it with map:

extension User {
    mutating func healthierMap() {
        foods = foods.map { x in
            var copy = x
            copy.name += "& Salad"
            return copy
        }
    }
}

The copy is annoying, distracts from the essence of the code, and is inefficient. Furthermore, the entire array of foods is copied.

Using a for loop is much shorter, but requires us to think about indices:

extension User {
    mutating func healthierFor() {
        for i in foods.indices {
            foods[i].name += "& Salad"
        }
    }
}

Finally, the solution using mapInPlace is the shortest, and as efficient as the for solution:

extension User {
    mutating func healthier() {
        foods.mapInPlace { $0.name += " & Salad" }
    }
}

(I don't know if mapInPlace is the best name yet, very much open to bike-shedding).

18 Likes

Looks very handy and Swifty! Thanks for suggesting this.

As for the name:

Wouldn't mapped be the mutating version of map?

3 Likes

Sounds awesome.

As per the API naming guidelines, the mutating version of map, which is not an ed/ing noun, would be formMap. Other than that, +1 to the idea.

1 Like

No, "map" would be the mutating version of "mapped", but we've decided to name the non-mutating version "map" as a term of art.

5 Likes

Yes please.

+1 to the InPlace suffix for terms of art like map that break Swift’s standard “verb tense indicates mutation” naming conventions. Despite extensive bikeshedding in the past on swift-evo, this remains the clearest, best option.

The entire stdlib sorely needs an audit for consistent availability of mutating and non-mutating counterparts. I was heartened to see the acceptance of removeAll(where:), though the resulting inconsistency between filter and removeAll is unfortunate. The library ought to have filterInPlace and removingAll; mutating / non-mutating and true selects / true removes should be orthogonal concerns.

Using the stdlib as it stands feels like navigating a maze.

5 Likes

This is debatable. The API guidelines specify:

When the operation is naturally described by a noun, use the noun for the nonmutating method and apply the “form” prefix to name its mutating counterpart.

This convention addressed union and intersection, which lack non-awkward verb forms in English. However, “map,” “filter,” and “remove” can all be used as verbs.

Contra extending the confusing, easily misread formFoo convention (looks like “from foo”), consider:

  • filterInPlace
  • mapInPlace

…versus:

  • formFilter
  • formMap

The latter both imply that we are creating a filter or a map, not applying one. Forcing these words that can be verbs to instead function as nouns makes them misleading.

4 Likes

One difference: map can change the element type, but this version can’t. It’s sort of implied by in-place that the type doesn’t change, yet still it feels a little wrong to me to call it map.

8 Likes

Sounds like you just don't like the "formFoo" convention--which is fine, but it's settled and we should stick with it for consistency.

What does it mean to "create a map, not apply one"? Is that a realistic point of confusion?

Good point. It's really a mutating forEach, isn't it?

4 Likes

No, it is is only settled for words which are “naturally described by a noun.”

The API guidelines are currently ambiguous as to whether that means “words that may function as nouns” or “words that must function as nouns to avoid awkwardness.” I argue, based on the swift-evo discussion that lead to that guideline, that the latter interpretation is the correct one.

If the former interpretation were correct, then we would also use the method names formPartition, formUpdate, formSubtraction, formMerge, and arguably formSort, formInsertion, and even formSuffix instead of dropFirst, since all those words can function as nouns.

The ridiculousness of all those names suggests the “may” reading of that convention is not the right one.

Again, recall that the “form-” convention was only invented to address the awkwardness of names like unioningInPlace.

Yes. Consider:

  • In ML-family languages, which IMO half-includes Swift, a “map” can refer to a curried call to map which already has its predicate and accepts a collection.
  • In some languages, “map” as a noun is a synonym for “dictionary.”

Or more simply, just compare the naming alternatives I posted above with the eyes of somebody who didn’t follow the discussion that lead to formIntersection.

2 Likes

I have implemented this in the past, under the name mutateAll.

4 Likes

This was my first thought.

This is similar to the discussion about reduce(into:) (Reduce with inout - #26 by Joe_Groff) - you could argue for another function also called forEach with an inout parameter, but that would probably be too much stress on the type-checker.

I wonder if we could do something like:

extension MutableCollection {

  func forEach(_ perform: (inout Element)->Void)) { ... }

  @available(*, unavailable)
  func forEach(_ perform: (Element)->Void)) { ... }
}
1 Like

Since we're moving in the direction of providing both mutating and non-mutating versions of the core Sequence/Collection methods, do you think it's worth including multiple in one proposal?

filterInPlace has already been brought up and seems like a natural parallel to the proposed method—I suspect that if one is supported, the other likely would be too.

It's worth mentioning the in-place equivalents for the other common functional API as well:

  • a flatten method that performs the effect of self = flatMap { $0 } in place (can be implemented today as an extension where Element: Sequence)
  • a compact (?) method that performs the effect of self = compactMap { $0 } in place (cannot be implemented without parameterized extensions or an additional protocol for Optional as far as I can tell)
1 Like

This implementation falls into a performance trap, as described in the documentation for indices. Essentially, indices might hold a reference to self, in which case mutating self while iterating over indices creates an extra copy of self. In the standard library, this occurs for Dictionary.Values.

The recommended approach is to manually advance an index.

extension MutableCollection {
    mutating func mutateAll(_ f: (inout Element) throws -> ()) rethrows {
        var i = startIndex
        while i != endIndex {
            try f(&self[i])
            formIndex(after: &i)
        }
    }
}

Also, if you want to directly pass in a function (eg. sin) rather than a closure, then the parameter should not be inout:

extension MutableCollection {
    mutating func mutateAll(_ f: (Element) throws -> Element) rethrows {
        var i = startIndex
        while i != endIndex {
            self[i] = try f(self[i])
            formIndex(after: &i)
        }
    }
}

The difference is in how you call it:

x.mutateAll{ $0 *= $0 }    // inout
x.mutateAll{ $0 * $0 }     // non-inout
x.mutateAll(sin)           // non-inout
11 Likes

I‘d prefer mutateEach instead to kind of mimic the naming of forEach which is the non-mutating part of it.

10 Likes

Again, sounds like the underlying motivation is that you just don't like names with form and think they're "ridiculous."

There is no need to parse the wording so finely. The API guideline write-up leaves much to be desired because gerunds are nouns in the first place. Simply, there are two and only two conventions for mutating vs. non-mutating method names: the "verb vs. noun" (ed/ing) convention, and the "form" convention when names can't use the first convention. When it comes to map, it's an exception to the "verb vs. noun" (ed/ing) convention, so the next one is the "form" convention.

Do you really think that Swift users are going to see formMap and wonder whether it's "a curried call to map which already has its predicate" or a synonym for a dictionary? Or, again, do you just not like names with "form"?

The last two examples are truly just map; not having to assign to self is a pretty meager win. The reason that an in-place version is so desirable is precisely to enable the inout functionality used in the first example, and I'd argue that's the only one we should be enabling.

@DevAndArtist's suggestion mutateEach sounds pretty good.

Me too, or: withEach.

I agree that the method is too far from map (since it's only "mapping" back onto itself, with the same element type) for it to be named mapInPlace or formMap.

If we are reading the guidelines clause by clause, that is not the rule the guidelines actually state. They draw the distinction in terms of words that are more naturally nouns.

However, this legalistic nit-picking over the correct way to dutifully follow the letter of the guidelines completely misses the substance of my argument, which is:

  1. the considerations that originally lead to the name formUnion do not apply here, and

  2. formFilter and formMap fail to meet the first of the “Fundamentals” in the API design guidelines, namely clarity at the point of use, regardless of what particular rules say. We should interpret the particular API guidelines laws in a manner consistent with the API guidelines constitution, as it were.

I really think Swift users seeing formMap or formFilter would be mightily confused. I have no idea what they would think. You asked for examples of other possible interpretations, and I provided them.

I don’t like these particular names with form, and would invite others in this discussion beside Xiaodi to weigh in whether they think they pass the smell test.

Again, the choice is:

  • filterInPlace
  • mapInPlace *

…versus:

  • formFilter
  • formMap

* (unless we do a forEach variant, as others propose)

Others, what do you think of formFilter and formMap? Are they clear at the point of use? Fluent? Natural?

3 Likes

I don't think either makes much sense and don't think it makes sense to argue for either. formFilter does not describe the action being performed, and neither does formMap.

I'm OK avoiding this argument entirely, though, in favor of a mutating forEach or another name, since we're losing the (A) -> B freedom of map.

11 Likes