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

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

问题描述

Clojure 开发人员常犯的错误有哪些,我们该如何避免?

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

例如;Clojure 的新手认为 contains? 函数的工作原理与 java.util.Collection#contains 相同.但是,contains? 仅在与索引集合(如地图和集合)一起使用并且您正在寻找给定键时才会类似地工作:

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

当与数字索引集合(向量、数组)一起使用时,contains? 检查给定元素是否在索引的有效范围内(从零开始):

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

如果给定一个列表,contains? 将永远不会返回 true.

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

推荐答案

文字八进制

有一次我读了一个矩阵,该矩阵使用前导零来保持正确的行和列.从数学上讲,这是正确的,因为前导零显然不会改变基础值.然而,尝试用这个矩阵定义一个 var 会神秘地失败:

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

这让我很困惑.原因是 Clojure 将前导零的字面整数值视为八进制,而八进制中没有数字 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.

我还应该提到 Clojure 通过 0x 前缀支持传统的 Java 十六进制值.您还可以使用基数+r+值"表示法来使用 2 到 36 之间的任何基数,例如 2r10101036r16,它们是 42 基数.

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.

尝试在匿名函数文字

这有效:

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

因为 #() 阅读器宏被扩展为

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

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

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

评论中给出了两个解决方案.Brian Carper 建议像这样使用序列实现构造函数(数组映射、哈希集、向量):

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)

Dan 表明您可以使用 identity 函数解开外括号:

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

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

Brian 的建议实际上让我犯了下一个错误...

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

认为hash-maparray-map 确定不变具体的地图实现

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

考虑以下事项:

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,这对于更大的地图执行速度更快.

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

事实上 loop 对于这个特定的函数会更简洁和惯用:

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

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

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

引用幻影"变量

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

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.

将 for 列表推导式 视为强制循环

Treating the for list comprehension like an imperative for loop

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

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  c)] (str x y))
("1a" "1b" "1c" "2a" "2b" "2c" "3a" "3b" "3c")

也没有中断继续提前退出.

过度使用结构

我来自 OOPish 背景,所以当我开始使用 Clojure 时,我的大脑仍在思考对象.我发现自己将一切建模为 结构,因为它的成员"分组,无论多么松散,都让我感觉很舒服.实际上,structs 应该主要被认为是一种优化;Clojure 将共享密钥和一些查找信息以节省内存.您可以通过定义 accessors 来进一步优化它们加快密钥查找过程.

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.

总的来说,除了性能之外,在 ma​​p 上使用 struct 没有任何好处,因此增加的复杂性可能不值得.

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.

使用未加糖的 BigDecimal 构造函数

我需要很多 BigDecimals 并且正在编写如下丑陋的代码:

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")]

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

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

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

使用加糖版本可以减少很多膨胀.在评论中,twils 提到你也可以使用 bigdecbigint 函数更明确,但保持简洁.

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.

使用命名空间的 Java 包命名转换

这本身实际上并不是一个错误,而是与典型 Clojure 项目的惯用结构和命名背道而驰的事情.我的第一个实质性 Clojure 项目具有命名空间声明 - 以及相应的文件夹结构 - 如下所示:

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

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

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

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

这比以下的标准"Clojure 结构更复杂:

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

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

这是 Leiningen 项目和 Clojure 本身.

which is the default of Leiningen projects and Clojure itself.

Map 使用 Java 的 equals() 而不是 Clojure 的 = 进行键匹配

最初由 chouserIRC 上报道,Java 的 equals() 会导致一些不直观的结果:

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

由于 IntegerLong 1 的实例默认打印相同,因此很难检测您的地图为什么没有返回任何值.当您通过一个可能不知道返回 long 的函数传递密钥时,尤其如此.

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.

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

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.

我正在使用 Stuart Halloway 的 Programming ClojurePractical Clojure 作者 Luke VanderHart,以及无数 Clojure 黑客在 IRC 和帮助我回答的邮件列表.

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天全站免登陆