Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to replace the last element in a vector in Clojure

Tags:

clojure

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.

like image 876
ead Avatar asked Sep 21 '17 19:09

ead


2 Answers

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.
like image 190
Thumbnail Avatar answered Nov 16 '22 16:11

Thumbnail


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.

like image 37
Alan Thompson Avatar answered Nov 16 '22 14:11

Alan Thompson