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之间使用任何基数,例如2r101010或36r16(基数为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地图的具体实现,但您应该知道,生成地图的函数(如assoc或conj )可以使用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的Integer和Long实例在默认情况下都是相同的,因此可能很难检测到为什么您的地图没有返回任何值。 当你通过一个你可能不知道的函数传递你的密钥时,这是特别真实的。
应该注意的是,使用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
从来没有被评估过,因为它很懒,所以它被默默地抛弃了。 你必须使用doseq
, dorun
, doall
等中的一个强制评估懒惰序列的副作用。
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。
有一些诱惑可以创建大量的ref
或atom
并编写不断与他们的状态相关的代码。 这可以完成,但它不适合。 它也可能性能差,很少从多核心中受益。
试图在功能上对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