2

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)))
2
  • Something seems wrong. In assign-santas, the santas argument is not used, the first let binding is a no-op since it is closed on the same line, and map doesn't actually map over a collection. I recommend stripping this code down further, that way you'll probably find the answer by yourself quickest. Commented Dec 31, 2015 at 15:51
  • yeah that is absolutely a problem - I wasn't supplying santas to the map. Not the problem, but certainly a problem :) Commented Dec 31, 2015 at 17:25

3 Answers 3

5

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)))
Sign up to request clarification or add additional context in comments.

3 Comments

That solution worked well - now I just need to dig into it and understand it :)
You could do (partition 2 1 (take 1 rand-santas) rand-santas), to get the ending items paired as well.
Note that this algorithm will never give mulitple "cycles". For example, if there are 6 santas a, b, c, d, e, and f, then the solution will never propose a-b-c-a; d-e-f-d.
4

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.

Comments

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.

4 Comments

Wow! Thanks a ton for the in-depth walk-through; I really appreciate it. Coming from JS (even functional JS), Clojure is a bit of a brain twister.
You're welcome. I think getting my brain twisted occasionally is a Good Thing - just not at the time. :-) The previous language that forced me to really learn a New Way of Doing Things was Smalltalk. I'd pick it up, try it out, put it down - rinse, repeat - until after several years of on-again, off-again messing with it I finally Got It. Note: it's syntax and approach are far different than most empirical languages. It was hard to learn. It taught me more than just about any other language. Clojure is having a similar effect. Hmmmmm.... :-) Best of luck.
What is an empirical language? Did you mean imperative?
(sigh) You are, of course, correct - I meant 'imperative'. (Next time I'll probably put it down as 'imperial'. Too many words, too little time... :-)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.