Clojure 개발자가 피해야 할 일반적인 프로그래밍 실수 [닫기]


92

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를 반환하지 않습니다.


4
참고로 java.util.Collection # contains 유형 기능을 찾는 Clojure 개발자의 경우 clojure.contrib.seq-utils / includes를 확인하십시오 . 문서에서 : 사용법 : (포함? coll x). coll이 선형 시간에서 x와 동일한 (=와 함께) 무언가를 포함하면 true를 반환합니다.
Robert Campbell

11
그 질문이 커뮤니티 위키라는 사실을 놓친 것 같습니다

3
나는 펄 질문 그냥 :) 모든 다른 사람들과 단계의 출력으로 얼마나 사랑
에테르

8
포함을 찾는 Clojure 개발자의 경우 rcampbell의 조언을 따르지 않는 것이 좋습니다. seq-utils는 오랫동안 사용되지 않았으며 그 함수는 처음부터 유용하지 않았습니다. Clojure의 some기능을 사용하거나 더 나은 방법은 contains자체를 사용할 수 있습니다. Clojure 컬렉션은 java.util.Collection. (.contains [1 2 3] 2) => true
레인

답변:


70

리터럴 옥탈

어느 시점에서 나는 적절한 행과 열을 유지하기 위해 선행 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))))

실제로 루프 가이 특정 함수에 대해 더 간결하고 관용적 일 때 :

; 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의 루프) 다시 재귀 지점으로 이동합니다.


"가상"변수 참조

탐색 프로그래밍 중에 REPL을 사용하여 정의 할 수있는 var의 유형에 대해 말하고 있습니다. 그런 다음 무의식적으로 소스에서 참조합니다. 네임 스페이스를 다시로드하고 (아마도 편집기를 닫아서) 나중에 코드 전체에서 참조 된 언 ​​바운드 기호를 발견 할 때까지 모든 것이 잘 작동합니다. 이것은 또한 한 네임 스페이스에서 다른 네임 스페이스로 var를 이동하면서 리팩토링 할 때 자주 발생합니다.


for 목록 이해력을 명령형 for 루프처럼 다루기

기본적으로 단순히 제어 된 루프를 수행하는 것이 아니라 기존 목록을 기반으로 지연 목록을 만듭니다. 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는 메모리를 절약하기 위해 키와 일부 조회 정보를 공유합니다. 접근 자를 정의 하여 키 조회 프로세스의 속도 를 높이면 추가로 최적화 할 수 있습니다 .

전반적으로 당신이 사용하는 어떤 이득도 얻을 수없는 구조체를 넘는 추가 복잡성이 가치가되지 않을 수도 있습니다, 성능을 제외하고.


승인되지 않은 BigDecimal 생성자 사용

나는 많은 BigDecimals가 필요했고 다음과 같은 추악한 코드를 작성했습니다.

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

실제로 Clojure가 숫자에 M 을 추가하여 BigDecimal 리터럴을 지원할 때 :

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

설탕을 넣은 버전을 사용하면 부풀음이 많이 제거됩니다. 주석에서 twilsbigdecbigint 함수를 사용하여 더 명확하면서도 간결하게 유지할 수 있다고 언급했습니다 .


네임 스페이스에 대한 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 자체 의 기본값입니다 .


맵은 키 매칭을 위해 Clojure의 = 대신 Java의 equals ()를 사용합니다.

원래에 의해보고 chouserIRC , 자바의 사용 등호 () 일부 직관적 결과를 리드 :

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을 반환하는 함수를 통해 키를 전달할 때 특히 그렇습니다.

Clojure의 = 대신 Java의 equals ()를 사용하는 것은 맵이 java.util.Map 인터페이스를 준수하는 데 필수적 이라는 점에 유의해야합니다 .


저는 Stuart Halloway의 Programming Clojure , Luke VanderHart의 Practical Clojure , IRC 의 수많은 Clojure 해커의 도움과 메일 링리스트를 사용하여 제 답변을 돕고 있습니다.


1
모든 판독기 매크로에는 일반 기능 버전이 있습니다. (#(hash-set %1 %2) :a 1)이 경우 또는 할 수 (hash-set :a 1)있습니다.
Brian Carper

2
ID가있는 추가 괄호를 '제거'할 수도 있습니다. (# (identity {% 1 % 2}) : a 1)

1
당신은 또한 사용할 수 있습니다 do: (#(do {%1 %2}) :a 1).
Michał Marczyk 2010 년

@ Michał-저는이 솔루션이 이전 솔루션만큼 마음에 들지 않습니다. do 는 실제로 여기에 해당되지 않는 부작용이 발생한다는 것을 의미 하기 때문 입니다.
Robert Campbell

@ rrc7cz : 글쎄, 실제로는 익명 함수를 사용할 필요가 없습니다. hash-map직접 사용 하는 것이 ( (hash-map :a 1)또는 에서 와 같이 (map hash-map keys vals)) 더 읽기 쉽고 이름이 지정된 함수에서 아직 구현되지 않은 특수한 것을 암시하지 않기 때문입니다. (의 사용 #(...)이 의미하는 바, 나는 발견) 일어나고 있습니다. 사실, 익명의 fns를 남용하는 것은 그 자체로 생각해야 할 문제입니다. :-) OTOH, 저는 때때로 do부작용이없는 매우 간결한 익명 함수를 사용합니다 ... 한 눈에보기에 명백한 경향이 있습니다. 맛의 문제라고 생각합니다.
Michał Marczyk 2010 년

42

게으른 시퀀스의 강제 평가를 잊어 버림

Lazy seq는 평가를 요청하지 않는 한 평가되지 않습니다. 이것이 무언가를 인쇄 할 것으로 예상 할 수 있지만 그렇지 않습니다.

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

mapREPL 종류에서 베어 를 사용하면 작동하는 것처럼 보이지만 REPL이 lazy seq 자체를 평가하기 때문에 작동합니다. 코드가 REPL에서 작동하고 소스 파일이나 함수 내에서 작동하지 않기 때문에 이로 인해 버그를 발견하기가 더 어려워 질 수 있습니다.

user=> (map println [:foo :bar])
(:foo
:bar
nil nil)

1
+1. 이것은 나를 물었지만 더 교활한 방식으로 : (map ...)내부 에서 평가 (binding ...)하고 왜 새로운 바인딩 값이 적용되지 않는지 궁금합니다.
Alex B

20

나는 Clojure 멍청이입니다. 고급 사용자는 더 흥미로운 문제가있을 수 있습니다.

무한한 지연 시퀀스를 인쇄하려고합니다.

게으른 시퀀스로 무엇을하는지 알고 있었지만 디버깅 목적으로 인쇄 / prn / pr 호출을 삽입하여 인쇄중인 내용을 일시적으로 잊어 버렸습니다. 재미 있네요. 왜 내 PC가 모두 끊겼나요?

Clojure를 반드시 프로그래밍하려고합니다.

많은 refs 또는 atoms를 만들고 지속적으로 상태를 왜곡하는 코드 를 작성하려는 유혹이 있습니다 . 이것은 할 수 있지만 적합하지 않습니다. 또한 성능이 좋지 않을 수 있으며 여러 코어의 이점을 거의 얻지 못합니다.

Clojure를 100 % 기능적으로 프로그래밍하려고합니다.

이에 대한이면 : 일부 알고리즘은 실제로 약간의 변경 가능한 상태를 원합니다. 어떤 희생을 치르더라도 종교적으로 변경 가능한 상태를 피하면 알고리즘이 느리거나 어색 할 수 있습니다. 결정을 내리려면 판단과 약간의 경험이 필요합니다.

자바에서 너무 많은 일을하려고합니다.

Java에 접근하기가 매우 쉽기 때문에 Clojure를 Java를 둘러싼 스크립팅 언어 래퍼로 사용하고 싶을 때가 있습니다. 물론 Java 라이브러리 기능을 사용할 때 정확히이 작업을 수행해야하지만 (예를 들어) Java에서 데이터 구조를 유지하거나 Clojure에 좋은 등가물이있는 컬렉션과 같은 Java 데이터 유형을 사용하는 것은 거의 의미가 없습니다.


13

이미 언급 된 많은 것. 하나만 더 추가하겠습니다.

값이 false 인 경우에도 Java Boolean 객체를 항상 true로 취급하는 경우 Clojure . 따라서 Java 부울 값을 반환하는 Java land 함수가있는 경우 직접 확인하지 (if java-bool "Yes" "No") 말고 (if (boolean java-bool) "Yes" "No").

데이터베이스 부울 필드를 java 부울 객체로 반환하는 clojure.contrib.sql 라이브러리로 이것에 의해 태워졌습니다.


8
참고 (if java.lang.Boolean/FALSE (println "foo"))foo는 인쇄되지 않습니다. (if (java.lang.Boolean. "false") (println "foo"))하지만 (if (boolean (java.lang.Boolean "false")) (println "foo"))그렇지는 않지만 ... 정말 혼란 스럽습니다!
Michał Marczyk 2010 년

Clojure 1.4.0에서 예상대로 작동하는 것 같습니다 : (assert (= : false (if Boolean / FALSE : true : false)))
Jakub Holý

나는 또한 최근에 (filter : mykey coll)을 할 때 (filter : mykey coll) 여기서 : mykey의 값을 태워 버렸습니다. 슬프게도 새로운 부울 (), 그리고 (새 부울 ()가 true = java.lang.Boolean에 / TRUE!)
Hendekagon

1
그냥 Clojure의 부울 값의 기본 규칙을 기억 - nilfalse거짓, 그리고 다른 모든 사실이다. Java Boolean는 그렇지 않으며 그렇지 nil않습니다 false(객체이기 때문에). 따라서 동작은 일관 적입니다.
erikprice 2015 년

13

머리를 계속 반복하십시오.
첫 번째 요소에 대한 참조를 유지하면서 잠재적으로 매우 크거나 무한한 지연 시퀀스의 요소를 반복하면 메모리가 부족할 위험이 있습니다.

TCO가 없다는 사실을 잊어 버렸습니다.
정기적 인 꼬리 호출은 스택 공간을 소비하며주의하지 않으면 오버플로됩니다. Clojure의가있다 'recur'trampoline최적화 꼬리 호출을 다른 언어로 사용되는 것이 많은 경우를 처리 할 수 있지만, 이러한 기술은 의도적으로 적용 할 수 있습니다.

그다지 게으른 시퀀스. 또는 (또는 더 높은 수준의 지연 API를
기반으로 빌드하여) 지연 시퀀스를 빌드 할 수 있지만,이를 래핑 하거나 시퀀스를 실현하는 다른 함수를 통해 전달하면 더 이상 지연되지 않습니다. 이로 인해 스택과 힙이 모두 오버플로 될 수 있습니다.'lazy-seq'lazy-cons'vec

refs에 변경 가능한 것을 넣기.
기술적으로 할 수는 있지만 참조 된 객체와 필드가 아닌 참조 자체의 객체 참조 만 STM에 의해 관리됩니다 (불변하고 다른 참조를 가리키는 경우 제외). 따라서 가능할 때마다 refs에서 변경 불가능한 객체 만 선호합니다. 원자도 마찬가지입니다.


4
다가오는 개발 브랜치는 로컬에 도달 할 수 없게되면 함수의 객체에 대한 참조를 삭제하여 첫 번째 항목을 줄이는 방향으로 먼 길을갑니다.
Arthur Ulfeldt 2010 년

9

loop ... recur지도가 할 때 시퀀스를 처리 하는 데 사용 합니다.

(defn work [data]
    (do-stuff (first data))
    (recur (rest data)))

(map do-stuff data)

맵 기능 (최신 브랜치에서)은 청크 시퀀스와 다른 많은 최적화를 사용합니다. 또한이 기능은 자주 실행되기 때문에 일반적으로 Hotspot JIT는이를 최적화하여 "예열 시간"없이 사용할 수 있습니다.


1
이 두 버전은 실제로 동일하지 않습니다. 귀하의 work기능은 (doseq [item data] (do-stuff item)). (사실 외에도, 작업 결코 끝에서 그 루프.)
kotarak

예, 첫 번째는 주장에 대한 게으름을 깨뜨립니다. 결과 seq는 더 이상 lazy seq가 아니지만 동일한 값을 갖습니다.
Arthur Ulfeldt 2010 년

+1! 나는 많은 작은 재귀 함수를 작성하여 map및 / 또는 reduce.
nperson325681

5

컬렉션 유형은 일부 작업에 대해 다른 동작을 갖습니다.

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]

문자열로 작업하는 것은 혼란 스러울 수 있습니다 (여전히 이해하지 못합니다). 특히 문자열은 시퀀스 함수가 ​​작동하더라도 문자 시퀀스와 동일하지 않습니다.

user=> (filter #(> (int %) 96) "abcdABCDefghEFGH")
(\a \b \c \d \e \f \g \h)

문자열을 다시 꺼내려면 다음을 수행해야합니다.

user=> (apply str (filter #(> (int %) 96) "abcdABCDefghEFGH"))
"abcdefgh"

3

너무 많은 괄호, 특히 NPE가 발생하는 void java 메서드 호출이 있습니다.

public void foo() {}

((.foo))

내부 괄호가 nil로 평가되기 때문에 외부 괄호에서 NPE가 발생합니다.

public int bar() { return 5; }

((.bar)) 

디버깅이 더 쉬워집니다.

java.lang.Integer cannot be cast to clojure.lang.IFn
  [Thrown class java.lang.ClassCastException]
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.