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

文字八进制

有一次,我正在阅读一个使用前导零来保持适当的行和列的matrix。 在math上这是正确的,因为前导零显然不会改变基础价值。 尝试用这个matrix来定义一个var,将会失败:

 java.lang.NumberFormatException: Invalid number: 08 

这让我很困惑。 原因在于Clojure将前导零作为八进制的整数值,八进制中没有数字08。

我还应该提到Clojure通过0x前缀支持传统的Javahex值。 您也可以使用“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 

因为#()读取器macros被扩展到

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

与地图文字包裹在括号。 由于它是第一个元素,所以它被视为一个函数(实际上是一个文字图),但是没有提供所需的参数(例如键)。 总之,匿名函数文字不会扩展到

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

所以你不能有任何字面值([],:a,4,%)作为匿名函数的主体。

评论中给出了两个解决scheme。 Brian Carperbuild议使用序列实现构造函数(array-map,hash-set,vector),如下所示:

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

表明,你可以使用身份函数解开外括号:

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

布赖恩的build议实际上给我带来了我的下一个错误…


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

考虑以下:

 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 ,对于较大的地图来说执行速度更快。


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

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

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

事实上,对于这个特定的function, 循环会更加简洁和习惯:

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

请注意,我用循环+初始绑定replace了空参数“default constructor”function body (p3 775147 600851475143 3) 。 循环现在重新绑定循环绑定(而不是fn参数)并跳回到recursion点(循环,而不是fn)。


参考“幻影”variables

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


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

基本上你是基于现有的列表创build一个懒惰的列表,而不是简单地执行一个控制的循环。 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 (> (* xx) 3)] (* 2 x))) (4 6 8 10 12) 

它们也可以处理多个绑定expression式,首先遍历最右边的expression式并继续工作:

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

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


过度使用结构

我来自OOP的背景,所以当我开始Clojure的时候,我的大脑还在思考物体。 我发现自己将所有东西都build模为一个结构体,因为它的“成员”分组无论是松散的,都让我感觉很舒服。 实际上, 结构大多应该被认为是一种优化; 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 =来进行密钥匹配

Java最初由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的=是符合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强制对lazy seq本身进行评估。 这可能会使得更难以注意的bug,因为你的代码在REPL工作,并且不能从源文件或函数内部工作。

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

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

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

我知道我在用懒惰的顺序做什么,但为了debugging目的,我插入了一些print / prn / pr调用,暂时忘记了我正在打印什么。 好笑,为什么我的电脑全挂了?

试图强制性地编程Clojure。

有一些诱惑可以创build大量的ref或者atom并且编写代码来不断地隐藏自己的状态。 这可以做到,但不适合。 它也可能性能差,很less从多核心中受益。

试图在function上对Clojure进行100%的编程。

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

试图在Java中做太多。

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

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

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

我被clojure.contrib.sql库烧了,它返回数据库布尔值字段作为java布尔对象。

保持你的头循环。
如果您在保留对第一个元素的引用的同时循环遍历潜在非常大或无限的惰性序列的元素,则可能会导致内存不足。

忘记没有TCO。
定期的tail-calls消耗堆栈空间,如果你不小心,它们会溢出。 Clojure有'recur'trampoline来处理许多其他语言使用优化'trampoline的情况,但这些技术必须有意应用。

不是很懒的序列。
你可以使用'lazy-seq'lazy-cons (或者通过构build更高级别的懒惰API)来构build一个懒惰的序列,但是如果你把它包装在'vec或者通过一些实现序列的其他函数来传递,不再懒惰。 堆栈和堆可以被这个溢出。

把可变的东西在refs中。
你可以在技术上做到这一点,但只有参考文件本身的对象引用是由STMpipe理的,而不是被引用的对象及其字段(除非它们是不可变的,并指向其他引用)。 所以只要有可能,就只select参考文献中的不可变对象。 primefaces也是一样的

使用loop ... recur处理序列时,地图将做。

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

 (map do-stuff data) 

map函数(在最新的分支中)使用分块序列和许多其他优化。 另外,由于这个function经常运行,所以热点JIT通常会对其进行优化,并且随时可以不用任何“预热时间”。

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

 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] 

使用string可能会令人困惑(我仍然不太明白)。 具体而言, string与字符序列不同,即使序列函数对它们有效:

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

要取出一个string,你需要做:

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

太多的禁忌,特别是在无效的Java方法调用里面导致NPE:

 public void foo() {} ((.foo)) 

导致NPE从外部parantheses,因为内在parantheses评估为零。

 public int bar() { return 5; } ((.bar)) 

导致更容易debugging:

 java.lang.Integer cannot be cast to clojure.lang.IFn [Thrown class java.lang.ClassCastException]