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
(andupdate-in
)assoc
(andassoc-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 seq
ables 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 deref
ing 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 Future
s 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