Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it bad practice to try and keep track of iterations while using reduce/map in Clojure?

Tags:

clojure

So being new to Clojure and functional programming in general, I sometimes (to quote a book) "feel like your favourite tool has been taken from you". Trying to get a better grasp on this stuff I'm doing string manipulation problems.

So knowing the functional paradigm is all about recursion (and other things) I've been using tail recursive functions to do things I'd normally do with loops, then trying to implement using map or reduce. For those more experienced, does this sound like a sane thing to do?

I'm starting to get frustrated because I'm running into problems where I need to keep track of the index of each character when iterating over strings but that's proving difficult because reduce and map feel "isolated". I can't increment a value while a string is being reduced...

Is there something I'm missing; a function for exactly this.. Or can this specific case just not be implemented using these core functions? Or is the way I'm going about it just wrong and un-functional-like which is why I'm stuck?

Here's an example I'm having:

This function takes five separate strings then using reduce, builds a vector containing all the characters at position char-at in each string. How could you change this code so that char-at (in the anonymous function) gets incremented after each string gets passed? This is what I mean by it feels "isolated" and I don't know how to get around this.

(defn new-string-from-five
  "This function takes a character at position char-at from each of the strings to make a new vector"
  [five-strings char-at]
  (reduce (fn [result string]
            (conj result (get-char-at string char-at)))
    []
    five-strings))

Old : "abc" "def" "ghi" "jkl" "mno" -> [a d g j m] (always taken from index 0)


Modified : "abc" "def" "ghi" "jkl" "mno" ->[a e i j n] (index gets incremented and loops back around)
like image 568
Nick97832954 Avatar asked Oct 31 '25 03:10

Nick97832954


2 Answers

I don't think there's anything insane about writing string manip functions to get your head around things, though it's certainly not the only way. I personally found clojure for the brave and true, 4clojure, and the clojurians slack channel most helpful when learning clojure.

On your question, probably the most common thing to do would be to add an index to your initial collection (in this case a string) using map-indexed

(user=> (map-indexed vector [9 9 9])
([0 9] [1 9] [2 9])

So for your example

(defn new-string-from-five
  "This function takes a character at position char-at from each of the strings to make a new vector"
  [five-strings char-at]
  (reduce (fn [result [string-idx string]]
            (conj result (get-char-at string (+ string-idx char-at))))
    []
    (map-indexed vector five-strings)))

But how would I build map-indexed? Well

Non-lazily:

(defn map-indexed' [f coll]
  (loop [idx 0
         res []
         rest-coll coll]
    (if (empty? rest-coll)
      res
      (recur (inc idx) (conj res (f idx (first rest-coll))) (rest rest-coll)))))

Lazily (recommend not trying to understand this yet):

(defn map-indexed' [f coll]
  (letfn [(map-indexed'' [idx f coll]
            (if (empty? coll)
              '()
              (lazy-seq (conj (map-indexed'' (inc idx) f (rest coll)) (f idx (first coll))))))]
    (map-indexed'' 0 f coll)))
like image 155
bfabry Avatar answered Nov 02 '25 23:11

bfabry


You can use reductions:

(defn new-string-from-five
  [five-strings]
  (->> five-strings
       (reductions
        (fn [[res i] string]
          [(get-char-at string i) (inc i)])
        [nil 0])
       rest
       (mapv first)))

But in this case, I think map, mapv or map-indexed is cleaner. E.g.

(map-indexed 
  (fn [i s] (get-char-at s i))
  ["abc" "def" "ghi" "jkl" "mno"])
like image 31
rmcv Avatar answered Nov 02 '25 23:11

rmcv