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

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? 永远不会回报真实。


文字八进制

有一次,我读了一个矩阵,它使用前导零来保持正确的行和列。 在数学上这是正确的,因为前导零显然不会改变基础价值。 然而,试图用这个矩阵定义一个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,hash-set,vector),如下所示:

(#(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))))

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


参考“幻影”变量

我在谈论你可能使用REPL定义的var类型 - 在你的探索性编程过程中 - 然后在不知不觉中引用你的源代码。 一切工作正常,直到你重新加载命名空间(也许通过关闭你的编辑器),然后发现你的代码中引用了一堆未绑定的符号。 当你重构,从一个名字空间移动到另一个名字空间时,这也经常发生。


治疗列表理解就像循环的必要条件

基本上,你正在创建一个基于现有列表的懒惰列表,而不是简单地执行一个受控循环。 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将共享密钥和一些查找信息以节省内存。 您可以通过定义访问器来进一步优化它们以加快密钥查找过程。

总体而言,除了性能之外,您不会从使用结构体映射中获益,因此增加的复杂性可能不值得。


使用unsugared BigDecimal构造函数

我需要很多BigDecimals,并且正在编写这样的丑陋代码:

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

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

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

使用加糖版本可以减少很多膨胀。 在评论中, twils提到你也可以使用bigdec和bigint函数来更加明确,但仍然简洁。


使用命名空间的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 =来进行密钥匹配

最初由在IRC 欺骗专家报道,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

由于1的IntegerLong实例在默认情况下都是相同的,因此可能很难检测到为什么您的地图没有返回任何值。 当你通过一个你可能不知道的函数传递你的密钥时,这是特别真实的。

应该注意的是,使用Java的equals()而不是Clojure's =对于符合java.util.Map接口的映射是非常重要的。


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


忘记强制懒惰seqs的评估

懒惰的seqs不会被评估,除非您要求他们进行评估。 您可能会希望打印某些内容,但事实并非如此。

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

在REPL上使用裸map看起来像是有效的,但它只能工作,因为REPL强制对懒惰seq本身进行评估。 这可能会使该bug更难以注意,因为您的代码在REPL中工作,并且不能从源文件或函数内部工作。

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

我是Clojure noob。 更高级的用户可能会有更有趣的问题。

试图打印无限的懒惰序列。

我知道我在用我的懒序列做什么,但为了调试目的,我插入了一些print / prn / pr调用,暂时忘记了我正在打印的内容。 有趣的是,为什么我的电脑全挂了?

试图强制性地编程Clojure。

有一些诱惑可以创建大量的refatom并编写不断与他们的状态相关的代码。 这可以完成,但它不适合。 它也可能性能差,很少从多核心中受益。

试图在功能上对Clojure进行100%编程。

另一方面:有些算法确实需要一些可变状态。 不惜一切代价宗教上避免可变状态可能导致算法缓慢或尴尬。 做出决定需要判断力和一点经验。

试图在Java中做太多。

因为接触Java非常容易,所以有时候很容易使用Clojure作为围绕Java的脚本语言包装。 当然,在使用Java库功能时你需要做到这一点,但是在Java中维护数据结构或者使用Java数据类型(比如在Clojure中有很好的等价物的集合)没有什么意义。

链接地址: http://www.djcxy.com/p/38573.html

上一篇: Common programming mistakes for Clojure developers to avoid

下一篇: How can I reload .emacs after changing it?