Reputation: 13425
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
Reputation: 50017
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:
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))))`
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.
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
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
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