Nick
Nick

Reputation: 9051

How to apply a function to a range in Clojure?

I'm new to Clojure and I tried to play with the example data from clojuredocs.org.

;; Data
(def scenes [{:subject "Frankie"
              :action "say"
              :object "relax"}

         {:subject "Lucy"
          :action "loves"
          :object "Clojure"}

         {:subject "Rich"
          :action "tries"
          :object "a new conditioner"}])

(defn play [scenes n]
  "Play a scene"
  (->>
   scenes
   (#(get % n))
   ((juxt :subject :action :object))
   (interpose " ")
   (apply str)))

The play function works fine:

my-stuff.core> (play scenes 0)
"Frankie say relax"
my-stuff.core> (play scenes 1)
"Lucy loves Clojure"
my-stuff.core> (play scenes 2)
"Rich tries a new conditioner"

This play-all function doesn't work:

(defn play-all [scenes]
  "Play all the scenes"
  (let [x (count scenes)]
    (for [n (range x)]
      (map play scenes n ))))

How to correct this play-all function, i.e. how to apply the play function to the range of data?

Upvotes: 1

Views: 841

Answers (3)

Thumbnail
Thumbnail

Reputation: 13473

Before we do anything with it, your play function should deal with a single scene:

(defn play [scene]
  "Play a scene"
  (->> scene
   ((juxt :subject :action :object))
   (interpose " ")
   (apply str)))

You use it like so:

(play (scenes 0))
;"Frankie say relax"

... which is no easier or harder than before. But

  • it will work with any scene and
  • the scenes don't have to be kept in a vector.

It also makes play-all simpler:

(defn play-all [ss]
  (map play ss))

(play-all scenes)
;("Frankie say relax" "Lucy loves Clojure" "Rich tries a new conditioner")

I was tempted to replace ((juxt :subject :action :object)) with vals in play, but we can't rely on the sequence order of the map entries.

Upvotes: 0

T.Gounelle
T.Gounelle

Reputation: 6033

map iterates over a collection (or several collections) to produce a sequence. for build a sequence from a list comprehension. In your case you can use either one or the other.

In terms of decomposition, it would make sens to have actually a function that plays one scene :

(defn play-one [scene]
  "Play a scene"
  (->>
   scene
   ((juxt :subject :action :object))
   (interpose " ")
   (apply str)))

Then playing the nth can use precedent definition:

(defn play-nth [scenes n]
  "Play the n(th) scene"
  (->
   scenes
   (#(get % n))
   play-one))

And you have several ways to play all the scenes:

(defn play-all-map1 [scenes]
  "Play all the scenes"
 (map (partial play-nth scenes) (range (count scenes))))

But you can really simplify since you don't need range because scenes can be treated as a sequence (assuming you are not interested in the index):

(defn play-all-map2 [scenes]
  "Play all the scenes with map"
  (map play-one scenes))

And with for:

(defn play-all-for [scenes]
  "Play all the scenes with for"
  (for [scene scenes]
    (play-one scene)))

Upvotes: 0

Dogbert
Dogbert

Reputation: 222198

You don't need both for and map.

With only for:

user=> (defn play-all [scenes]
  #_=>   "Play all the scenes"
  #_=>   (let [x (count scenes)]
  #_=>     (for [n (range x)]
  #_=>       (play scenes n ))))
#'user/play-all
user=> (play-all scenes)
("Frankie say relax" "Lucy loves Clojure" "Rich tries a new conditioner")

and with only map:

user=> (defn play-all [scenes]
  #_=>   "Play all the scenes"
  #_=>   (let [x (count scenes)]
  #_=>     (map #(play scenes %1) (range x))))
#'user/play-all
user=> (play-all scenes)
("Frankie say relax" "Lucy loves Clojure" "Rich tries a new conditioner")

(I prefer the latter.)

Edit: If you like ->>, this is even better:

user=> (defn play-all [scenes]
  #_=>   "Play all the scenes"
  #_=>   (->> scenes
  #_=>     (count)
  #_=>     (range)
  #_=>     (map #(play scenes %))))
#'user/play-all
user=> (play-all scenes)
("Frankie say relax" "Lucy loves Clojure" "Rich tries a new conditioner")

Upvotes: 2

Related Questions