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
Run Code Online (Sandbox Code Playgroud)
与数字索引集合(向量,数组)一起使用时,contains?
仅检查给定元素是否在有效索引范围内(从零开始):
(contains? [1 2 3 4] 4)
;=> false
(contains? [1 2 3 4] 0)
;=> true
Run Code Online (Sandbox Code Playgroud)
如果给出一个列表,contains?
将永远不会返回true.
Rob*_*ell 70
文字八进制
有一次,我在一个矩阵中读取,该矩阵使用前导零来维护正确的行和列.数学上这是正确的,因为前导零显然不会改变基础价值.然而,试图用这个矩阵定义var会神秘地失败:
java.lang.NumberFormatException: Invalid number: 08
Run Code Online (Sandbox Code Playgroud)
这让我很困惑.原因是Clojure将带有前导零的文字整数值视为八进制,并且八进制中没有数字08.
我还要提一下,Clojure通过0x前缀支持传统的Java十六进制值.您还可以使用"base + r + value"表示法使用2到36之间的任何基数,例如2r101010或36r16,它们是42 base 10.
试图在匿名函数文字中返回文字
这有效:
user> (defn foo [key val]
{key val})
#'user/foo
user> (foo :a 1)
{:a 1}
Run Code Online (Sandbox Code Playgroud)
所以我相信这也会奏效:
(#({%1 %2}) :a 1)
Run Code Online (Sandbox Code Playgroud)
但它失败了:
java.lang.IllegalArgumentException: Wrong number of args passed to: PersistentArrayMap
Run Code Online (Sandbox Code Playgroud)
因为#()阅读器宏被扩展为
(fn [%1 %2] ({%1 %2}))
Run Code Online (Sandbox Code Playgroud)
用括号括起来的地图文字.因为它是第一个元素,所以它被视为一个函数(实际上是文字映射),但是没有提供必需的参数(例如键).综上所述,匿名函数文本并没有扩大到
(fn [%1 %2] {%1 %2}) ; notice the lack of parenthesis
Run Code Online (Sandbox Code Playgroud)
所以你不能有任何文字值([],:a,4,%)作为匿名函数的主体.
评论中给出了两种解决方案.Brian Carper建议使用序列实现构造函数(array-map,hash-set,vector),如下所示:
(#(array-map %1 %2) :a 1)
Run Code Online (Sandbox Code Playgroud)
而Dan显示您可以使用identity函数来展开外括号:
(#(identity {%1 %2}) :a 1)
Run Code Online (Sandbox Code Playgroud)
Brian的建议实际上让我陷入了下一个错误......
考虑以下:
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
Run Code Online (Sandbox Code Playgroud)
虽然您通常不必担心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))))
Run Code Online (Sandbox Code Playgroud)
事实上,对于这个特定的函数,循环会更加简洁和惯用:
; 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))))
Run Code Online (Sandbox Code Playgroud)
请注意,我用循环+初始绑定替换了空参数"default constructor"函数体(p3 775147 600851475143 3).在易复发,现在重新绑定环路绑定(而不是FN参数)和跳跃(而不是FN环)回递归点.
引用"幽灵"变种
我在谈论你可能使用REPL定义的var的类型 - 在你的探索性编程期间 - 然后在你的源代码中不知不觉地引用.一切正常,直到你重新加载命名空间(可能通过关闭你的编辑器),然后发现你的代码中引用了一堆未绑定的符号.当您重构,将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)
Run Code Online (Sandbox Code Playgroud)
另一种不同的方式是它们可以在无限的懒惰序列上运行:
user> (take 5 (for [x (iterate inc 0) :when (> (* x x) 3)] (* 2 x)))
(4 6 8 10 12)
Run Code Online (Sandbox Code Playgroud)
它们还可以处理多个绑定表达式,首先迭代最右边的表达式并向左移动:
user> (for [x '(1 2 3) y '(\a \b \c)] (str x y))
("1a" "1b" "1c" "2a" "2b" "2c" "3a" "3b" "3c")
Run Code Online (Sandbox Code Playgroud)
也没有休息或继续提前退出.
过度使用结构
我来自OOPish背景,所以当我开始Clojure时,我的大脑仍然在思考物体.我发现自己把所有东西都塑造成一个结构,因为它的"成员"组合,无论多么宽松,都让我觉得舒服.实际上,结构应该主要被认为是一种优化; Clojure将共享密钥和一些查找信息以节省内存.您可以通过定义访问器来加速键查找过程,从而进一步优化它们.
总体来说你不使用一个获得任何结构在地图除了性能,所以增加的复杂性可能并不值得.
使用未加粗的BigDecimal构造函数
我需要很多BigDecimals并且正在写这样丑陋的代码:
(let [foo (BigDecimal. "1") bar (BigDecimal. "42.42") baz (BigDecimal. "24.24")]
Run Code Online (Sandbox Code Playgroud)
实际上,Clojure通过在数字上附加M来支持BigDecimal文字:
(= (BigDecimal. "42.42") 42.42M) ; true
Run Code Online (Sandbox Code Playgroud)
使用加糖版可以减少很多膨胀.在评论中,twils提到你也可以使用bigdec和bigint函数更明确,但仍然简洁.
使用Java包为命名空间命名转换
这本身并不是一个错误,而是与典型的Clojure项目的惯用结构和命名相悖的东西.我的第一个实质性的Clojure项目有名称空间声明 - 以及相应的文件夹结构 - 如下所示:
(ns com.14clouds.myapp.repository)
Run Code Online (Sandbox Code Playgroud)
这使我的完全限定函数引用膨胀:
(com.14clouds.myapp.repository/load-by-name "foo")
Run Code Online (Sandbox Code Playgroud)
为了使事情复杂化,我使用了标准的Maven目录结构:
|-- src/
| |-- main/
| | |-- java/
| | |-- clojure/
| | |-- resources/
| |-- test/
...
Run Code Online (Sandbox Code Playgroud)
这比"标准"Clojure结构更复杂:
|-- src/
|-- test/
|-- resources/
Run Code Online (Sandbox Code Playgroud)
地图使用Java的equals()而不是Clojure的=来进行密钥匹配
最初由chouser在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
Run Code Online (Sandbox Code Playgroud)
由于默认情况下,Integer和Long实例1都打印相同,因此很难检测到地图未返回任何值的原因.当你通过一个函数传递你的密钥时尤其如此,这个函数可能是你不知道的,返回一个长整数.
应该注意的是,使用Java的equals()而不是Clojure =对于映射符合java.util.Map接口是必不可少的.
我正在使用Stuart Halloway的Programming Clojure,Luke VanderHart的Practical Clojure,以及无数Clojure黑客在IRC和邮件列表上的帮助,以帮助我的答案.
Bri*_*per 42
除非您要求对它们进行评估,否则不会评估惰性seqs.您可能希望这会打印一些东西,但事实并非如此.
user=> (defn foo [] (map println [:foo :bar]) nil)
#'user/foo
user=> (foo)
nil
Run Code Online (Sandbox Code Playgroud)
该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
Run Code Online (Sandbox Code Playgroud)
map
在REPL上使用裸机看起来有效,但它只能起作用,因为REPL会强制评估惰性seqs本身.这可能会使bug更难以注意到,因为您的代码在REPL中工作,并且不能从源文件或函数内部工作.
user=> (map println [:foo :bar])
(:foo
:bar
nil nil)
Run Code Online (Sandbox Code Playgroud)
Car*_*icz 20
我是一个Clojure noob.更高级的用户可能会有更多有趣的问题.
我知道我在懒惰序列中做了什么,但出于调试目的,我插入了一些print/prn/pr调用,暂时忘记了我正在打印的内容.好笑,为什么我的电脑都挂了?
有一些诱惑可以创建大量的ref
s或atom
s并编写代码,这些代码会不断地破坏其状态.这可以做到,但它不适合.它也可能性能较差,很少受益于多个内核.
另一方面:有些算法确实需要一些可变状态.不惜一切代价宗教地避免可变状态可能导致算法缓慢或笨拙.做出决定需要判断力和一点经验.
因为它很容易与Java接触,所以有时候很容易使用Clojure作为Java的脚本语言包装器.当然,在使用Java库功能时你需要做到这一点,但是(例如)在Java中维护数据结构,或使用Java数据类型(例如Clojure中具有良好等价物的集合)几乎没有意义.
Chr*_*est 13
保持头部循环.
如果在保留对第一个元素的引用的同时循环遍历可能非常大或无限的惰性序列的元素,则存在内存不足的风险.
忘记那里没有TCO.
定期尾调用会占用堆栈空间,如果不小心它们会溢出.Clojure的拥有'recur
和'trampoline
处理很多的地方优化尾调用会在其他语言中使用的情况下,但这些技术都有意申请.
不是很懒的序列.
您可以使用'lazy-seq
或'lazy-cons
(或通过构建更高级别的延迟API)构建一个惰性序列,但是如果您将其包装'vec
或通过实现序列的其他函数传递它,那么它将不再是惰性的.堆栈和堆都可以溢出.
把可变的东西放在refs中.
您可以在技术上这样做,但只有ref本身中的对象引用由STM控制 - 而不是引用的对象及其字段(除非它们是不可变的并指向其他引用).所以只要有可能,就更喜欢refs中只有不可变的对象.原子也是如此.
Vag*_*rdi 13
已经提到了很多东西.我再添加一个.
Clojure if将Java Boolean对象始终视为true,即使它的值为false.所以,如果你有一个Java的土地函数返回一个Java布尔值,确保不直接检查
(if java-bool "Yes" "No")
,而是
(if (boolean java-bool) "Yes" "No")
.
我被clojure.contrib.sql库烧毁了,它将数据库布尔字段作为java布尔对象返回.
使用loop ... recur
处理序列时地图都行.
(defn work [data]
(do-stuff (first data))
(recur (rest data)))
Run Code Online (Sandbox Code Playgroud)
与
(map do-stuff data)
Run Code Online (Sandbox Code Playgroud)
map函数(在最新的分支中)使用分块序列和许多其他优化.此外,由于此功能经常运行,Hotspot JIT通常会对其进行优化并准备好进行任何"预热时间".
集合类型对于某些操作具有不同的行为:
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]
Run Code Online (Sandbox Code Playgroud)
使用字符串可能会令人困惑(我仍然不太明白).具体来说,字符串与字符序列不同,即使序列函数对它们起作用:
user=> (filter #(> (int %) 96) "abcdABCDefghEFGH")
(\a \b \c \d \e \f \g \h)
Run Code Online (Sandbox Code Playgroud)
要恢复字符串,您需要执行以下操作:
user=> (apply str (filter #(> (int %) 96) "abcdABCDefghEFGH"))
"abcdefgh"
Run Code Online (Sandbox Code Playgroud)