3

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:

  • Take the first element, add ", "
  • Take the second element, add ", "
  • Take the third element, add ", "
  • ...
  • Take the second to last element, add ", "
  • Take the last element and do nothing

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:

  • "grow" - components 1, 2, 4, 5, 7, 8, 9
  • "grow, wrap" - components 3, 6

Note that the above can be said like this:

  • "grow" for each component except the last component in the row
  • "grow, wrap" for all other components except the last component in the last row

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

So two questions:

  • Does the above reasoning make sense? I.e. should I change the design fundamentally and think about this in a different way given the above sample problems and the general theme?
  • If not, is there an idiomatic and short way to do this?

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?

icyrock.com
  • 27,952
  • 4
  • 66
  • 85
  • I don't know clojure at all and just googled it. If I talk bullshit just take the idea: http://clojure.github.com/clojure/clojure.core-api.html#clojure.core/last get this element and check in the iteration if the element equals the extracted one. – nuala Apr 14 '12 at 03:24
  • Thanks @yoshi - the problem with that is that there might be duplicates. E.g. in a vector `[1 2 3 1 2 3]` that would not work. It's possible also to work with the count, but on a 1-million element vector that's not a good approach (note that `last` also goes through all elements). It's not a hard problem, it's just I am not sure I'm attacking it from the right angle / not missing some (maybe obvious) solution. – icyrock.com Apr 14 '12 at 03:29
  • could you set a boolean in the beginning of the loop? e.g. `if(first) first=false` then shift the grow and grow, wrap accordingly. (wrapping on 4 and 7). – nuala Apr 14 '12 at 03:33
  • (sorry for writing then deleting an answer as i understood better what you were saying). i disagree - i think interpose is the fundamental pattern here and i don't think you've made a convincing case for "last item special" being more useful or general (i do agree you sometimes feel a need for this, but in my experience you can normally rewrite with interpose). the sudoku grid, for example, is obviously two interposes (horizontal + vertical). (but interesting question) – andrew cooke Apr 14 '12 at 05:30
  • To me `interpose` is "*precede* every element but the *first* one". – Rafał Dowgird Apr 14 '12 at 11:39
  • In which case you're making an exception for the first on, which is basically the same argument – NielsK Apr 14 '12 at 22:56
  • 2
    @NielsK, no, handling the first element specially is simpler, because you can do that and then enter a recursion or loop for the rest without any more special treatment. – ivant Apr 15 '12 at 05:59

3 Answers3

1

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.

sw1nn
  • 7,278
  • 1
  • 26
  • 36
1

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")
NielsK
  • 6,886
  • 1
  • 24
  • 46
0

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.

dcow
  • 7,765
  • 3
  • 45
  • 65