Clojure: Common beginner mistakes (WIP)

  1. Style
    1. Naming
    2. From the unofficial Clojure style guide
  2. Namespace declaration
  3. State
    1. Use let instead of def for local state
  4. Functions
    1. Use recur for recursive calls to avoid blowing the stack
    2. Prefer higher-level sequence functions over low-level recursion
    3. Prefer = over ==
    4. Use functions directly in map/filter/…​
  5. Trivialities
    1. Too much code on a single line
    2. Overusing “private”
  6. Not embracing the platform
  7. Additional resources

Common mistakes made by Clojure beginners and style recommendations.

Style

Naming

  1. Use kebab-case-names, not camelCase or something else (unless you have some interop reasons)

  2. End predicates with ?, i.e. isAlienalien?

  3. Don’t add unnecessary prefixes to function names, such as get- (this comes likely from the Java Bean standard and has no place here) or compute- (most functions compute something!). Use digits for fn that turns a number into a sequence of digits, armstrong-num for fn that computes the Armstrong Number, and armstrong? for fn that tells you whether a number is an Armstrong number. See also Stuart Sierra’s great How to Name Clojure Functions.

Namespace declaration

Use (ns ... (:require ...)) instead of (require ...) or (use ...). Only exceptionally do :refer :all and only use :refer [..] for frequently used symbols, i.e. prefer using an alias (e.g. [clojure.set :as set]) - typically the last part of the namespace name or a well-established abbreviation.

(require is intended for use in the REPL, not in namespace files.)

State

Use let instead of def for local state

def, defn etc. should only ever be used as a top level form (barring few exceptions such as comment and troubleshooting during REPL-driven development). Even if you use them inside a function, they still create globally visible Vars. Use let for local state inside a function.

Functions

Use recur for recursive calls to avoid blowing the stack

Clojure lacks (for good reasons and due to JVM limitations) automatic tail-call optimization (TCO) and therefore calling a function from its body runs into the risk of StackOverflowError when recursing too many times. Use recur (perhaps together with loop) to explicitly mark the call for TCO and avoid the risk.

Prefer higher-level sequence functions over low-level recursion

We rarely use recursion directly in Clojure because we can often achieve the desired result on a higher level by leveraging existing sequence functions, often reduce. (That themselves are implemented with recursion.) It makes for a clearer code.

(defn exp [x n]
  (loop [res 1, pow n]
    (case pow
      0 1
      1 res
      (recur
        (* res x)
        (dec pow)))))
;; =>
(defn exp [x n] (reduce * (repeat n x)))

Prefer = over ==

In Clojure it is most common to use just = for equality comparison. Only use == if you really need it. Even if you come from JavaScript :-).

An example when this might be useful is when comparing a BigDecimal and a constant to avoid the need to manually wrap the constant with (bigec) (Clojure will do it for you).

Use functions directly in map/filter/…​

Remember that functions are first-class citizens and can be send as arguments:

;; WRONG:
(map #(compute-age %) people)
;; CORRECT:
(map compute-age people)

Trivialities

Too much code on a single line

Don’t be afraid to split a line for readability, to visually separate individual important arguments. Otherwise it might be difficult to spot the individual arguments in the flood of the text. Ex.:

(reduce (fn [a b] (+ a (exp b (first res)))) 0 (rest res)))
;; =>
(reduce
  (fn [a b] (+ a (exp b (first res))))
  0
  (rest res)))

Overusing “private”

If you come from Java, you are used to making everything private. In Clojure we use defn- and ^private sometimes but much less than in Java. Clojure targets trustworthy developers and allows them to shoot themselves into their foot if they really want to. Think about who do you want to protect from what with the privacy setting and whether it is necessary - especially if the function is essentially generic data transformation function. It is common to keep functions that are implementation details subject to change at any time and without notice into a separate *.impl.*/,internal.* namespace to mark them clearly as such and keep them public.

Public vs. Private Functions Clojure is biased toward making data and functions available by default. However, most namespaces have functions that are used as helpers or never intended to be part of the public usage. When you’re defining the functions in a namespace, consider how a consumer will perceive those functions and is expected to use it. Some tools and conventions are private vars, documentation strings, and the namespace structure itself. [..] You may find any or all of these techniques useful in indicating to users of your own code where to start

Not embracing the platform

Clojure is a hosted language and you are expected to leverage the host platform. Sometimes there are convenience Clojure wrappers but most often you are expected to use the JVM and Java libraries directly. Know and don’t hesitate to use methods such as Integer/parseInt, Character/digit etc. In particular, do not misuse read-string for parsing.

Additional resources


Tags: clojure


Copyright © 2024 Jakub Holý
Powered by Cryogen
Theme by KingMob