Ganesh
Ganesh

Reputation: 307

Adding element in the list using conj and doseq in clojure

In the below code I am trying to add an element in the list using (conj listvalue countString)

(defn unique-character [parms]
  (let [split (#(str/split % #"") parms)
        countString (count split)
        listvalue #{}]
  (dbg (doseq [countString split]
               (println countString)
               (conj listvalue countString)
               (println listvalue)
         ))listvalue))

(unique-character "Leeeeeerrroyyy")

Output to be - Leroy

But I am getting an empty list as an output result

Can someone help me why the character is not added to the list, Maybe this is not good code but I wanted to understand how conj behaves inside doseq

Upvotes: 0

Views: 835

Answers (3)

leetwinski
leetwinski

Reputation: 17849

there are multiple ways to deduplicate in clojure.

the aforementioned dedupe is obviously the best, since it is short AND in the standard library.

but there are more:

  1. exploring the standard lib you will find handy functions for seq manipulation, like partition-by and map:

    (->> "Leeeerrrooyyyyyyy"
         (partition-by identity)
         (map first)
         (apply str))
    
    ;;=> "Leroy"
    

    or their transducers counterparts variant:

    (apply str (eduction (partition-by identity) (map first) "Leeeerrrooyyyyyyy"))
    
    ;;=> "Leroy"
    
  2. you could think of using iterate:

    (->> "Leeeerrrooyyyyyyy"
         (iterate #(drop-while #{(first %)} %))
         (take-while seq)
         (map first)
         (apply str))
    
    ;;=> "Leroy"
    
  3. reduce is always good, especially if you can abstract the logic:

    (defn vec-conj-unless-last [data x]
      (cond (empty? data) [x]
            (= x (peek data)) data
            :else (conj data x)))
    
    (apply str (reduce vec-conj-unless-last [] "Leeeerrrooyyyyyyy"))
    
    ;;=> "Leroy" 
    

    the nice thing about abstracting the logic, is that in can be applied in other places if needed, like you can use this function in loop:

    (loop [data "Leeeerrrooyyyyyyy" res []]
      (if (seq data)
        (recur (rest data) (vec-conj-unless-last res (first data)))
        (apply str res)))
    
    ;;=> "Leroy"
    
  4. if you're deduplicating string content, as in your example, you could use the regex for that:

    (clojure.string/replace "Leeeerrrooyyyyyyy" #"(.)\1*" "$1")
    
    ;;=> "Leroy"
    

Upvotes: 1

Gwang-Jin Kim
Gwang-Jin Kim

Reputation: 10035

More idiomatic:

(defn unique-character [s]
  (clojure.string/join (dedupe s)))

dedupe deduplicates string characters while keeping appearing order. However, it returns a list of characters. clojure.string/join joins the list of characters to a string.

(unique-character "Leeeerrrooyyyyyyy")
;; => "Leroy"

But this works too:

(defn add-if-new [s s1]
  (if (clojure.string/includes? s s1) s (str s s1)))

(defn unique-character [s]
  (reduce add-if-new (clojure.string/split s #"")))

(str s s1) is kind of a conj on string s by element s1.

Upvotes: 2

Rulle
Rulle

Reputation: 4901

Most importantly, conj will not change the input sequence, instead it will return a new version of the input sequence with the element added at the end:

(def x [:a :b :c])

(conj x :d)
x
;; => [:a :b :c]

(def y (conj x :d))
y
;; => [:a :b :c :d]

This is one of many important reasons why you would want to use Clojure and its standard library over imperative languages and their standard libraries: Having functions return new version of collections instead of modifying them makes the data flow through the program easier to reason about and makes concurrency easier, too.

You also don't need to split the string using split, because it can be treated like a sequence directly. It is true that doseq will loop over a sequence element-by-element, like for-each-loops in other languages, for the sake of some side-effect in every iteration. But conj does not have a side effect except for returning a new version of the input sequence.

In this scenario, we would instead use reduce that will, just like doseq, iterate over a sequence. But it will keep track of a value (loop-state) in the code below, that holds the state of the loop and is returned at the end. Here is a rewritten version of the function unique-characters of the question asked:

(defn unique-characters [parms]
  (reduce (fn [loop-state input-character]
            (conj loop-state input-character)) ;; <-- Return next loop state

          #{} ;; <-- Initial loop state (that is "listvalue")

          parms ;; <-- The input string (sequence of characters)
          ))

(unique-characters "Leeeeeerrroyyy")
;; => #{\e \L \o \r \y}

This will return a set of the character of the input sequence. From how the question is worded, this might not be the result that you would like. Here is a modified version that adds each character to the output sequence at most once and makes a string.

(defn unique-characters-2 [parms]
  (apply str ;; <-- Build a string from the value returned by reduce below
         (reduce (fn [loop-state input-character]
                   (if (some #(= input-character %) loop-state)
                     loop-state
                     (conj loop-state input-character)))

                 [] ;; <-- Initial loop state

                 parms ;; <-- Input string (sequence of characters)
                 )))

(unique-characters-2 "Leeeeeerrroyyy")
;; => "Leroy"

Upvotes: 2

Related Questions