Naming of `chained(with:)`

The chained(with:) method concatenates the elements of two sequences.

An example from the documentation is:

let numbers = [10, 20, 30].chained(with: 1...5)
// Array(numbers) == [10, 20, 30, 1, 2, 3, 4, 5]

In my opinion, that does not read well at the point of use, and I think a clearer name can be found.

Swift already uses append(contentsOf:) for a mutating method on RangeReplaceableCollection, so I think it would be most consistent to name this one appending.

Personally, I’d say a name like followed(by:), would be even clearer, as in:

let numbers = [10, 20, 30].followed(by: 1...5)
// Array(numbers) == [10, 20, 30, 1, 2, 3, 4, 5]

But either way, chained(with:) is neither clear nor consistent.

8 Likes

I agree with Nevin that the naming is poor, however it apparently matches equally poorly named functions in Rust and Python itertools. However, in looking at the Chain guide for chained(with:), the naming may be wrong for additional reasons.

The example given:
let numbers = [10, 20, 30].chained(with: 1...5)
could trivially be re-expressed as:
let numbers = [10, 20, 30] + Array(1...5)
without the need for the chained(with:) method.

But more deeply, the result is being converted into a Chain type (or Concatenation type (the guide was inconsistent in this regard)), which is "a sequence, with conditional conformance to the Collection , BidirectionalCollection , and RandomAccessCollection when both the first and second arguments conform."

This could lead to unexpected, implicit result type conversions. Given that the only seeming usefulness of this method is to sidestep explicit conversions, perhaps its inclusion as well as its naming should be questioned.

The usefulness of chained is to express the desire to have a Sequence that iterates the elements of multiple Sequences in order without constructing a new intermediate object with storage. The fact that this does not return an Array is very valuable.

11 Likes

I remember this coming up once before, and the name that I liked at the time was concatenate(d).

2 Likes

The naming is motivated in the guide:

Naming

This method’s and type’s name match the term of art used in other languages and libraries.

Comparison with other langauges

Rust: Rust provides a chain function that concatenates two iterators.

Ruby/Python: Ruby and Python’s itertools both define a chain function for concatenating collections of different kinds.

And the actually returned type is Chain<Self, S>:

  public func chained<S: Sequence>(with other: S) -> Chain<Self, S>
    where Element == S.Element
  {
    Chain(base1: self, base2: other)
  }

It is not Concatenation<Self, S> as the guide says.

I also think that the guide might be potentially misleading when it says:

let numbers = [10, 20, 30].chained(with: 1...5)
// Array(numbers) == [10, 20, 30, 1, 2, 3, 4, 5]
// 
let letters = "abcde".chained(with: "FGHIJ")
// String(letters) == "abcdeFGHIJ"

since it (if hastily read) might seem to suggest that the return types are Array and String, rather than:

Chain<Array<Int>, ClosedRange<Int>>

and

Chain<String, String>

As @lukasa said, the usefulness of chained is that it produces a Chain<Base1, Base2> in O(1) time, by composing Base1 and Base2 (chaining them and keeping their types, not by eg copying their contents into an Array or String).

So for example, Base1 can be [1, 2, 3] and Base2 can be an infinite sequence of random numbers and they can still be chained (in essentially no time).

2 Likes

Right, I am saying the name does not fit well with Swift. I do not think chain rises to the same term-of-art level as map, filter, and reduce, and I am strongly of the opinion that chain fails the basic “clarity at the point of use” standard from the Swift API design guidelines.

Right, we understand what the method does. The fact that the return type was given the same name as the method does not provide new information. If and when the name chain is changed to something clear, expressive, and Swifty, then the return type’s name should also be changed to match.

As I said above, I would prefer x.followed(by: y), but I could understand a consistency argument for x.appending(contentsOf: y). The current spelling x.chained(with: y) simply does not convey the purpose and effect of this method.

• • •

Edit:

Another option would be to make a free function in line with zip, instead of a method. Perhaps concatenate(x, y).

5 Likes

Note that the example code wraps numbers in Array(...), so it doesn't follow that numbers is itself an array.

Thanks for starting this thread, Nevin! Those are both good suggestions for alternative names. Anyone have any others?

That's a good point — I think the guide can be made more explicit here to clear up this confusion.

2 Likes

Apple's Combine framework calls the equivalent method append(_:) and the type Publishers.Concatenate.

1 Like

I really like this idea. It’s a suitably fundamental operation that it feels “right” to make it a free function.

2 Likes

My problem with concatenate and append as names is that precedents in the language strongly imply (to me, at least) that they are functions of type (T, T) -> T, rather than (T, T) -> AnotherCollectionType<T.Element>.

9 Likes

I don't think a free function is sufficiently justifiable here. The API naming guidelines state:

Prefer methods and properties to free functions. Free functions are used only in special cases:

  1. When there’s no obvious self :
min(x, y, z)
  1. When the function is an unconstrained generic:
print(x)
  1. When function syntax is part of the established domain notation:
sin(x)

Unlike min and zip where there is no obvious self (or in other words the arguments are clearly symmetrical), in chained(with:) one sequence of elements obviously precedes the other (the arguments are asymmetrical), making the first sequence is an obvious candidate to be self.

2 Likes

I agree that we've already established a strong precedent in the standard library that append* will not change the type of self, and it would be mistake to use it here.

I agree that chain does not rise to the same term-of-art level as map , filter , and reduce, however I don't think we should needlessly diverge from other languages. Is there any other language with this kind of lazy concatenation operation that is not called chain?

It's not clear to me that chain fails the "clarity at the point of use" standard. You suggest concatenate (implying it's clear at the point of use), but "concatenate" is literally defined (in the dictionary on my Mac) as "link (things) together in a chain".

3 Likes

When I first reviewed this chained(with:) I realized „oh, they actually mean rope” because I knew those as Rope (data structure) - Wikipedia as we implemented ByteString as such thing in Akka. So while that’s a term of art I’ve never seen it used as the operation name — it’s all concat and split where I’ve seen those.

So since we don’t seem to like concat for previously mentioned reasons then might as well stick with chain/chained— I kind of liked it after a moment of throught, it’s a kind of rope I guess :-)

1 Like

It is my understanding that this:

let bigArray = (some big array)
let upAndDown = bigArray.chained(with: bigArray.reversed())

would not create an actual copy of "big array but reversed" appended to big array, so a lot less memcpy, and roughly 1/3rd of the storage, but each access to upAndDown would be slightly slower. That seems pretty significant if bigArray is actually big.

1 Like

I think the name chained(with:) fits well. Thought it seems to be lazy something which should probably be documented. A non in-place version of rotate? - #4 by nnnnnnnn

1 Like

chained(with:) is lazy in the same way that the ReversedCollection wrapper is lazy, it doesn't eagerly allocate new memory to return an array or other contiguous data type. We typically cover that in the documentation by noting that calling chained(with:) is an O(1) operation.

It isn't part of the .lazy collection subsystem, however, so calling something like map on it will eagerly produce the result.

2 Likes

append currently only exists on RangeReplaceableCollection, and returns the same type.
concatenate only exists on Combine.Publisher, and returns a new type.

I don't think those are strong enough to prevent us using those names for broader versions of the same operations. We don't need to add a weird new synonym.

Since the current documentation defines Chain in terms of concatenation, it seems that "concatenate" is the correct basis for the name. It would be very similar in use, behaviour and type signatures as the Combine version.

3 Likes

“Chain with,” it should be noted, is a pretty literal translation of “concatenate” (Latin: cum (with) + catena (chain)).

We would do well to keep in mind Orwell’s advice on the matter: “Bad writers, and especially scientific, political and sociological writers, are nearly always haunted by the notion that Latin or Greek words are grander than Saxon ones... [T]he normal way of coining a new word is to use a Latin or Greek root with the appropriate suffix and, where necessary, the -ize formation. It is often easier to make up words of this kind ... than to think up words in English that will cover one’s meaning. The result, in general, is an increase in slovenliness and vagueness.”

10 Likes

That is a very strange appeal to authority. We're not discussing prose.

2 Likes