Clojure开发人员避免的常见编程错误 [英] Common programming mistakes for Clojure developers to avoid

查看:464
本文介绍了Clojure开发人员避免的常见编程错误的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

Clojure开发人员犯了一些常见错误,我们如何避免它们?



例如: Clojure的新手认为包含?函数与 java.util.Collection#contains 相同。但是,包含?只有在使用索引集合(如地图和集合)时才能工作,并且您正在查找给定的键:

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

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

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

如果给定列表,包含?将永远不会返回true。

解决方案

立即数八进制



有一点,我正在读一个矩阵,使用前导零来保持正确的行和列。数学上这是正确的,因为前导零显然不改变基础值。但是,尝试使用此矩阵定义var将会失败:

  java.lang.NumberFormatException:无效数字:08 

这完全让我不知所措。原因是Clojure将前导零作为八进制的字面值整数值,而八进制中没有数字08.



我还应该提到Clojure支持传统的Java十六进制值通过 0x 前缀。您也可以使用「base + r + value」符号,例如 2r101010 36r16 (以42为底数),使用介于2到36之间的任何基数。






尝试在匿名功能文字



这个工作原理:

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

工作:

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

但它失败:

  lang.IllegalArgumentException:传递给:PersistentArrayMap的参数数错误

因为# / strong>读取器宏扩展到

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

包含在括号中的地图文字。因为它是第一个元素,它被当作一个函数(字面图实际上是),但不提供所需的参数(如键)。总之,匿名函数文字展开为

 (fn [%1%2] {%1%2});注意缺少括号

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



在注释中给出了两个解决方案。 Brian Carper 建议使用序列实现构造函数(array-map,hash-set,vector),如下所示:

 <$ c $        显示您可以使用身份函数打开外括号:

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

Brian的建议实际上带来了我的下一个错误...






认为 hash-map array-map 确定具体地图实施



  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 ,对大型地图执行速度更快。






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



当我开始使用时,我写了很多函数:

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

当事实上循环对于这个特定的功能更简洁和惯用:

 ;经过时间:387毫秒
(defn p3 [] {:post [(=%6857)]}
(loop [i 775147 n 600851475143 times 3] in)(fast-prime?i times))i
(recur(dec i)n times))))

请注意,我用循环+初始绑定替换了空参数default constructor函数体(p3 775147 600851475143 3) recur 现在重新绑定循环绑定(而不是fn参数),并跳回到递归点(循环,而不是fn)。






引用phantomvars



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






列表解析像循环的命令式



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



它们有什么不同的一个例子是过滤使用任意谓词迭代的元素:

  user> (对于[n'(1 2 3 4):when(even n)] n)
(2 4)

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

另一种不同的方法是,它们可以操作无限延迟序列:

  user> (对于[x(iterate inc 0):when(>(* xx)3)](* 2 x)))
(4 6 8 10 12)

他们也可以处理多个绑定表达式,先迭代最右边的表达式,然后按照左边的方式:

  user> (对于[x'(1 2 3)y'(\a \b \c)](str xy))
(1a1b1c2a2b strong>或继续以过早退出。






结构过度使用



我来自一个OOPish背景,所以当我开始Clojure我的大脑仍在思考对象。我发现自己把所有的东西模拟成一个 struct ,因为它的成员分组,但是松散,使我感到舒适。实际上, structs 应该被视为优化; Clojure将共享密钥和一些查找信息以节省内存。您可以通过定义访问者来进一步优化它们



总的来说,在地图上使用 struct 不会获得任何结果




使用未绑定的BigDecimal构造函数

strong>



我需要很多 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-namefoo)

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

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

Clojure结构:

  |  -  src / 
| - test /
| - 资源/

这是默认的 Leiningen 项目和 Clojure 本身。 p>




地图使用Java的equals(),而不是Clojure's =来匹配键



最初由 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

由于整数长整型实例1都默认打印相同,因此很难检测为什么您的地图不返回任何值。这是特别真实的,当你传递你的密钥通过一个函数,或许你不知道,返回一个长。



应该注意,使用Java的 equals )而不是Clojure的 = 对于地图符合java.util.Map接口是必不可少的。






我使用的是Stuart Halloway的编程Clojure , Luke VanderHart的一个href =http://www.apress.com/book/view/1430272317 =nofollow noreferrer>实践Clojure ,以及无数个Clojure黑客在 IRC 和邮件列表帮助我的答案。


What are some common mistakes made by Clojure developers, and how can we avoid them?

For example; newcomers to Clojure think that the contains? function works the same as java.util.Collection#contains. However, contains? will only work similarly when used with indexed collections like maps and sets and you're looking for a given key:

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

When used with numerically indexed collections (vectors, arrays) contains? only checks that the given element is within the valid range of indexes (zero-based):

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

If given a list, contains? will never return true.

解决方案

Literal Octals

At one point I was reading in a matrix which used leading zeros to maintain proper rows and columns. Mathematically this is correct, since leading zero obviously don't alter the underlying value. Attempts to define a var with this matrix, however, would fail mysteriously with:

java.lang.NumberFormatException: Invalid number: 08

which totally baffled me. The reason is that Clojure treats literal integer values with leading zeros as octals, and there is no number 08 in octal.

I should also mention that Clojure supports traditional Java hexadecimal values via the 0x prefix. You can also use any base between 2 and 36 by using the "base+r+value" notation, such as 2r101010 or 36r16 which are 42 base ten.


Trying to return literals in an anonymous function literal

This works:

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

so I believed this would also work:

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

but it fails with:

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

because the #() reader macro gets expanded to

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

with the map literal wrapped in parenthesis. Since it's the first element, it's treated as a function (which a literal map actually is), but no required arguments (such as a key) are provided. In summary, the anonymous function literal does not expand to

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

and so you can't have any literal value ([], :a, 4, %) as the body of the anonymous function.

Two solutions have been given in the comments. Brian Carper suggests using sequence implementation constructors (array-map, hash-set, vector) like so:

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

while Dan shows that you can use the identity function to unwrap the outer parenthesis:

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

Brian's suggestion actually brings me to my next mistake...


Thinking that hash-map or array-map determine the unchanging concrete map implementation

Consider the following:

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

While you generally won't have to worry about the concrete implementation of a Clojure map, you should know that functions which grow a map - like assoc or conj - can take a PersistentArrayMap and return a PersistentHashMap, which performs faster for larger maps.


Using a function as the recursion point rather than a loop to provide initial bindings

When I started out, I wrote a lot of functions like this:

; 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))))

When in fact loop would have been more concise and idiomatic for this particular function:

; 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))))

Notice that I replaced the empty argument, "default constructor" function body (p3 775147 600851475143 3) with a loop + initial binding. The recur now rebinds the loop bindings (instead of the fn parameters) and jumps back to the recursion point (loop, instead of fn).


Referencing "phantom" vars

I'm speaking about the type of var you might define using the REPL - during your exploratory programming - then unknowingly reference in your source. Everything works fine until you reload the namespace (perhaps by closing your editor) and later discover a bunch of unbound symbols referenced throughout your code. This also happens frequently when you're refactoring, moving a var from one namespace to another.


Treating the for list comprehension like an imperative for loop

Essentially you're creating a lazy list based on existing lists rather than simply performing a controlled loop. Clojure's doseq is actually more analogous to imperative foreach looping constructs.

One example of how they're different is the ability to filter which elements they iterate over using arbitrary predicates:

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)

Another way they're different is that they can operate on infinite lazy sequences:

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

They also can handle more than one binding expression, iterating over the rightmost expression first and working its way left:

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

There's also no break or continue to exit prematurely.


Overuse of structs

I come from an OOPish background so when I started Clojure my brain was still thinking in terms of objects. I found myself modeling everything as a struct because its grouping of "members", however loose, made me feel comfortable. In reality, structs should mostly be considered an optimization; Clojure will share the keys and some lookup information to conserve memory. You can further optimize them by defining accessors to speed up the key lookup process.

Overall you don't gain anything from using a struct over a map except for performance, so the added complexity might not be worth it.


Using unsugared BigDecimal constructors

I needed a lot of BigDecimals and was writing ugly code like this:

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

when in fact Clojure supports BigDecimal literals by appending M to the number:

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

Using the sugared version cuts out a lot of the bloat. In the comments, twils mentioned that you can also use the bigdec and bigint functions to be more explicit, yet remain concise.


Using the Java package naming conversions for namespaces

This isn't actually a mistake per se, but rather something that goes against the idiomatic structure and naming of a typical Clojure project. My first substantial Clojure project had namespace declarations - and corresponding folder structures - like this:

(ns com.14clouds.myapp.repository)

which bloated up my fully-qualified function references:

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

To complicate things even more, I used a standard Maven directory structure:

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

which is more complex than the "standard" Clojure structure of:

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

which is the default of Leiningen projects and Clojure itself.


Maps utilize Java's equals() rather than Clojure's = for key matching

Originally reported by chouser on IRC, this usage of Java's equals() leads to some unintuitive results:

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

Since both Integer and Long instances of 1 are printed the same by default, it can be difficult to detect why your map isn't returning any values. This is especially true when you pass your key through a function which, perhaps unbeknownst to you, returns a long.

It should be noted that using Java's equals() instead of Clojure's = is essential for maps to conform to the java.util.Map interface.


I'm using Programming Clojure by Stuart Halloway, Practical Clojure by Luke VanderHart, and the help of countless Clojure hackers on IRC and the mailing list to help along my answers.

这篇关于Clojure开发人员避免的常见编程错误的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆