PurelyFunctional.tv Newsletter 314: Collection functions vs. sequence functions

Issue 314 - February 18, 2019 · Archives · Subscribe

Clojure Tip 💡

Use -> for collection functions and ->> for sequence functions.

Many people complain that Clojure's argument order is inconsistent. Take the example of map. The function comes before the sequence.

(map inc [1 2 3]) ;; function then sequence

But in update, the collection comes first, and the function is last:

(update [1 2 3] 2 inc) ;; collection, key, then function

This seeming inconsistency is actually deliberate. It indicates two distinct classes of operation. Let's group some function by whether they take the collection at the beginning or end.

1. At the beginning

  • update (and update-in)
  • assoc (and assoc-in)
  • dissoc
  • conj

2. At the end

  • map
  • filter
  • take
  • drop
  • cons

When we group them like this, some other differences become apparent. The functions in group 2 all return a sequence. On the other hand, functions in group 1 return a collection of the same type as the argument.

It turns out that the functions in group 2 are sequence functions. They take seqables and return sequences. When you pass them a collection, seq is called implicitly. The functions in group 1 are collection operations. These functions take a collection and return a collection of the same type. However, they may not work for all collections. For instance, dissoc does not work on vectors.

As Clojurists, we recognize the differences and how best to use them.

For example, collection functions do well with the -> (thread first) macro.

(-> {}
  (assoc :a 1)
  (assoc :b 2)
  (update :a inc))

Whereas the sequence functions do well with the ->> (thread last) macro.

(->> (range)
  (filter even?)
  (map #(* % %))
  (take 10))

More importantly, collection functions work well with the reference modifier operations. swap!, alter!, and send for atoms, refs, and agents, respectively. The argument order matches the order expected by those functions. That lets us neatly do:

(swap! counts update :strawberry + 3) ;; count 3 more strawberries

If you've ever tried using sequence functions with swap!, it's a lot more awkward. Give it a shot at the REPL.

If you learn better in video form, I've got a video on the topic. Or you can read more about collections in general. Do you know any other tips for using collection functions vs. sequence functions? Let me know and I'll share your answer.

PurelyFunctional.tv Update 💥

There are 2 updates to report this week.

1. reCaptcha is no longer

My Chinese Clojurist friends should know that I've gotten rid of reCaptcha for registering a new user. There is now a custom robot filter. Sorry it has taken so long! Let's hope it holds up against the torrents of bots. Please sign up for a membership if you couldn't before.

2. Better registration form

I've revamped the layout of the registration page where you can sign up for a membership. It should be easier to fill in. If it was holding you back, go forth and register!

Clojure Media 🍿

I attended IN/Clojure last month and it was amazing. I really appreciated meeting so many enthusiastic Clojurists from the other side of the world from me. Thanks for the invitation. You can catch my talk on design in Clojure and watch all of the other talks on YouTube. We're lucky to have such a great conference as part of the lineup.

Brain skills 😎

When you're learning some new Clojure stuff, jump into a REPL and get active. What better way to learn a skill than by doing it. I like to use a Leiningen plugin called lein try that lets you try out a new library from a REPL without creating a new project.

After installing it, you can test out clj-http like this:

$CMD lein try clj-http "3.9.1"

You can install lein try here.

The Clojure command-line interface lets you do something similar. There's no setup, but the command is longer:

$CMD clj -Sdeps '{:deps {clj-http {:mvn/version "3.9.1"}}}'

clj also has the advantage of being able to load libraries from git. Learn more about the CLI here.

Clojure Teaser 🤔

Answer to last week's puzzle

Here's the faulty code:

(def counter (atom 0))

(defn print-counter []
  (let [counter @counter]
    (println "===========")
    (println "| COUNTER |")
    (println (format "| %07d |" @counter))
    (println "==========")))

The reason it was throwing an exception was that I was derefing the value twice. One in the let binding and one in the body. The reason the exception was about casing to a Future is that deref has two branches. (See the deref source code if you want.) The first branch is for when the argument is an IDeref, the second when it's not. But in that second branch, it assumes it's a Future and casts. This is an example of what I would call an accidental error message. I don't know how much time I've spent looking for Futures in my code when it was just a stray @.

About a dozen people wrote in with the correct answer. You get gold stars! ⭐️⭐️⭐️ Some people suggested that spec could easily detect this error and give a better error message. I agree. Let's make it happen.

This week's challenge

The Sieve of Eratosthenes is a neat way to find all of the prime numbers below some number. If you do it on paper (and I suggest you do), you can easily find all primes below 100 within a few minutes. The challeng e this week is to write a version in Clojure that lets you calculate all the primes below a given number n.

The biggest challenge with this is that you might run into problems with stack overflows or speed. Make sure the algorithm can still run with n=100,000.

I'd love to see your answers. Bonus points for creative and innovative solutions. I'll share the best next week. Fire up a REPL, try it out, and send me your answers. (Please note that, unless you tell me otherwise, I will share your name in the newsletter if you send me an answer.)

See you next week!
Eric