3

Clojure: Common beginner mistakes (WIP)

 2 years ago
source link: https://blog.jakubholy.net/clojure-common-beginner-mistakes/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Clojure: Common beginner mistakes (WIP)

September 18, 2019

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

Are you benefitting from my writing? Consider buying me a coffee or supporting my work via GitHub Sponsors. Thank you! You can also book me for a mentoring / pair-programming session via Codementor or (cheaper) email.

Allow me to write to you!

Let's get in touch! I will occasionally send you a short email with a few links to interesting stuff I found and with summaries of my new blog posts. Max 1-2 emails per month. I read and answer to all replies.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK