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?
true를 반환하지 않습니다.
리터럴 옥탈
어느 시점에서 나는 적절한 행과 열을 유지하기 위해 선행 0을 사용하는 행렬을 읽었습니다. 선행 0은 분명히 기본 값을 변경하지 않기 때문에 수학적으로 정확합니다. 그러나이 행렬로 var를 정의하려는 시도는 다음과 같이 신비하게 실패합니다.
java.lang.NumberFormatException: Invalid number: 08
나를 완전히 당황하게했다. 그 이유는 Clojure가 선행 0이있는 리터럴 정수 값을 8 진법으로 취급하고 8 진법에는 숫자 08이 없기 때문입니다.
Clojure는 0x 접두사 를 통해 전통적인 Java 16 진수 값을 지원한다는 점도 언급해야 합니다. 42 진법 10 인 2r101010 또는 36r16 과 같이 "base + r + value"표기법을 사용하여 2와 36 사이의 기수를 사용할 수도 있습니다 .
익명 함수 리터럴에서 리터럴 반환 시도
이것은 작동합니다 :
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)
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
같은 - 당신은 일반적으로 Clojure의지도의 구체적인 구현에 대해 걱정할 필요가 없지만, 당신은지도 성장 기능을 알고 있어야합니다 ASSOC 또는 접속사가 - 테이크 수 PersistentArrayMap을 a와 반환 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))))
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
탐색 프로그래밍 중에 REPL을 사용하여 정의 할 수있는 var의 유형에 대해 말하고 있습니다. 그런 다음 무의식적으로 소스에서 참조합니다. 네임 스페이스를 다시로드하고 (아마도 편집기를 닫아서) 나중에 코드 전체에서 참조 된 언 바운드 기호를 발견 할 때까지 모든 것이 잘 작동합니다. 이것은 또한 한 네임 스페이스에서 다른 네임 스페이스로 var를 이동하면서 리팩토링 할 때 자주 발생합니다.
for 목록 이해력을 명령형 for 루프처럼 다루기
기본적으로 단순히 제어 된 루프를 수행하는 것이 아니라 기존 목록을 기반으로 지연 목록을 만듭니다. Clojure의 doseq 는 실제로 명령형 foreach 반복 구조와 더 유사합니다.
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.
Forgetting to force evaluation of lazy seqs
Lazy seqs aren't evaluated unless you ask them to be evaluated. You might expect this to print something, but it doesn't.
user=> (defn foo [] (map println [:foo :bar]) nil)
#'user/foo
user=> (foo)
nil
The map
is never evaluated, it's silently discarded, because it's lazy. You have to use one of doseq
, dorun
, doall
etc. to force evaluation of lazy sequences for side-effects.
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
Using a bare map
at the REPL kind of looks like it works, but it only works because the REPL forces evaluation of lazy seqs itself. This can make the bug even harder to notice, because your code works at the REPL and doesn't work from a source file or inside a function.
user=> (map println [:foo :bar])
(:foo
:bar
nil nil)
I'm a Clojure noob. More advanced users may have more interesting problems.
trying to print infinite lazy sequences.
I knew what I was doing with my lazy sequences, but for debugging purposes I inserted some print/prn/pr calls, temporarily having forgotten what it is I was printing. Funny, why's my PC all hung up?
trying to program Clojure imperatively.
There is some temptation to create a whole lot of ref
s or atom
s and write code that constantly mucks with their state. This can be done, but it's not a good fit. It may also have poor performance, and rarely benefit from multiple cores.
trying to program Clojure 100% functionally.
A flip side to this: Some algorithms really do want a bit of mutable state. Religiously avoiding mutable state at all costs may result in slow or awkward algorithms. It takes judgement and a bit of experience to make the decision.
trying to do too much in Java.
Because it's so easy to reach out to Java, it's sometimes tempting to use Clojure as a scripting language wrapper around Java. Certainly you'll need to do exactly this when using Java library functionality, but there's little sense in (e.g.) maintaining data structures in Java, or using Java data types such as collections for which there are good equivalents in Clojure.
Lots of things already mentioned. I'll just add one more.
Clojure if treats Java Boolean objects always as true even if it's value is false. So if you have a java land function that returns a java Boolean value, make sure you do not check it directly (if java-bool "Yes" "No")
but rather (if (boolean java-bool) "Yes" "No")
.
I got burned by this with clojure.contrib.sql library that returns database boolean fields as java Boolean objects.
Keeping your head in loops.
You risk running out of memory if you loop over the elements of a potentially very large, or infinite, lazy sequence while keeping a reference to the first element.
Forgetting there's no TCO.
Regular tail-calls consume stack space, and they will overflow if you're not careful. Clojure has 'recur
and 'trampoline
to handle many of the cases where optimized tail-calls would be used in other languages, but these techniques have to be intentionally applied.
Not-quite-lazy sequences.
You may build a lazy sequence with 'lazy-seq
or 'lazy-cons
(or by building upon higher level lazy APIs), but if you wrap it in 'vec
or pass it through some other function that realizes the sequence, then it will no longer be lazy. Both the stack and the heap can be overflown by this.
Putting mutable things in refs.
You can technically do it, but only the object reference in the ref itself is governed by the STM - not the referred object and its fields (unless they are immutable and point to other refs). So whenever possible, prefer to only immutable objects in refs. Same thing goes for atoms.
using loop ... recur
to process sequences when map will do.
(defn work [data]
(do-stuff (first data))
(recur (rest data)))
vs.
(map do-stuff data)
The map function (in the latest branch) uses chunked sequences and many other optimizations. Also, because this function is frequently run, the Hotspot JIT usually has it optimized and ready to go with out any "warm up time".
Collection types have different behaviors for some operations:
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]
Working with strings can be confusing (I still don't quite get them). Specifically, strings are not the same as sequences of characters, even though sequence functions work on them:
user=> (filter #(> (int %) 96) "abcdABCDefghEFGH")
(\a \b \c \d \e \f \g \h)
To get a string back out, you'd need to do:
user=> (apply str (filter #(> (int %) 96) "abcdABCDefghEFGH"))
"abcdefgh"
too many parantheses, especially with void java method call inside which results in NPE:
public void foo() {}
((.foo))
results in NPE from outer parantheses because inner parantheses evaluate to nil.
public int bar() { return 5; }
((.bar))
results in the easier to debug:
java.lang.Integer cannot be cast to clojure.lang.IFn
[Thrown class java.lang.ClassCastException]
'program story' 카테고리의 다른 글
Google+ 담벼락에 게시하는 방법 (0) | 2020.09.01 |
---|---|
IE의 드롭 다운 목록 너비 (0) | 2020.09.01 |
Maven이 컴파일하고 빌드 jar에 포함 할 추가 소스 디렉토리를 추가하는 방법은 무엇입니까? (0) | 2020.09.01 |
사용자가 언어를 선택할 때 앱의 언어를 변경하는 방법은 무엇입니까? (0) | 2020.09.01 |
Eclipse에서 작업 태그를 현재 프로젝트로 제한하는 방법은 무엇입니까? (0) | 2020.09.01 |