内容简介: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 todoall
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.
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。