PurelyFunctional.tv Newsletter 430: Use :: keyword notation for unique values

Sign up for weekly Clojure tips, software design, and a Clojure coding challenge.

Issue 430 - June 14, 2021 · Archives · Subscribe

Clojure Tip 💡

Use :: 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 namespace.

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 (::) notation:

(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 keyword changes:

(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 a ::not-found value? Good question. This code successfully distinguishes between three different cases:

  1. There is an address.
  2. There is a nil for the address.
  3. There is no value for :address.

As long as nothing else in the namespace might put ::not-found into 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.

Course 📚

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.

Podcast episode🎙

This week on the podcast, I explore the notion of fit and how it is missing from most discussions of design. Listen here.

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

Issue 429

This week's challenge

New Numbers

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.

Examples

(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.

Rock on!
Eric Normand