In-place map for MutableCollection

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

It's neither here nor there given that we have a consensus that the best name for the proposed function is something that doesn't have anything to do with map, but I'm genuinely curious about this: you're saying everything but confirming or denying whether you dislike the form convention in general.

So let me ask you this directly: Are you saying that you like other particular names with form? Or do you dislike them all?


No, I did not ask for "possible" interpretations, I asked whether it was "a realistic point of confusion"; it sounds like you don't believe that to be the case.

I don't think this is the place for this conversation. Let's keep it focused on the pitch. If either of you is interested in revisiting the form prefix, let's take it to another thread.

In the specific case of defining it as formMap here, I think that it's totally a realistic point of confusion in that it doesn't make a whole lot of sense to me semantically.

3 Likes

No, I'm not at all interested in revisiting the form prefix, but I am very interested in understanding where people are coming from when they approach a design problem such as this.

It's hard to engage in meaningful conversation when someone deliberately mentions another aim ("+1 to the InPlace suffix...this remains the clearest, best option"/"the entire stdlib sorely needs an audit for consistent availability of mutating and non-mutating counterparts"/"contra extending the confusing, easily misread formFoo convention") and then refuses to actually confirm that it's what they're doing.

So I'm calling it out, and the only place to do that is when and where it happens.

I still think you could solicit this feedback in a new thread or private message. This is different enough from the focus.

1 Like

I'm a big +1 on the concept/functionality.

If we're going to bike shed on the name, then:

I dislike using the word form as part of the API name, because "form" is just another term for "make", implying that formMap means I'm creating a map, which is not the case. Ditto formFilter: I'm not making a filter, I'm applying a filter.

I like the "obviousness" of the name mapInPlace although I agree with @Chris_Eidhof that this technically isn't a map, unless you're willing to down the road of saying that a (T) → T is a map (which wouldn't be a hard sell).

I don't really like mutateInPlace because IMO mutate is an uncommon enough word in English that it can increase the barrier to entry for non-english speakers. However, given that we already use mutate elsewhere in our API, I wouldn't object strongly to using this term in the name.

TL;DR:

+1 on the functionality
-1 on formMap
+½ on mapInPlace or mutateInPlace

1 Like

If this is the potential confusion to be avoided, then that issue can be easily accommodated with a slight adjustment to formMapped.

1 Like

I like the idea. It seems to promise something along the lines of space efficiency rather than generating a sequence or returning a same-type copy. I'd prefer mutateInPlace or applyInPlace for f(_:T) -> T.

(I considered apply but that might seem to people to be too similar to map and indicate a non-mutating application)

1 Like

The use of mapped in this form would be extremely unconventional. The only usage of mapped in Foundation comes from the NSData methods that deal with mapped memory.

formMapped would get an even stronger -1 from me.