Naming of `chained(with:)`

I take this back.

I think appending(contentsOf:) would be consistent with other APIs in the Standard Library like reversed(). The precedent, it seems to me, is that there is a bias for laziness and it is not important to call out when a different type (rather than self) is returned.

1 Like

And here we have a philosophical disagreement.

I am reminded in a way of the Sharp regrets article, specifically regret #7, where one of the designers of C# explains how they made the syntax for a new feature clearly call attention to how it was different from the existing features. But then after a short time the ā€œnewā€ feature was just another existing feature, and it looked different and out of place for no reason.

We do not and should not try to make the name of the API communicate all the nuances of the implementation. The place for that is documentation. The naming of the API should simply reflect the purpose of the method, to make call sites clear and easy to read.

3 Likes

I don’t think that’s actually an argument against the name followed(by:).

Instead, it is simply a question of, ā€œShould we also have an overload that takes a single element?ā€

I suspect the answer is ā€œProbably notā€, but I could be wrong. The point is, that’s a reasonable thing to bring up as a future direction or a follow-on proposal.

If enough people actually reach for the single-element version so frequently that they find it confusing and annoying when it doesn’t work, then we could consider adding it. I’m guessing that won’t happen, because I doubt it’s a common pattern, but if I’m mistaken that’s not a problem because as you note the spelling ā€œfollowed(by:)ā€ works just fine for both overloads so we can easily add the other one if needed.

Let me try to state my objection a little more directly: I think the name followed(by:) suffers from an ambiguity which does not apply to followed(byContentsOf:), concatenated(with:), or chained(with:).

I also agree with @kylemacomber that (unless there’s an existing API that has been overlooked in this thread) appending(contentsOf:) would be a natural name for this method.

2 Likes

And let me state my counterargument more clearly:

The fact that followed(by:) works equally well with both a sequence (as proposed) and a single element (not proposed, probably unnecessary), is a significant advantage and benefit. It is a point in favor of this spelling.

That is because, if people actually want the single-element version (which I doubt the will), then we can easily add it as an overload with the same name and behavior.

Right, I mentioned that in the original post as the most consistent option.

2 Likes

But it’s not the same behavior—the behavior is subtly different and requires a reader to reason about the type of the argument the method target to understand what the method would do. Given that we have append(_:) and append(contentsOf:), I don’t see it as out of the question that we would want both non-mutating versions.

(Aside: if both versions used the followed(by:) name, wouldn’t someString.followed(by: ā€œ!ā€)) become ambiguous?)

I think you have overlooked the point of @Jumhyn's argument.

Consider a collection type T where T.Element == T. (Perhaps one might use such a type to represent HTML, our favorite toy example these days.) Suppose I have two instances representing the following two snippets of HTML:

<!-- List A -->
<ol>
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ol>
<!-- List B -->
<ol>
  <li>4</li>
  <li>5</li>
  <li>6</li>
</ol>

What is the semantic meaning of listA.followed(by: listB)? There are three possibilities:

The words "contents of" gives a strong indication to the reader that the elements in the second list are added after the elements of the first list (middle result). It also allows users to distinguish append(contentsOf:) from append(_:) (bottom result); without that label, two methods with the same name would have different semantics.

Now, we are trying to name a method that has the semantics shown in the top result. It is true that, if we merely wish to iterate over the list items, the top and middle result behave identically. But the result of the method we're naming is a full-blown collection type; we can and might want to do something other than iteration. For instance, we might want to actually render the HTML represented by the result; at that point, the top and middle results behave differently.

2 Likes

Wait, what? Your top result shows either two separate lists, with no way to iterate over both of them at once, or else it shows a single list of two elements. We do not want either of those semantics.

We want to produce a sequence of elements as in your 2nd result.

The precise internal storage pattern of that sequence is essentially irrelevant. The actual implementation is designed to avoid unnecessary copying and allocation, which is great. But that has nothing to do with the semantics of the method, which are to take two sequences and produce a sequence that iterates over the elements of one followed by the elements of the other.

No, because the default type for string literals is String. However, you can easily construct an ambiguous example using something like [Any]. That is the same issue that comes up in discussions of variadic parameter splatting, and the solution is the same in both cases: either require annotation for the ambiguous cases, or else pick one choice, document it, and require annotation to get the other behavior.

1 Like

It shows neither of those things. It shows two separate lists, with a way of iterating over both of them at once, one after the other.

Again, there is more semantically to a specific type which conforms to Sequence than its behavior on iteration. This method doesn't merely return some Sequence.

So, it's like zip(_: _: ), but in series instead of parallel. Why isn't this a top-level chain(_: _: ) function? Like zip, the operands are pretty much equal; just because we do have a sequence that goes first in chain doesn't mean it's more primary.

Also, a free function chain can be extended to 3+ operands, just like zip, when we add Variadic Generics. The variadic route would look weirder if we had one receiver and the other operands as regular arguments. (Or, if we add multi-methods, we could make every operand a receiver.)

1 Like

Order doesn't matter for zip() because the same elements are accessible on each iteration. However, changing the order for chain() changes the order of iteration. It most definitely does matter which is listed first.

That's completely unnecessary with the current design.

let chain = seq1.chain(to: seq2).chain(to: seq3)

This works today because the result of chain(to:) is a sequence.

3 Likes

I don't necessarily agree with that - if the rationale is that the first sequence deserves to be self, then it follows that the first element in zip's tuple could stake a claim to being an obvious self. In other words, if the standard library used this design, we'd have:

someSequence.zipped(with: anotherSequence)

Another thing that might be worth considering is what variadic generics might do to this API. Personally I would prefer to concatenate multiple sequences using something like:

concatenate(sequence1, sequence2, sequence3) 

As opposed to a varidic or repeated application of the .chained(with:) function, which I don't think looks as nice:

sequence1.chained(with: sequence2).chained(with: sequence3)
// or even:
sequence1.chained(with: sequence2, sequence3)

Which to me further weakens the argument that the first sequence is an obvious self.

4 Likes

When I was recently building this component for the umpteenth time we ended up with the following names:

  • Concatenation<First, Second>, which reads as ā€œconcatenation ofā€ first and second.
  • x.concatenated(to: y)

FWIW, I happen to like Nevin's suggestion of x.followed(by: y), but the reason we didn't use it is that it could easily be construed as returning CollectionOf2<X, Y>, which would only result in a concatenation after flattening.

I think the fact that you end up precisely describing the semantics as ā€œconcatenationā€ makes a very clear argument for using that root word in the name.

3 Likes

One downside with that name is that it tends to imply that the self parameter gets privileged treatment (e.g. determines characteristics of the result).

1 Like

I take this back.

I think it's an interesting point that if an API still makes sense as you increase the arity, then it might be a good candidate for a free function. Maybe this is a good rule of thumb for testing this API guideline?

3 Likes

Wouldn’t the application of the free function there be equally applied to other ā€œchainableā€ APIs? For instance appending(one, two, three) would work just fine. Unless the core team wants to modify the guideline and allow both forms of API (if so, it should likely guarantee that APIs must take both forms) I’m not sure considering the free function is worthwhile.

There's no semantic difference between the orderings of elements within a zip tuple. The programmer will simply choose .0 or .1 as needed, but they have no difference in meaning.

With chain(to:), the order of iteration changes if the order of sequences is changed. Let's say the two sequences are a favorites list and an MRU history list. You are chaining them for display in a menu. You will get user-visible differences in behavior if you flip the order of the chain. The difference matters.

In other words, the natural candidate for self is the chain segment which should be iterated first, and "first" actually has meaning.

2 Likes

@dabrahams Do you have any additional context around the "Prefer methods and properties to free functions" API guideline and whether (ignoring the base name for a moment) it's justifiable for this API to use a free function?