Idiomatic Clojure: Code Smells

栏目: IT技术 · 发布时间: 4年前

内容简介:We all make mistakes. We have all written bad pieces of code at some point of our lives. They might have been non-idiomatic, unreadable, or had bad performance.This post catalogs all the little things which sit down at the bottom of your code, accumulate l

We all make mistakes. We have all written bad pieces of code at some point of our lives. They might have been non-idiomatic, unreadable, or had bad performance.

This post catalogs all the little things which sit down at the bottom of your code, accumulate like dust, and when taken in as a whole, clog down the code base. It's important to remember code is read more than it is edited, and is not only a way for us to communicate with the computer, but with other programmers (including ourselves in the future).

Hopefully the advice in this post will help you recognize all those little code smells and write more readable, maintainable code.

Happy hacking.

Mapping

mapcat To The Rescue

Sometimes you need to concat the results of mapping. Using mapcat is an idiomatic option for this case:

;;; Don't                       Do
(apply concat (map f xs))  =>  (mapcat xs)

What Is It For?

Avoid trivial for (list comprehensions)

;;; Don't                Do
(for [x xs] (f x))  =>  (map f xs)

And yes, I've seen a combination of both:

;;; Please Don't
(apply concat (for [x xs] (f x)))

Trivial Lambda

There's no reason to wrap a function in an anonymous function:

;;; Don't             Do
(map #(f %) xs)  =>  (map f xs)

Realize When Collections Want To Be Associative

If you find yourself scanning collections of maps looking for a map where a certain key has a certain value, your collection might be telling you it wants to be associative, not sequential:

(def people
  [{:person/name "Fred"}
   {:person/name "Ethel"}
   {:person/name "Lucy"}])

(defn person-in-people?
  [person people]
  (some #(= person (:person/name %)) people))

(person-in-people? "Fred" people);; => true
(person-in-people? "Bob" people) ;; => nil

Instead, index the collection according to the parameter which interests you:

(def collected-people
  (clojure.set/index people [:person/name]))

(contains? collected-people {:person/name "Fred"});; => true
(contains? collected-people {:person/name "Bob"}) ;; => false

This code is readable: "Does collected-people contain a person who's name is Bob?".

You can also index it yourself:

(defn index
  [xs k]
  (reduce
   (fn [m x]
     (let [ik (get x k)]
       (assoc m ik (conj (get m ik #{}) x)))) {} xs))

(def collected-people
  (index people :person/name))

(contains? collected-people "Fred");; => true
(contains? collected-people "Bob") ;; => false

If you repeatedly filter according to certain attributes, you might want to index according to them, too, with group-by or clojure.set/index .

An important distinction between group-by and index, is that group-by indexes according to a function, so you can presumably bin values as well, for example, by age or height, while index works specifically with keys.

Filtering

This problem is similar to a trivial lambda, just wrapped in negation. Remember remove is the negated case of filter :

;;; Don't                      Do
(filter #(not (p %)) xs)  =>  (remove p xs)
(remove #(not (p %)) xs)  =>  (filter p xs)

Sequence navigation

Sometimes you need to dig into elements in a sequence. Plenty of users aren't familiar with the concise versions:

;;; Don't               Do
(first (first xs)) =>  (ffirst xs)
(first (next xs))  =>  (fnext xs)
(next (first xs))  =>  (nfirst xs)
(next (next xs))   =>  (nnext xs)

Sort of like cadr but makes more sense.

Emptiness

The concept of emptiness is tricky in Clojure. Did you know nil is also empty? What defines an empty sequence? How can we know if a lazy unrealized sequence is empty? Do we count it? What if it's infinite?

To begin with, let's look at what empty? actually does:

(defn empty?
  "Returns true if coll has no items - same as (not (seq coll)).
  Please use the idiom (seq x) rather than (not (empty? x))"
  [coll] (not (seq coll)))

If you have eyes to see, you notice the docstring and know exactly where this is going:

Don't use (not (empty? x)) !

You've probably seen or written at least one of the following examples:

;;; Don't                        Do
(when (not (empty? x)) ...)  => (when (seq x) ...)
(when-not (empty? x) ...)    => (when (seq x) ...)
(when (= 0 (count x)) ...)   => (when (empty? x) ...)
(when (< 0 (count x)) ...)   => (when (seq x) ...)

Into into

into is a pretty useful function, but one often abused.

The (mis)usage of into can usually be broken to three distinct cases:

Type Transformations

;;; Don't           Do
(into [] xs)   =>  (vec xs)
(into #{} xs)  =>  (set xs)

Map Mapping

;;; Don't
(into {} (map (fn [[k v]] [k (f v)]) m))
(into {} (for [[k v] m] [k (f v)]))
;;; Do
(reduce-kv (fn [m k v] (assoc m k (f v))) {} m)
;;; Or faster* but less pretty
(persistent!
 (reduce-kv (fn [m k v] (assoc! m k (f v))) (transient {}) m))

Don't write it out manually every time. Turn it into a function and throw it into one of half-dozen util namespaces.

NOTE: the benefits from the transient version increase with the size of the input. For very small maps, the non-transient version is faster.

Not Using The Transducer API

;;; Don't                       Do
(into coll (map f xs))     =>  (into coll (map f) xs)
(into coll (filter p xs))  =>  (into coll (filter p) xs)

Working With Maps

The Value Of Nothing

Clojure maps are collections, not slots . Combined with nil 's meaning being "nothing", nil values inside maps are confusing:

(get {:a nil} :a) ;; => nil
(get {:b 111} :a) ;; => nil

Try to avoid inserting nil values into a map.

If it is not a map you control, you can always prune it.

Both implementations are valid:

(defn assoc-some [m k v] (if (nil? v) m (assoc m k v)))
(defn prune-nils [m] (reduce-kv assoc-some {} m))

(prune-nils {:a 1 :b nil :c 2 :d nil}) ;; => {:a 1, :c 2}

(defn remove-nil [m k v] (if (nil? v) (dissoc m k) m))
(defn prune-nils [m] (reduce-kv remove-nil m m))

(prune-nils {:a 1 :b nil :c 2 :d nil}) ;; => {:a 1, :c 2}

Beware of merge Where Performance Matters

merge is clear and easy to work with, but has terrible performance.

If you're more interested in the performance side of things, watch this talk .

Keep in mind while reading the following sections that using merge and select-keys comes at a price. While the general advice is to avoid doing things manually, every rule has an exception.

Be aware there are alternatives as well, but merge still performs poorly in general.

Thanks to Borkdude for highlighting this issue quickly.

Avoid Manual Merge

The below piece of code conditionally merges m2 into m1 when the values in m2 are not nil :

(let [m1 {}
      m1 (if (nil? (:k m2)) m1 (assoc m1 :k (:k m2)))])

Imagine it being done for every key in m2 . There can be 20 Keys. Who can even make sense of the important parts of the code afterwards?

Instead, combine nil pruning with merge, as two steps of understandable data transformations:

(merge m1 (prune-nils m2))

Avoid Manual Key Selection

I see this usually going hand-in-hand with manual merges:

{:a (:a m1)
 :b (:b m2)}

Again, this can usually involve pretty big maps. Instead, try to:

(merge (select-keys m1 [:a])
       (select-keys m2 [:b]))

Conditional Build-Up

I often see this pattern repeating itself:

(defn foo
  [in]
  (let [m {:k0 (f0 in)} ;; mandatory
        m (if (p1 in) (assoc m :k1 (f1 in)) m) ;; optional
        m (if (p2 in) (assoc m :k2 (f2 in)) m)]
    m))

Instead, use cond-> :

(defn foo
  [in]
  (cond-> {:k0 (f0 in)} ;; mandatory
    (p1 in) (assoc :k1 (f1 in)) ;; optional
    (p2 in) (assoc :k2 (f2 in))))

This way, flow control turns into syntax and there's no state to keep track of.

Numbers!

Clojure has functions covering some common use cases when working with numbers which both perform and convey intent better.

Absolute Zero

;;; Don't     Do
(= 0 x)  =>  (zero? x)
(> x 0)  =>  (pos? x)
(< x 0)  =>  (neg? x)

One Away

;;; Don't     Do
(+ 1 x)  =>  (inc x)
(- 1 x)  =>  (dec x)

Truth Be Told

Same case with numbers, no need to compare to booleans and nil.

;;; Don't          Do
(= true x)   =>   (true? x)
(= false x)  =>   (false? x)
(= nil x)    =>   (nil? x)

doall

doall is a macro which forcefully realizes lazy sequences. It should not be used in production.

See my previous posts regarding alternative to doall in asingle threaded and amulti-threaded context.

on a side note, I hope you never see something like: (doall (doseq [x xs] ..)) . It's wrong on two levels

  • doseq is already strict
  • doseq returns nothing, so there's nothing to doall

Style

Implicit do blocks

Some expressions have implicit do blocks in them, making it unnecessary to use a do block.

when

;;; Don't
(when test
  (do expr1
      expr2))

;;; Do
(when test
  expr1
  expr2)

let

;;; Don't
(let bindings
  (do expr1
      expr2))

;;; Do
(let bindings
  expr1
  expr2)

Function body

;;; Don't
(fn []
  (do expr1
      expr2))

;;; Do
(fn []
  expr1
  expr2)

More

Same goes for try , catch , and any other macro with a type signature & body .

Threading

Avoid trivial threading:

(-> x f)     => (f x)
(-> x (f a)) => (f x a)

And remember to thread with style :

->
->>

Thread Instead Of A Deep Call Stack

Deep call stacks tie implementations together and make testing more difficult as the input space becomes larger instead of smaller by successive function calls.

They take atomic and understandable data transformation functions and make them opaque.

;;; Don't
(defn h [x] ...)
(defn g [x] ... (h x))
(defn f [x] ... (g x))

;;; Do
(-> x f g h)

some->

Nested when/let can be converted to some->

;;; Don't
(when-let [x1 (f0 x0)]
  (when-let [x2 (f1 x1)]
    (when-let [x3 (f2 x2)]
      (f3 x3))))

;;; Do
(some-> x0 f0 f1 f2 f3)

Same goes for this structure:

;;; Don't
(let [x (if (nil? x) nil (x f0))
      x (if (nil? x) nil (x f1))
      x (if (nil? x) nil (x f2))]
  (if (nil? x) nil (f3 x)))

;;; Do
(some-> x0 f0 f1 f2 f3)

cond->

(let [x (if (p0 x) (f0 x) x)
      x (if (p1 x) (f1 x) x)]
  (if (p2 x) (f2 x) x))
(cond-> x
  (p0 x) f0
  (p1 x) f1
  (p2 x) f2)

The only subtle difference to keep in mind that the predicates can't depend on the result of the previous computation stage.

We can achieve that behavior with a slight modification to cond-> :

(defmacro cond*->
  [expr & clauses]
  (assert (even? (count clauses)))
  (let [g (gensym)
        steps (map (fn [[test step]] `(if (-> ~g ~test) (-> ~g ~step) ~g))
                   (partition 2 clauses))]
    `(let [~g ~expr
           ~@(interleave (repeat g) (butlast steps))]
       ~(if (empty? steps)
          g
          (last steps)))))

(cond*-> x
         p0 f0
         p1 f1
         p2 f2)

let Shadowing

There's usually very little reason to shadow a binding inside a let body:

(let [x (f0 x)
      x (f1 x)
      x (f2 x)]
  (f3 x))

Should be:

(-> x f0 f1 f2 f3)

Nested Forms

Plenty of macros with binding forms don't need to be nested:

;;; Don't
(let [x 1]
  (let [y 2]
    [x y]))

;; Do
(let [x 1
      y 2]
  [x y])

A special case is that of nested iteration:

;;; Don't
(doseq [x xs]
  (doseq [y ys]
    (println x y)))

;;; Do
(doseq [x xs
        y ys]
  (println x y))

Trivial Mapping Wrappers

We can sometimes find the following form in code, of do-one-thing then do-one-thing-n-times:

(defn build-person [x] ...)
(defn build-persons [xs] (map build-person xs))

It may not be immediately apparent why it's a code smell.

build-persons
comp

Positional Arguments

Avoid An Explosion In Input Arguments

If your functions take a very large number of arguments, suspect they should be broken apart, because it's unlikely all arguments are used simultaneously.

(defn f
  [fizz buzz foo bar quux ohgod enough])

Prefer A map To Key-Value Rest Arguments

(defn f
  ([x y]
   (f x y :a 1 :b 2))
  ([x y & {a :a b :b}]
   ,,,))

(f x y :a 3 :b 4)

;;; vs.

(defn f
  ([x y]
   (f x y {:a 1 :b 2}))
  ([x y {a :a b :b}]
  ,,,))

(f x y {:a 3 :b 4})

The main reason for this is when you wish to pass the function around as a higher order function you need to be mindful of the type of the tail arguments, make sure they always get unpacked, and remember to usually apply everywhere. This is easily avoided by using the second style.

Prefer Returning Maps Over Returning Positional Values

Using positional return values encodes meaning to indices, giving semantic or business meaning to indices/ordering. It's better to encode that meaning as explicit keywords:

(defn sieve
  [p xs]
  [(filter p xs) (remove p xs)])

(first (sieve even? (range 9)))
;; => (0 2 4 6 8)

;;; vs.

(defn sieve
  [p xs]
  {:true (filter p xs) :false (remove p xs)})

(:true (sieve even? (range 9)))
;; => (0 2 4 6 8)

Afterword

This post and my advice are clearly opinionated. If you disagree with anything I've said, found a mistake, or think I missed important bits, feel free to shout at me on any of the social media platforms I'm on.


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

浪潮之巅(第2版)(套装上下册)

浪潮之巅(第2版)(套装上下册)

吴军 / 人民邮电出版社 / 2013-7 / 80.00元

一个企业的发展与崛起,绝非只是空有领导强人即可达成。任何的决策、同期的商业环境,都在都影响着企业的兴衰。《浪潮之巅》不只是一本历史书,除了讲述科技顶尖企业的发展规律,对于华尔街如何左右科技公司,以及金融风暴对科技产业的冲击,也多有着墨。此外,这本书也着力讲述很多尚在普及或将要发生的,比如微博和云计算,以及对下一代互联网科技产业浪潮的判断和预测。因为在极度商业化的今天,科技的进步和商机是分不开的。 ......一起来看看 《浪潮之巅(第2版)(套装上下册)》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具