icyrock.com
icyrock.com

Reputation: 28598

Treating last element specially while iterating a sequence

I find that I often need to treat the last element in the sequence in a special way when iterating it. For example, take the built-in function interpose:

> (interpose ", " (range 4))

(0 ", " 1 ", " 2 ", " 3)

One way of thinking about this is:

I also find that I need to do something special when building a Mig layout using Seesaw. For example, let's say I want to build this:

+---+---+---+
| 1 | 2 | 3 |
+---+---+---+
| 4 | 5 | 6 |
+---+---+---+
| 7 | 8 | 9 |
+---+---+---+

where each of the numbers is some component (e.g. a button). I can do this by making a Mig layout constraint of the whole panel "flow" and then add the following constraints to each of the components:

Note that the above can be said like this:

where again the same "special last element" theme appears.

So two questions:

Yes, you can make helper functions and macros, but I find this often enough that I am inclined to think that it should be one of the above. In other words - do you run into the same types of "problems" and how do you solve them?

Upvotes: 3

Views: 1093

Answers (3)

sw1nn
sw1nn

Reputation: 7328

You're thinking about the wrong end of the sequence. All sequences in clojure (in fact LISPs in general) are an element cons'd with a sequence of more elements.

Sequence processing is designed for you to do something for the first element of the sequence and then treat the rest as a whole (possibly recursively). In fact clojure has functions named first and rest to encourage this way of thinking.

As noted in a comment by @Rafal interpose can be though of as...

  • take the first element
  • take the second element, precede with ","
  • take the third element, precede with "," ...

In clojure you could (almost) implement this with:

(defn interpose [s]
    (cons (first s) (map #(str "," %) (rest s))))

the actual implementation in core.clj builds on the interleave function, which is more complex because it handles two sequences, but still builds on the first + rest idiom.

Many (most?) algorithm's are amenable to this way of thinking.

Upvotes: 1

NielsK
NielsK

Reputation: 6956

Most of the discussion here has been based on your assumption that the needed function is interpose 'like'. It is in some respects, but the big difference is that to optimize interpose's uses, it is made to be lazy.

Lazy functions takes elements from a sequence that can be of unknown (i.e. stream) or even infinite (i.e. range) length. Only elements needed to produce the values at the root function (i.e. take) are calculated. Calling last and functions that depend on last, like count, means the original sequence needs to be completely traversed for last or count to be realized. This is what sw1nn warns for.

However, in this case the amount of elements ,or the index of the last element, is probably already known, as per David's answer. As soon as you can use this as a parameter without using count, you can create such a function pretty easily, and even make it lazy.

(def buttons [\a \b \c \d \e \f \g \h \i])

(defn partition-nth-but
  [n b coll]
  (map
    (partial map second)                            ; remove index
    (partition-by
      #(and (integer? (/ (% 0) n)) (not= (% 0) b))  ; partition by index every nth but b
      (map-indexed (fn [i v] [(inc i) v]) coll))))  ; index coll offset 1 

=> (partition-nth-but 3 9 buttons)
((\a \b) (\c) (\d \e) (\f) (\g \h \i))

(def grow str)
(def grow-and-wrap (comp clojure.string/upper-case grow))

=> (map apply (cycle [grow grow-and-wrap]) (partition-nth-but 3 9 buttons))
("ab" "C" "de" "F" "ghi")

But if we're applying a sequence of functions anyway, we could also just cycle through the right repetition of functions

(defn every-but-nth
  [n rf nf]
  (concat (repeat (dec n) rf) (repeat 1 nf)))

=> (apply concat
     (every-but-nth 3
       (every-but-nth 3 "grow" "grow-and")
     (repeat 3 "grow")))
("grow" "grow" "grow-and" "grow" "grow" "grow-and" "grow" "grow" "grow")

=> (map
     #(% %2)
     (apply concat (every-but-nth
                     3
                     (every-but-nth 3 grow grow-and-wrap)
                     (repeat 3 grow)))
     buttons)
("a" "b" "C" "d" "e" "F" "g" "h" "i")

Upvotes: 1

dcow
dcow

Reputation: 7975

Since you know the size, say n, of the input, simply do something for only the first n-1 elements. This is the easiest solution to your initial interpose example.

In your grow example, grow n-1 (or 8) elements wraping at 3 and 6. Then tack n (9) on the end.

However, you may not always know the size of your input. If this is the case, the same result can be accomplished by leaving off the first element, and only operating on the remaining elements. This is the more general case, and is probably closer to how you are encouraged to think when using clojure.

Upvotes: 0

Related Questions