Clojure Geek

Writing about learning Clojure

Getting the hang of (map)

Since Jan 2014 I’ve been working on Clojure as my “language of the year” to learn. Aside from a brief stint last spring when my company at the time switched to .NET (but then I came back to ruby) I have been keeping at it, learning here and there. Sadly I realized I need to write/read more code and less just reading/watching about it. Bridget Hillyer leads a study group for ladies learning clojure online (we meet in irc) and we want to make a site to help us keep track of recipes as a project to practice clojure. Initially we will just have a list with name, source and url. First versions of the app just created recipes into maps/vector. Now we want to have an API with a database so I started playing with Korma and Liberator these past few weeks. I was wanting a quick way to add a bunch of records for testing and then remove them.

1
2
3
4
5
6
7
(defn current-time []
  (Timestamp. (.getTime (Date.))))

(defn add-recipe [data]
  (let [{:keys [name source url]} data]
    (insert recipe (values {:name name :source source :url url :created_at
(current-time)}))))

Ok I have a function to create a recipe.

Now to add a bunch of recipes

1
2
3
4
5
6
7
8
9
(def sample-data
  [{:name "Hard Boiled Eggs" :url "www.eggs.com"   :source "Mom"}
   {:name "Grilled Cheese"   :url "www.cheese.com" :source "Ultra Foods"}
   {:name "Sliced Bread"     :url "www.bread.com"  :source "Bread for you"}
   {:name "Pizza"            :url "www.pizza.com"  :source "Good Recipes"}])

(defn load-sample-data []
  (for [recipe sample-data]
      (add-recipe recipe)))

What about a function to add a bunch of data without for

1
2
3
4
(defn add-recipes [datas]
  (map add-recipe datas))

(add-recipes sample-data)

Now I wanted to insert the word TEST in front of the name, so I could easily remove the test recipes. I know I could probably switch out databases maybe or something more awesome but just let me play here..

1
2
3
4
5
6
7
8
(defn delete-test-data []
  (delete recipe
    (where {:name [like "TEST%"]})))

(defn load-sample-data2 []
  (for [recipe sample-data]
    (let [{:keys [name url source]} recipe]
      (add-recipe {:name (str "TEST " name) :url url :source source}))))

When I wrote it, I knew it was looooong and not the best way. But blame my ruby-make-it-work-refactor-rinse-repeat roots but hey it works. Now to make it better!

Super excited, I show Bridget at our weekly study group meeting. I told her I know there is a one liner, but I got this one working.. I’m trying assoc-in but can’t get it quite, she said look at map .. you’ll need a couple of maps. So that was enough of a hint to set me in the right direction.

Starting with a simpler example and playing in the repl, I came up with:

1
2
3
4
5
recipe-api.sample-data> (def data [{:name "nola"}, {:name "bob"}])
#'recipe-api.sample-data/data

recipe-api.sample-data> (map #(str "TEST " (:name %)) data)
("TEST nola" "TEST bob")

Thinking the road to completion of this function was near, I attempt to apply this to my map structure with all the info, hmm hmm hmm. Was tough to get it all in on function. So, I decided to split it up. Adding TEST to one recipe, then applying that to a vector of recipes.

1
2
(defn testify-data [data]
   (merge data (hash-map :name (str "TEST " (:name data) ))))

I pull out the name from the data, made a new map with the modified name then smooshed them together returning a new data structure with merge.

Then to apply that to a bunch of data, then add all of them (maybe this is not necessary to be in its own function but I’m getting the hang of map here:

1
2
3
4
(defn testify-all-data [datas]
  (map #(testify-data %) datas))

(add-recipes (testify-all-data sample-data))

Ok thats was fun.

1
2
3
4
5
6
 id |         name          |      url       |    source     | created_at
----+-----------------------+------------------------+---------------+----------------------------
 45 | TEST Hard Boiled Eggs | www.eggs.com   | Mom           | 2015-02-06 08:21:47.15-06
 46 | TEST Grilled Cheese   | www.cheese.com | Ultra Foods   | 2015-02-06 08:21:47.271-06
 47 | TEST Sliced Bread     | www.bread.com  | Bread for you | 2015-02-06 08:21:47.273-06
 48 | TEST Pizza            | www.pizza.com  | Good Recipes  | 2015-02-06 08:21:47.276-06

But how I arrived at these was a learning process. Thanks to Bridget for the hint to use map. :)

Any suggesting on writing it differently? I’d lean towards readability over perl golf anyday!

I know I barely scratched the surface of map but its more than I knew before, so progress! :)

Update: Got a great suggestion from @stijlist

1
2
map can take the name of a fn on its own `(map testify-data datas)`
 instead of wrapping with `#()`

Comments