Clojure开发人员应避免的常见编程错误


92

Clojure开发人员常犯哪些常见错误,如何避免这些错误?

例如; Clojure的新手认为该contains?函数与java.util.Collection#contains。但是,contains?仅在与索引集合(如地图和集合)一起使用并且您正在寻找给定的键时,才可以类似地工作:

(contains? {:a 1 :b 2} :b)
;=> true
(contains? {:a 1 :b 2} 2)
;=> false
(contains? #{:a 1 :b 2} :b)
;=> true

与数字索引集合(向量,数组)一起使用时,contains? 检查给定元素是否在索引的有效范围内(从零开始):

(contains? [1 2 3 4] 4)
;=> false
(contains? [1 2 3 4] 0)
;=> true

如果给出列表,contains?则永远不会返回true。


4
仅供参考,对于那些正在寻找java.util.Collection#包含类型功能的Clojure开发人员,请查看clojure.contrib.seq-utils / includes吗?从文档中:用法:(包括coll x)。如果coll在线性时间内包含与x相等(等于=)的值,则返回true。
罗伯特·坎贝尔

11
你似乎已经错过了一个事实,即这些问题是社区维基

3
我喜欢Perl问题与其他所有问题都必须
Ether

8
对于Clojure开发人员要寻找的容器,我建议不要遵循rcampbell的建议。seq-utils早已被弃用,因此该功能从一开始就没有用。您可以使用Clojure的some函数,或者更好的是,仅使用contains自身。Clojure集合工具java.util.Collection(.contains [1 2 3] 2) => true
Rayne 2012年

Answers:


70

文字八进制

有一次,我正在读一个矩阵,该矩阵使用前导零来维持适当的行和列。从数学上讲这是正确的,因为前导零显然不会改变基础价值。尝试使用此矩阵定义var时,将神秘地失败:

java.lang.NumberFormatException: Invalid number: 08

这完全让我感到困惑。原因是Clojure将前导零的文字整数值视为八进制,而八进制中没有数字08。

我还应该提到Clojure通过0x前缀支持传统的Java十六进制值。您还可以通过使用“ base + r + value”表示法使用介于2到36之间的任何底数,例如2r10101036r16,它们是42个底数十。


尝试返回匿名函数文字中的文字

这有效:

user> (defn foo [key val]
    {key val})
#'user/foo
user> (foo :a 1)
{:a 1}

所以我相信这也会起作用:

(#({%1 %2}) :a 1)

但是它失败了:

java.lang.IllegalArgumentException: Wrong number of args passed to: PersistentArrayMap

因为#()阅读器宏被扩展为

(fn [%1 %2] ({%1 %2}))  

地图文字用括号括起来。由于它是第一个元素,因此将其视为一个函数(实际上是文字映射),但未提供必需的参数(例如键)。综上所述,匿名函数文本并没有扩大到

(fn [%1 %2] {%1 %2})  ; notice the lack of parenthesis

因此您不能使用任何文字值([] 、: a,4,%)作为匿名函数的主体。

评论中给出了两种解决方案。Brian Carper建议使用序列实现构造函数(数组映射,哈希集,向量),如下所示:

(#(array-map %1 %2) :a 1)

表明,你可以使用身份功能拆开包装,外层的括号:

(#(identity {%1 %2}) :a 1)

布莱恩的建议实际上使我陷入下一个错误。


认为哈希映射数组映射确定不变的具体映射实现

考虑以下:

user> (class (hash-map))
clojure.lang.PersistentArrayMap
user> (class (hash-map :a 1))
clojure.lang.PersistentHashMap
user> (class (assoc (apply array-map (range 2000)) :a :1))
clojure.lang.PersistentHashMap

虽然通常不必担心Clojure映射的具体实现,但是您应该知道,增长映射的函数(例如assocconj)可以采用PersistentArrayMap并返回PersistentHashMap,对于较大的映射,该函数执行得更快。


使用函数作为递归点而不是循环来提供初始绑定

当我开始时,我写了很多这样的函数:

; Project Euler #3
(defn p3 
  ([] (p3 775147 600851475143 3))
  ([i n times]
    (if (and (divides? i n) (fast-prime? i times)) i
      (recur (dec i) n times))))

实际上,对于此特定功能,循环会更加简洁明了:

; Elapsed time: 387 msecs
(defn p3 [] {:post [(= % 6857)]}
  (loop [i 775147 n 600851475143 times 3]
    (if (and (divides? i n) (fast-prime? i times)) i
      (recur (dec i) n times))))

请注意,我用循环+初始绑定替换了空参数“默认构造函数”函数体(p3 775147 600851475143 3)。在易复发,现在重新绑定环路绑定(而不是FN参数)和跳跃(而不是FN环)回递归点。


引用“幻像”变量

我说的是您可能在探索性编程期间使用REPL定义的var类型,然后在不知不觉中在您的源代码中引用。一切工作正常,直到您重新加载命名空间(也许通过关闭编辑器),然后发现整个代码中引用的一堆未绑定符号。当您进行重构时,将var从一个名称空间移动到另一个名称空间时,也会经常发生这种情况。


将for 列表理解视为必须的for循环

本质上,您是在基于现有列表创建一个惰性列表,而不是简单地执行受控循环。Clojure的doseq实际上更类似于命令式foreach循环构造。

关于它们与众不同的一个示例是使用任意谓词过滤要迭代的元素的能力:

user> (for [n '(1 2 3 4) :when (even? n)] n)
(2 4)

user> (for [n '(4 3 2 1) :while (even? n)] n)
(4)

它们不同的另一种方式是,它们可以对无限的惰性序列进行操作:

user> (take 5 (for [x (iterate inc 0) :when (> (* x x) 3)] (* 2 x)))
(4 6 8 10 12)

它们还可以处理多个绑定表达式,首先迭代最右边的表达式,然后向左移动:

user> (for [x '(1 2 3) y '(\a \b \c)] (str x y))
("1a" "1b" "1c" "2a" "2b" "2c" "3a" "3b" "3c")

也没有任何中断继续过早退出。


过度使用结构

我来自OOPish背景,因此当我开始使用Clojure时,我的大脑仍在思考物体。我发现自己将所有事物都建模为结构,因为它的“成员”分组无论多么松散,都让我感到很舒服。实际上,大多数情况下应将结构视为优化。Clojure将共享密钥和一些查找信息以节省内存。您可以通过定义访问器来进一步优化它们,以加快密钥查找过程。

总体而言,除了性能之外,在映射上使用结构不会获得任何好处,因此增加的复杂性可能不值得。


使用不敏感的BigDecimal构造函数

我需要大量的BigDecimals,并且正在编写如下的丑陋代码:

(let [foo (BigDecimal. "1") bar (BigDecimal. "42.42") baz (BigDecimal. "24.24")]

实际上,Clojure通过将M附加到数字来支持BigDecimal文字:

(= (BigDecimal. "42.42") 42.42M) ; true

使用加糖版本可以消除很多膨胀。在评论中,twils提到您还可以使用bigdecbigint函数更加明确,但仍保持简洁。


对名称空间使用Java包命名转换

实际上,这本身并不是一个错误,而是与典型Clojure项目的惯用结构和命名背道而驰的。我的第一个重要的Clojure项目具有名称空间声明-以及相应的文件夹结构-如下所示:

(ns com.14clouds.myapp.repository)

这使我完全合格的函数引用变得:肿:

(com.14clouds.myapp.repository/load-by-name "foo")

为了使事情更加复杂,我使用了标准的Maven目录结构:

|-- src/
|   |-- main/
|   |   |-- java/
|   |   |-- clojure/
|   |   |-- resources/
|   |-- test/
...

它比以下“标准” Clojure结构更复杂:

|-- src/
|-- test/
|-- resources/

这是Leiningen项目和Clojure本身的默认设置。


映射使用Java的equals()而不是Clojure的=进行键匹配

最初由chouserIRC上报道,这种Java的equals()用法导致一些不直观的结果:

user> (= (int 1) (long 1))
true
user> ({(int 1) :found} (int 1) :not-found)
:found
user> ({(int 1) :found} (long 1) :not-found)
:not-found

由于默认情况下,IntegerLong实例的1均打印为相同,因此很难检测为什么地图不返回任何值。当您通过可能不为人所知的函数传递键而返回键时,尤其如此。

应该注意的是,使用Java的equals()代替Clojure的=对映射符合java.util.Map接口至关重要。


我正在使用Stuart Halloway 编写的Programming Clojure,Luke VanderHart编写的实用Clojure,以及IRC和邮件列表上无数Clojure黑客的帮助,以帮助我解答。


1
所有阅读器宏都具有正常的功能版本。您可以这样做,(#(hash-set %1 %2) :a 1)或者在这种情况下(hash-set :a 1)
Brian Carper 2010年

2
您还可以“删除”带有标识的其他括号:(#(identity {%1%2}):a 1)

1
您还可以使用do(#(do {%1 %2}) :a 1)
米哈尔Marczyk

@Michał-我不喜欢以前的解决方案,因为它确实暗示了副作用的发生,但实际上并非如此。
罗伯特·坎贝尔

@ rrc7cz:嗯,实际上,这里根本不需要使用匿名函数,因为hash-map直接使用(如in (hash-map :a 1)(map hash-map keys vals))更具可读性,并不意味着在命名函数中尚未实现一些特殊的和尚未实现的功能正在发生(#(...)我发现使用的确暗示)。实际上,过度使用匿名FNS本身就是一个思考的陷阱。:-) OTOH,我有时do在无副作用的超简洁匿名函数中使用...乍一看,似乎很明显。我想是关于口味的问题。
米哈尔Marczyk

42

忘记强制评估懒惰序列

除非您要求对延迟序列进行评估,否则不会对其进行评估。您可能希望它能打印出一些东西,但事实并非如此。

user=> (defn foo [] (map println [:foo :bar]) nil)
#'user/foo
user=> (foo)
nil

map不会求,它丢弃,因为它的懒惰。你必须使用一个doseqdorundoall等给力的副作用懒序列的评价。

user=> (defn foo [] (doseq [x [:foo :bar]] (println x)) nil)
#'user/foo
user=> (foo)
:foo
:bar
nil
user=> (defn foo [] (dorun (map println [:foo :bar])) nil)
#'user/foo
user=> (foo)
:foo
:bar
nil

map在REPL上使用裸露看起来像是可行的,但之所以可行,是因为REPL会强制评估惰性序列。由于您的代码可以在REPL上运行,而不能在源文件或函数内运行,因此这可能会使bug更加难以发现。

user=> (map println [:foo :bar])
(:foo
:bar
nil nil)

1
+1。这让我感到有些毛骨悚然,但以一种更阴险的方式:我正在(map ...)从内部进行评估,(binding ...)并想知道为什么不应用新的绑定值。
Alex B 2010年

20

我是Clojure新手。更高级的用户可能会遇到更多有趣的问题。

尝试打印无限的惰性序列。

我知道我在处理惰性序列,但是出于调试目的,我插入了一些print / prn / pr调用,暂时忘记了我正在打印的内容。有趣,为什么我的PC都挂了?

试图对Clojure进行编程。

创建大量refs或atoms编写不断变化其状态的代码有些诱惑。可以做到,但这不是一个很好的选择。它的性能可能也很差,并且很少会受益于多个内核。

尝试对Clojure进行100%功能编程。

不利的一面:某些算法确实确实需要一些可变状态。竭尽全力地避免可变状态会导致算法变慢或笨拙。做出决定需要判断力和经验。

试图在Java中做太多事情。

因为很容易接触到Java,所以有时会倾向于将Clojure用作Java的脚本语言包装。当然,在使用Java库功能时,您确实需要执行此操作,但是在(例如)以Java维护数据结构或使用Java数据类型(例如集合中具有Clojure的等效项)的意义不大。


13

已经提到了很多事情。我再添加一个。

Clojure if将Java布尔对象始终视为true,即使其值为false。因此,如果您有一个返回Java布尔值的java land函数,请确保不要直接检查它,(if java-bool "Yes" "No") 而是要 检查它 (if (boolean java-bool) "Yes" "No")

我对clojure.contrib.sql库感到厌倦,该库将数据库布尔字段作为java布尔对象返回。


8
请注意,(if java.lang.Boolean/FALSE (println "foo"))它不会输出foo。(if (java.lang.Boolean. "false") (println "foo"))确实有,但是(if (boolean (java.lang.Boolean "false")) (println "foo"))却没有。。。确实确实令人困惑!
米哈尔Marczyk

它似乎在Clojure 1.4.0中按预期工作:(assert(=:false(如果Boolean / FALSE:true:false)))
JakubHolý2012年

最近在执行(filter:mykey coll)时,我也对此感到不知所措,其中:mykey的值,其中布尔值-与Clojure创建的集合按预期工作,但对于使用默认Java序列化进行序列化的反序列化集合不起作用-因为这些布尔值被反序列化作为新的Boolean()和可悲的是(新的Boolean(true)!= java.lang.Boolean / TRUE)
Hendekagon 2013年

1
只要记住Clojure中的布尔值的基本规则- nilfalse都是假的,其他一切都是真实的。Java Boolean不是nil,也不是false(因为它是一个对象),所以行为是一致的。
erikprice 2015年

13

保持头部循环。
如果循环访问潜在的非常大或无限的惰性序列的元素,同时保留对第一个元素的引用,则可能会耗尽内存。

忘了没有TCO。
常规的尾部调用会占用堆栈空间,如果不小心,它们会溢出。Clojure的拥有'recur'trampoline处理很多的地方优化尾调用会在其他语言中使用的情况下,但这些技术都有意申请。

不是很懒惰的序列。
您可以使用'lazy-seq'lazy-cons(或通过建立更高级别的惰性API)来构建惰性序列,但是如果将其包装'vec或通过实现该序列的其他函数传递,则它将不再是惰性的。堆栈和堆都可以由此溢出。

将可变的东西放入裁判中。
您可以从技术上做到这一点,但是ref本身中的对象引用仅由STM控制-而不是所引用的对象及其字段(除非它们是不可变的并且指向其他ref)。因此,只要有可能,最好只引用ref中的不可变对象。原子也一样。


4
即将到来的开发分支将通过减少对对象中本地无法访问的对象的引用来减少第一项。
亚瑟·乌尔费尔特

9

使用loop ... recur处理序列时地图都行。

(defn work [data]
    (do-stuff (first data))
    (recur (rest data)))

(map do-stuff data)

map函数(在最新分支中)使用分块序列和许多其他优化。另外,由于此功能经常运行,因此Hotspot JIT通常对其进行了优化,并准备好消除任何“预热时间”。


1
这两个版本实际上并不等效。您的work功能等同于(doseq [item data] (do-stuff item))。(除了事实,工作循环永远不会结束。)
kotarak 2010年

是的,第一个打破了其争论的懒惰。尽管不再是惰性序列,但所得序列仍将具有相同的值。
亚瑟·乌尔费尔特

+1!我写了无数小的递归函数,只是为了找到另一天可以使用map和/或将它们归纳reduce
nperson325681 2011年

5

集合类型对于某些操作具有不同的行为:

user=> (conj '(1 2 3) 4)    
(4 1 2 3)                 ;; new element at the front
user=> (conj [1 2 3] 4) 
[1 2 3 4]                 ;; new element at the back

user=> (into '(3 4) (list 5 6 7))
(7 6 5 3 4)
user=> (into [3 4] (list 5 6 7)) 
[3 4 5 6 7]

使用字符串可能会造成混乱(我仍然不太了解它们)。具体地说,字符串与字符序列不同,即使序列函数可以对它们起作用:

user=> (filter #(> (int %) 96) "abcdABCDefghEFGH")
(\a \b \c \d \e \f \g \h)

要取回字符串,您需要执行以下操作:

user=> (apply str (filter #(> (int %) 96) "abcdABCDefghEFGH"))
"abcdefgh"

3

过多的寄生函数,尤其是在内部使用void java方法调用而导致NPE的情况下:

public void foo() {}

((.foo))

会导致外部括号中产生NPE,因为内部括号中的值等于0。

public int bar() { return 5; }

((.bar)) 

导致更容易调试:

java.lang.Integer cannot be cast to clojure.lang.IFn
  [Thrown class java.lang.ClassCastException]
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.