65

I find myself doing a lot of:

(concat coll [e]) where coll is a collection and e a single element.

Is there a function for doing this in Clojure? I know conj does the job best for vectors but I don't know up front which coll will be used. It could be a vector, list or sorted-set for example.

Michiel Borkent
  • 34,228
  • 15
  • 86
  • 149
  • 1
    "It could be a ... sorted-set for example." Allowing callers to put items in the tail position and maintaining sort order are mutually exclusive. Your level of concern about the tail position hints at vector. How about (conj (vec coll) e) ? – Mocky Apr 21 '11 at 11:55

5 Answers5

63

Some types of collections can add cheaply to the front (lists, seqs), while others can add cheaply to the back (vectors, queues, kinda-sorta lazy-seqs). Rather than using concat, if possible you should arrange to be working with one of those types (vector is most common) and just conj to it: (conj [1 2 3] 4) yields [1 2 3 4], while (conj '(1 2 3) 4) yields (4 1 2 3).

amalloy
  • 89,153
  • 8
  • 140
  • 205
  • 1
    How would you write a function that takes any kind of coll to do this? – Michiel Borkent Apr 20 '11 at 17:52
  • 15
    Edit two years later: I wouldn't write that function. – amalloy Jun 26 '13 at 16:48
  • @amalloy can you elaborate why you would not write that function (I assume you were talking about concat, and not your original answer)? – Jon Lorusso Aug 20 '13 at 12:20
  • 4
    @Jon If I needed to add items to the end of the collection, I would either: (a) arrange things so that I can use vectors or build the sequence lazily, or (b) make sure it looks ugly, by forcing myself to write `(concat coll [x])` each time rather than calling out to some innocent-looking function that does this dirty work. – amalloy Aug 20 '13 at 18:19
  • I get the sentiment, but I find most often when I need this it's always for super small size lists. It happens say if I use rest or destructuring on a vector input, and I want to add more things at the end, but the destructuring/rest call turned the input vec into a list. The time spent converting it back to a vector compared to a O(n) add to the end is probably similar. Do you have an alternative for this? Like any way to destructure the rest as a vector? – Didier A. Aug 05 '22 at 06:45
13

concat does not add an element to the tail of a collection, nor does it concatenate two collections.

concat returns a seq made of the concatenation of two other seqs. The original type of the collections from which seqs may be inferred are lost for the return type of concat.

Now, clojure collections have different properties one must know about in order to write efficient code, that's why there isn't a universal function available in core to concatenate collections of any kind together. To the contrary, list and vectors do have "natural insertion positions" which conj knows, and does what is right for the kind of collection.

Laurent Petit
  • 1,201
  • 7
  • 12
9

This is a very small addendum to @amalloy's answer in order to address OP's request for a function that always adds to the tail of whatever kind of collection. This is an alternative to (concat coll [x]). Just create a vector version of the original collection:

(defn conj*
  [s x]
  (conj (vec s) x))

Caveats:

If you started with a lazy sequence, you've now destroyed the laziness--i.e. the output is not lazy. This may be either a good thing or a bad thing, depending on your needs.

There's some cost to creating the vector. If you need to call this function a lot, and you find (e.g. by benchmarking with Criterium) that this cost is significant for your purposes, then follow the other answers' advice to try to use vectors in the first place.

Mars
  • 8,689
  • 2
  • 42
  • 70
  • 2
    The destruction of the laziness isn't really a cost in this case. Appending to the end of the list means the list will be completely realized. – skillet-thief Nov 26 '19 at 00:08
7

To distill the best of what amalloy and Laurent Petit have already said: use the conj function.

One of the great abstractions that Clojure provides is the Sequence API, which includes the conj function. If at all possible, your code should be as collection-type agnostic as it can be, instead using the seq API to handle operations on collections and picking a particular collection type only when you need to be specific.

If vectors are a good match, then yes, conj will be adding items onto the end. If use lists instead, then conj will be adding things to the front of your collection. But if you then use the standard seq API functions for pulling items from the "top" of a collection (the back of a vector, the front of a list), it doesn't matter which implementation you use, because it will always use the one with best performance and thus adding and removing items will be consistent.

semperos
  • 4,674
  • 28
  • 31
  • I need to make sure the element is added to the tail of the collection. – Michiel Borkent Apr 20 '11 at 19:13
  • 2
    @Michiel Borkent Never hurts to do a "am I using the most idiomatic constructs for the language" check. Without more details (e.g. why it needs to be at the end), difficult to help further. – semperos Apr 20 '11 at 19:40
  • I don't know about Michiel, but I'm working on an extension to predicate parameters. So for example (defn x [ [ a b ] ] ) works but (defn x [ ( a b ) ] does not. – redfish64 Mar 03 '15 at 08:51
  • 3
    For tuples order matters. In Scheme, a Lisp dialect, `append` is built-in. I think it's perfectly reasonable to expect such a function from a language. – m33lky Mar 09 '17 at 12:51
0

If you are working with lazy sequences, you can also use lazy-cat:

(take 5 (lazy-cat (range) [1])) ; (0 1 2 3 4)

Or you could make it a utility method:

(defn append [coll & items] (lazy-cat coll items))

Then use it like this:

(take 5 (append (range) 1)) ; (0 1 2 3 4)