Kevin Whitaker
Kevin Whitaker

Reputation: 13425

Clojure: map function isn't returning something I can eval

I'm writing a little "secret santa" program to get my hands dirty with Clojure, and I'm stumbling with my output.

The program takes a list of sets (Santas), extracts their emails into another list, then randomly assigns recipients to Santas. I think I've mostly got it, but when I try to output the results of my map, I'm getting #<Fn@dc32d15 clojure.core/map$fn__4549>,

(ns secret-santas-helper.core
  (:require [clojure.pprint :as pprint])
  (:gen-class))

(def santas [{:name "Foo" :email "[email protected]"}
             {:name "Bar" :email "[email protected]"}
             {:name "Baz" :email "[email protected]"}])

(defn pluck
  "Pull out the value of a given key from a seq"
  [arr k]
  (map #(get % k) arr))

(defn find-first
  "Find the first matching value"
  [f coll]
  (first (filter f coll)))

(defn assign-santas
  "Iterate over a list of santas and assign a recipient"
  [recipients santas]
  (let [r (atom recipients)])
  (map (fn [santa]
          (let [recipient (find-first #(= % (get santa :email)) @recipients)]
            (assoc santa :recipient recipient)
            (swap! recipients (remove #(= % recipient) recipients))))))

(defn -main []
  (let [recipients (shuffle (pluck santas :email))
        pairs (assign-santas recipients santas)]
      (pprint/pprint pairs)))

Upvotes: 2

Views: 114

Answers (3)

Lisp (in general) and Clojure (specific case) are different, and require a different way of approaching a problem. Part of learning how to use Clojure to solve problems seems to be unlearning a lot of habits we've acquired when doing imperative programming. In particular, when doing something in an imperative language we often think "How can I start with an empty collection and then add elements to it as I iterate through my data so I end up with the results I want?". This is not good thinking, Clojure-wise. In Clojure the thought process needs to be more along the lines of, "I have one or more collections which contain my data. How can I apply functions to those collections, very likely creating intermediate (and perhaps throw-away) collections along the way, to finally get the collection of results I want?".

OK, let's cut to the chase, and then we'll go back and see why we did what we did. Here's how I modified the original code:

(def santas [{:name "Foo" :email "[email protected]"}
             {:name "Bar" :email "[email protected]"}
             {:name "Baz" :email "[email protected]"}])

(def kids [{:name "Tommy" :email "[email protected]"}
           {:name "Jimmy" :email "[email protected]"}
           {:name "Jerry" :email "[email protected]"}
           {:name "Johny" :email "[email protected]"}
           {:name "Juney" :email "[email protected]"}])

(defn pluck
  "Pull out the value of a given key from a seq"
  [arr k]
  (map #(get % k) arr))

(defn assign-santas [recipients santas]
  ; Assign kids to santas randomly
  ; recipients is a shuffled/randomized vector of kids
  ; santas is a vector of santas

  (let [santa-reps  (inc (int (/ (count recipients) (count santas))))  ; counts how many repetitions of the santas collection we need to cover the kids
        many-santas (flatten (repeat santa-reps santas))]              ; repeats the santas collection 'santa-reps' times
    (map #(hash-map :santa %1 :kid %2) many-santas recipients)
  )
)

(defn assign-santas-main []
  (let [recipients (shuffle (pluck kids :email))
        pairs (assign-santas recipients (map #(%1 :name) santas))]
      ; (pprint/pprint pairs)
      pairs))

I created a separate collection of kids who are supposed to be assigned randomly to a santa. I also changed it so it creates an assign-santas-main function instead of -main, just for testing purposes.

The only function changed is assign-santas. Instead of starting with an empty collection and then trying to mutate that collection to accumulate the associations we need I did the following:

  1. Determine how many repetitions of the santas collection are needed so we have have at least as many santas as kids (wait - we'll get to it... :-). This is just

    TRUNC(#_of_kids / #_of_santas) + 1

or, in Clojure-speak

`(inc (int (/ (count recipients) (count santas))))`
  1. Create a collection which the santas collection repeated as many times as needed (from step 1). This is done with

    (flatten (repeat santa-reps santas))

This duplicates (repeat) the santas collection santa-reps times (santa-reps was computed by step 1) and then flatten's it - i.e. takes the elements from all the sub-collections (try executing (repeat 3 santas) and see what you get) and just makes a big flat collection of all the sub-collection's elements.

  1. We then do

    (map #(hash-map :santa %1 :kid %2) many-santas recipients)

This says "Take the first element from each of the many-santas and recipients collections, pass them in to the anonymous function given, and then accumulate the results returned by the function into a new collection". (New collection, again - we do that a lot in Clojure). Our little anonymous function says "Create an association (hash-map function), assigning a key of :santa to the first argument I'm given, and a key of :kid to the second argument". The map function then returns that collection of associations.

If you run the assign-santas-main function you get a result which looks like

({:kid "[email protected]", :santa "Foo"}
 {:kid "[email protected]", :santa "Bar"}
 {:kid "[email protected]", :santa "Baz"}
 {:kid "[email protected]", :santa "Foo"}
 {:kid "[email protected]", :santa "Bar"})

(I put each association on a separate line - Clojure isn't so gracious when it prints it out - but you get the idea). If you run it again you get something different:

({:kid "[email protected]", :santa "Foo"}
 {:kid "[email protected]", :santa "Bar"} 
 {:kid "[email protected]", :santa "Baz"}
 {:kid "[email protected]", :santa "Foo"}
 {:kid "[email protected]", :santa "Bar"})

And so on with each different run.

Note that in the rewritten version of assign-santas the entire function could have been written on a single line. I only used a let here to break the calculation of santa-reps and the creation of many-santas out so it was easy to see and explain.

For me, one of the things I find difficult with Clojure (and this is because I'm still very much climbing the learning curve - and for me, with 40+ years of imperative programming experience and habits behind me, this is a pretty steep curve) is just learning the basic functions and how to use them. Some that I find handy on a regular basis are:

map
apply
reduce
  I have great difficulty remembering the difference between apply and
  reduce. In practice, if one doesn't do what I want I use the other.
repeat
flatten
interleave
partition
hash-map
mapcat

and of course all the "usual" things like +, -, etc.

I'm pretty sure that someone who's more expert than I am at Clojure (not much of a challenge :-) could come up with a way to do this faster/better/cooler, but this might at least give you a different perspective on how to approach this.

Best of luck.

Upvotes: 3

rabidpraxis
rabidpraxis

Reputation: 556

Also be careful on how you use map. You are returning the result of your swap! which I don't believe is what you are aiming at.

Keep working on getting your version compiling and functioning correctly. I wanted to give an alternative solution to your problem that works less with mutation and instead is focused on combining collections.

(def rand-santas
  "Randomize the current santa list"
  (shuffle santas))

(def paired-santas
  "Use partition with overlap to pair up all random santas"
  (partition 2 1 rand-santas))

(def final-pairs
  "Add the first in the list as santa to the last to ensure everyone is paired"
  (conj paired-santas (list (last rand-santas) (first rand-santas))))

(defn inject-santas 
  "Loop through all pairs and assoc the second pair into first as the secret santa"
  [pairs]
  (map 
    (fn [[recipent santa]]
      (assoc recipent :santa santa))
    pairs))

(defn -main [] 
  (pprint/pprint (inject-santas final-pairs)))

Upvotes: 5

noisesmith
noisesmith

Reputation: 20194

Your assign-santas function is returning a map transducer. When you apply map to a single argument, it returns a transducer that will perform that transform in a transducing context. Most likely you intended to provide a third arg, santas, to map over.

Inside the assign-santas function, you are using @ to deref a value that is not an atom. Perhaps you meant @r instead of @recipients, but your let block is stops too soon and doesn't yet provide the r binding to the rest of the function body.

Upvotes: 4

Related Questions