Naming of `chained(with:)`

No, but we are writing, and Swift API guidelines particularly emphasize the importance of clarity and fluency, including the use of “grammatical English phrases” when it is possible. Good English makes for good Swift APIs.

7 Likes

You argue that two words from the exact same latin root have vastly different merit. The one you prefer transited through French (in medieval times), the one I prefer was re-borrowed directly from Latin (15th C.). Whatever.

I like that "concatenate" says what it means by itself. "chain" requires a preposition ("with") in order to properly make sense.

2 Likes

Could you specify which precedents? RangeReplaceableColleciton.append(_:) is mutating, so it doesn't return anything, and as noted Publisher.append(_:) returns a different type. Is there something I'm missing?

I ask because I actually have the opposite reaction.To me, it seems like the idiomatic pattern in Sequence methods is to return a transformed type when doing so makes sense (e.g. PrefixSequence, DropWhileSequence).

3 Likes

Why does it warrant a type rather than returning AnySequence through AnyBidirectionalCollection, as the base sequences allow, all made with the + operator? These are all + overloads, so either + needs to go, or it needs to be renamed too.

I've seen people hate + but I think it's good.

Type erasure through AnySequence results in comparitively slow dynamic calls.

1 Like

The resulting type is random access if the collections it chains are (er, if I understand the documentation...I'm itching to actually have time to try it). AnySequence while cool, doesn't let you randomly access anything.

I observe that no one has yet commented on what I consider to be the clearest, most readily-understood option:

let numbers = [10, 20, 30].followed(by: 1...5)

I tried to reply to this, but perhaps I was too indirect. I prefer chained(with:) to followed(by:):

  • I disagree with you that it's unclear at the point of use.
  • I think consistency with other languages is valuable.
1 Like

IMO, concatenated(with:) and followed(by:) are all a bit ambiguous about what the return type would be—the argument could be made that it either does what it does today, or just tacks the elements of the argument onto the receiver, whatever that data type happens to be.

Unlike those, chained(with:) aligns with the specific mental model of a physical chain—a sequence of things that, when put together, don't meld together or combine into each other, but can be quickly attached together while retaining their individual shapes.

12 Likes

We could try looking at this from the other end. Consider these:

[10, 20, 30].followed(by: 1...5)
[10, 20, 30].chained(with: 1...5)
[10, 20, 30].concatenated(with: 1...5)

Now ask a random Swift programmer which one of these least looks like it creates a single new collection that contains all of the mentioned elements.

To me, there's a pretty clear winner according to that criterion.

[@allevato beat me to it by making a similar point without the straw poll.]

1 Like

Yes, I agree, according to that criterion, followed(by:) seems like the clear winner. The way I read it, x.followed(by: y) obviously creates one result which consists of x followed by y. And that is exactly what the method in question does. Its return type is a single value of a type conforming to Sequence, which when iterated will produce all the elements of x followed by all the elements of y.

I think x.concatenated(with: y) is essentially just as clear, but needlessly verbose. It says the same thing but with more typing.

And I think x.chained(with: y) does not obviously mean anything. The word “chain” has multiple meanings, I’ve never heard the phrase “chained with” in normal English—people talking about chaining one thing to another, but that is usually a means of preventing movement, or at least restricting movement to the general vicinity.

“I chained my goat to the fencepost” means something. “I chained my goat to the cow” also means something, but it does not imply the goat is always in front of the cow.

If somebody said “I chained my goat with the cow”, that would first make me pause and wonder why they said “with” instead of “to”, and it still would not communicate anything about the geometric arrangement of the two animals, except that they are near each other.

1 Like

That's a good point. chained(to:) is definitely the grammatically correct way to spell it.

But even there, and even if we are talking about a sequence of one thing chained to a sequence of another, it still doesn’t communicate the desired semantics:

“The parade had a line of six goats chained to a line of four cows” means something, but it doesn’t necessarily mean the goats are ahead of the cows.

On the other hand, “the parade had a line of six goats followed by a line of four cows” means precisely that.

It does mean that they're ahead of the cows relative to the chain. There relative positions to each other outside of the chain are irrelevant if all you are doing is following the chain from head to tail.

1 Like

I agree that x.followed(by: y) reads well and clearly conveys the semantics of the method. But what should the name of the result type be?

None of eg these makes a lot of sense:

Chain<Base1, Base2> // Its current name, which doesn't match `followed(by:)`.
Following<Base1, Base2> // This kind of matches `followed(by:)` but ...
Succession<Base1, Base2>
MetaSequence<Base1, Base2>
Sequences<Base1, Base2>
...
?

Another way to look at it, as mentioned upthread, is that the current:

zip(x, y) -> Zip2Sequence<X, Y>

(and the standard unix command) could lead to:

cat(x, y) -> Cat2Sequence<X, Y>

: )

Yes, I think we understand what you're saying, but I think you're missing the other point: that "following" and "concatenated" don't clearly reflect the structural outcome.

If you say, “the parade had a line of six goats followed by a line of four cows”, that's plausibly a line of 10 animals, which is not the intended structure.

Please note that I'm saying this interpretation is plausible, not necessary. Different people may understand it different ways. As a result, your suggested name might introduce more confusion than it removes.

FWIW, when I posted up-thread, "chained" was the word that I thought to be structurally most informative. I agree that in non-technical use there's no particular implication of sequentiality of linking, but in software "chained together" would (I claim, in the absence of other information) be understood as a linear chain.

Maybe all of this means we've discovered the naming pitfalls here, but we haven't discovered the right name yet?

I strongly disagree. A line of 10 animals is precisely the intended structure.

The purpose of the chained(with:) method is exactly and entirely to return a sequence which iterates through all the elements of both inputs.

The specific technicalities of the return type are essentially an implementation detail that most users shouldn’t ever have to think about, much like Zip2Sequence, LazyMapSequence, ReversedCollection, and all the rest.

It’s a sequence, you can iterate it, and that’s the main goal, the primary point, the whole raison d’être of the API itself. That’s what people use it for, that’s what people want it to do, and that’s what its name should reflect.

IMO, the problem with followed(by:) is that it requires me to reason about the type of the argument relative to the type of the collection to understand what's going to happen. For instance, it seems not-unreasonable to me to have an API such as:

[1, 2, 3].followed(by: 4) // 1, 2, 3, 4

I.e., I don't think that followed(by:) on its own effectively communicates whether the argument is of type Element, or a sequence with matching Element type.

If usage of the word followed is desired, I think it at least deserves to be spelled as followed(byContentsOf:) just as we have append(contentsOf:). Even with this naming tweak I fall on the side of feeling that chained(with:) and concatenated(with:) are better names, if just because they are slightly less awkward when read as a phrase.

FWIW, I would actually assume that @QuinceyMorris was describing two separate rows of animals here, but that's mostly because of my general knowledge of how parades work—I don't think that

[1, 2, 3].followed(by: [4, 5, 6])

plausibly describes the same sequence represented by:

[[1, 2, 3], [4, 5, 6]]

Guides/Chain.md:

Unlike placing two collections in an array and calling joined(), chaining permits different collection types, performs no allocations, and can preserve the shared conformances of the two underlying types.

If chained(with:) is a variant of the same API family, then possibly it could become joined(to:), returning a Joined2Sequence.

(cf. joined() and joined(separator:) and SE-0133)

1 Like

@Nevin is correct that this returns a sequence you can iterate through. However, if it's "exactly and entirely" that (at the level of API contract, not implementation), then it may as well return a sequence that contains all of the elements in a single collection.

But it's not exactly and entirely that, because the whole point of the API (AFAICT) is to guarantee that the elements are not conjoined into a single collection, with the potential performance hit that might imply. Without that guarantee, chained(with:) would have no reason to exist, since we already have APIs to conjoin collections.

I don't think we're looking for the most natural wording of the operation performed by this API. I think maybe we're looking for the wording that best alerts us to what is unusual about the API.