10

As a newbie to Clojure I often have difficulties to express the simplest things. For example, for replacing the last element in a vector, which would be

v[-1]=new_value

in python, I end up with the following variants in Clojure:

(assoc v (dec (count v)) new_value)

which is pretty long and inexpressive to say the least, or

(conj (vec (butlast v)) new_value) 

which even worse, as it has O(n) running time.

That leaves me feeling silly, like a caveman trying to repair a Swiss watch with a club.

What is the right Clojure way to replace the last element in a vector?


To support my O(n)-claim for butlast-version (Clojure 1.8):

(def v (vec (range 1e6)))
#'user/v
user=> (time (first (conj (vec (butlast v)) 55)))
"Elapsed time: 232.686159 msecs"
0
(def v (vec (range 1e7)))
#'user/v
user=> (time (first (conj (vec (butlast v)) 55)))
"Elapsed time: 2423.828127 msecs"
0

So basically for 10 time the number of elements it is 10 times slower.

ead
  • 32,758
  • 6
  • 90
  • 153
  • 1
    Your first way would be how it's done. Obviously, you could write a "replace-last" function to clean it up. I think Python's overly succinct way of expressing that has spoiled your expectations unfortunately. I don't think indexing from the rear is needed often enough to warrent its own syntax in Clojure. +1 because I'd like to be proven wrong. – Carcigenicate Sep 21 '17 at 19:54
  • Note: solution #3 is not O(n) if `v` is already a vector (I recommend always using a Clojure vector over a Clojure list as the default choice, unless measurement proves otherwise). – Alan Thompson Sep 21 '17 at 20:08
  • 1
    This is a well-delimited, specific question and answer, but I think it's worth pointing out that many algorithms expressed with index expressions in Python have an idiomatic Clojure equivalent that does not require using indexes. – glts Sep 21 '17 at 20:34
  • @AlanThompson my measurements imply that it is `O(n)`, please see my edit. – ead Sep 22 '17 at 03:53
  • @AlanThompson The [source code](https://github.com/clojure/clojure/blob/clojure-1.9.0-alpha14/src/clj/clojure/core.clj#L272) for `butlast` shows that it is order of `(count coll)` for sequence `coll`, as OP suggests. – Thumbnail Sep 22 '17 at 13:02
  • You are correct that the source code loops over all items in the sequence. I had assumed that a vector arg would use structural sharing to make it O(log32 n). Doh! – Alan Thompson Sep 22 '17 at 17:57

2 Answers2

17

I'd use

(defn set-top [coll x]
  (conj (pop coll) x))

For example,

(set-top [1 2 3] :a)
=> [1 2 :a]

But it also works on the front of lists:

(set-top '(1 2 3) :a)
=> (:a 2 3)

The Clojure stack functions - peek, pop, and conj - work on the natural open end of a sequential collection.

But there is no one right way.


How do the various solutions react to an empty vector?

  • Your Python v[-1]=new_value throws an exception, as does your (assoc v (dec (count v)) new_value) and my (defn set-top [coll x] (conj (pop coll) x)).
  • Your (conj (vec (butlast v)) new_value) returns [new_value]. The butlast has no effect.
Thumbnail
  • 13,293
  • 2
  • 29
  • 37
1

If you insist on being "pure", your 2nd or 3rd solutions will work. I prefer to be simpler & more explicit using the helper functions from the Tupelo library:

(s/defn replace-at :- ts/List
  "Replaces an element in a collection at the specified index."
  [coll     :- ts/List
   index    :- s/Int
   elem     :- s/Any]
   ...)

(is (= [9 1 2] (replace-at (range 3) 0 9)))
(is (= [0 9 2] (replace-at (range 3) 1 9)))
(is (= [0 1 9] (replace-at (range 3) 2 9)))
As with drop-at, replace-at will throw an exception for invalid values of index.

Similar helper functions exist for

  • insert-at
  • drop-at
  • prepend
  • append

Note that all of the above work equally well for either a Clojure list (eager or lazy) or a Clojure vector. The conj solution will fail unless you are careful to always coerce the input to a vector first as in your example.

Alan Thompson
  • 29,276
  • 6
  • 41
  • 48