PurelyFunctional.tv Newsletter 430: Use :: keyword notation for unique values
Sign up for weekly Clojure tips, software design, and a Clojure coding challenge.
Clojure Tip 💡
:: keyword notation for unique values
I'm not really a big fan of the
::keyword notation. Yes, it is much
shorter than writing out the current namespace every time. But the code
is not mobile. I only use it for keywords that won't escape the
Clojure's syntax allows you to write a namespace-qualified keyword in a
number of ways. If you want to qualify a keyword with the current
namespace, the shortest way to do it is with the double-colon (
(ns com.lispcast.users) ::user-id ;=> :com.lispcast.users/user-id
That feels like a great savings. Namespaces tend to be long and with a single character, we can refer directly to it. Since we are encouraged to qualify keywords with globally unique namespaces, it seems like a big win.
I'm not sure that it is a win. I often start a project with a single namespace and gradually migrate code into secondary namespaces as the original namespace grows. This practice is common. However, double-colon keywords don't migrate. Whatever namespace they happen to be in is the namespace that qualifies them. That means that moving code from one namespace to another, if it contains a double-colon keyword, changes meaning. Here's an example:
Imagine I have an e-commerce app I've been working on. Since it's new,
it's all in one namespace,
com.lispcast.core. It's now time to
organize it into new namespaces because it's getting big. Here's one
function I have:
(ns com.lispcast.core) ;; :com.lispcast.core/discount (defn compound-discount [item percent] (update item ::discount (fnil + 0) percent))
Now I want to move
compoud-discount into a new namespace called
com.lispcast.items. If I move the function into the new namespace, the
(ns com.lispcast.items) ;; :com.lispcast.items/discount (defn compound-discount [item percent] (update item ::discount (fnil + 0) percent))
If the namespaced keyword is supposed to be globally unique and fixed, I've just broken that. If I use double-colon keywords, I'll have to be very careful every time I move code around or copy-paste. The tedium is not worth it. Just on a time efficiency basis, I've saved typing a few characters but it costs me lots of time to thoroughly scan code every time it changes namespaces. No thanks.
If a namespace is globally unique and fixed for all time, it will outlast any organization scheme.
Despite this problem, there is one case where I think double-colon namespaces are quite useful and not dangerous. Very often, you want a globally unique value. It doesn't quite matter what the value is as long as it's different from every other value that might be in use. Here's an example:
(case (get person :address ::not-found) ::not-found (do-not-found-case) nil (do-nil-case) (do-found-case))
This code will get the address of a person record or return
::not-found if the
:address key is not in the
person map. Why use
::not-found value? Good question. This code successfully
distinguishes between three different cases:
- There is an address.
- There is a
nilfor the address.
- There is no value for
As long as nothing else in the namespace might put
the map at the
:address key, this is very safe. Outside of the
namespace, code is very unlikely to use this qualified keyword. And we
can easily ensure that nothing in the namespace is using it. Further,
::not-found will not leave the namespace. It's not a global
identifier for an idea. It's a local identifier. Finally, the code is
mobile. If I move this into another namespace, the same things are true:
nothing is going to put that into the
:address field, inside or
outside the namespace, and it is entirely local.
Shortcuts are good, but sometimes they hide costs that are bigger than the savings. If we're writing code that needs to be mobile, we should divorce the choice of namespaces that should be unique (like keywords) from the temporary organization of code (like libs). Only in cases where the keyword does not leave the namespace is it safe to use the bare double-colon keyword.
The Beginning Clojure Video Course Bundle is a great place to get started with your Clojure journey. It contains 8 courses and is discounted more than 20% if you buy the bundle versus buying the courses individually. There's no magic in it. I simply looked at what professional Clojurists do and what beginners do, and I taught the difference. If that sounds like it's for you, check it out.
Book update ✍️
Grokking Simplicity is now available on Amazon! People have already been receiving their copies.
Please, if you like the book and/or believe in its mission of starting a discussion about the practice of FP in the industry, please leave a 5-star review. Reviews will help people learn whether the book is good before they buy. I'm up to four reviews so far.
You can also get a copy of Grokking Simplicity at Manning's site. There you can use the coupon TSSIMPLICITY for 50% off.
Quarantine update 😷
I know a lot of people are going through tougher times than I am. If you, for any reason, can't afford my courses, and you think the courses will help you, please hit reply and I will set you up. It's a small gesture I can make, but it might help.
I don't want to shame you or anybody that we should be using this time to work on our skills. The number one priority is your health and safety. I know I haven't been able to work very much, let alone learn some new skill. But if learning Clojure is important to you, and you can't afford it, just hit reply and I'll set you up. Keeping busy can keep us sane.
Stay healthy. Wash your hands. Wear a mask. Take care of loved ones.
Clojure Challenge 🤔
Last issue's challenge
- Headline Scroller - Submissions
This week's challenge
A number is new if its digits are not a permutation of a smaller number. For instance, 789 is a new number because its permutations (879, 798, 897, 978, and 987) are all larger than it is. However, 645 is not a new number since 456 and 465 are smaller than it.
Write a function that takes an integer and returns true if it is a new number and false otherwise.
(new-number? 789) ;=> true (new-number? 645) ;=> false (new-number? 444) ;=> true (it's permutations are not smaller than it)
Bonus: You may find a clever way to write
new-number?. In addition to
that implementation, implement it in such a way that the definition (no
permutations are smaller) is clear from the code.
Thanks to this site for the problem idea, where it is rated Very Hard in Ruby. The problem has been modified.
Please submit your solutions as comments on this gist.