Những lỗi lập trình phổ biến mà nhà phát triển Clojure cần tránh [đóng]


92

Các nhà phát triển Clojure thường mắc phải một số lỗi nào và làm cách nào để tránh chúng?

Ví dụ; những người mới đến với Clojure nghĩ rằng contains?chức năng hoạt động giống như java.util.Collection#contains. Tuy nhiên, contains?sẽ chỉ hoạt động tương tự khi được sử dụng với các bộ sưu tập được lập chỉ mục như bản đồ và bộ và bạn đang tìm một khóa nhất định:

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

Khi được sử dụng với các bộ sưu tập được lập chỉ mục số (vectơ, mảng), contains? chỉ kiểm tra xem phần tử đã cho có nằm trong phạm vi hợp lệ của chỉ mục (dựa trên không):

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

Nếu đưa ra một danh sách, contains?sẽ không bao giờ trả về true.


4
Chỉ FYI, đối với những nhà phát triển Clojure đang tìm kiếm java.util.Collection # chứa chức năng loại, hãy xem clojure.contrib.seq-utils / include? Từ tài liệu: Cách sử dụng: (bao gồm? Coll x). Trả về true nếu coll chứa một giá trị nào đó bằng (với =) với x, theo thời gian tuyến tính.
Robert Campbell

11
Có vẻ như bạn đã bỏ lỡ sự thật rằng những câu hỏi đó là Wiki Cộng đồng

3
Tôi yêu như thế nào câu hỏi Perl chỉ có được trong bước với tất cả những người khác :)
Ether

8
Đối với các nhà phát triển Clojure đang tìm kiếm chứa, tôi khuyên bạn không nên làm theo lời khuyên của rcampbell. seq-utils từ lâu đã không còn được dùng nữa và chức năng đó không bao giờ hữu ích khi bắt đầu. Bạn có thể sử dụng somechức năng của Clojure hoặc tốt hơn là chỉ sử dụng containschính nó. Thực hiện bộ sưu tập vải vóc java.util.Collection. (.contains [1 2 3] 2) => true
Rayne

Câu trả lời:


70

Số bát phân theo nghĩa đen

Có lúc tôi đang đọc trong một ma trận sử dụng các số 0 ở đầu để duy trì các hàng và cột thích hợp. Về mặt toán học, điều này đúng, vì số 0 đứng đầu rõ ràng không làm thay đổi giá trị cơ bản. Tuy nhiên, cố gắng xác định một var với ma trận này sẽ thất bại một cách bí ẩn với:

java.lang.NumberFormatException: Invalid number: 08

điều đó hoàn toàn khiến tôi bối rối. Lý do là vì Clojure xử lý các giá trị số nguyên theo nghĩa đen với các số 0 ở đầu là số bát phân và không có số 08 trong hệ bát phân.

Tôi cũng nên đề cập rằng Clojure hỗ trợ các giá trị thập lục phân Java truyền thống thông qua tiền tố 0x . Bạn cũng có thể sử dụng bất kỳ cơ số nào từ 2 đến 36 bằng cách sử dụng ký hiệu "cơ số + r + giá trị", chẳng hạn như 2r101010 hoặc 36r16 là 42 cơ số 10.


Cố gắng trả về các ký tự trong một ký tự hàm ẩn danh

Những công việc này:

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

vì vậy tôi tin rằng điều này cũng sẽ hoạt động:

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

nhưng nó không thành công với:

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

vì macro trình đọc # () được mở rộng thành

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

với bản đồ được bao bọc trong dấu ngoặc đơn. Vì nó là phần tử đầu tiên nên nó được coi như một hàm (thực sự là một bản đồ chữ), nhưng không cung cấp đối số bắt buộc (chẳng hạn như khóa). Tóm lại, nghĩa đen của hàm ẩn danh không mở rộng thành

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

và vì vậy bạn không thể có bất kỳ giá trị chữ nào ([],: a, 4,%) làm phần thân của hàm ẩn danh.

Hai giải pháp đã được đưa ra trong các bình luận. Brian Carper đề xuất sử dụng các hàm tạo triển khai trình tự (bản đồ mảng, bộ băm, vectơ) như sau:

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

trong khi Dan cho thấy rằng bạn có thể sử dụng hàm nhận dạng để mở ngoặc đơn bên ngoài:

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

Đề nghị của Brian thực sự đưa tôi đến sai lầm tiếp theo của tôi ...


Nghĩ rằng bản đồ băm hoặc bản đồ mảng xác định việc triển khai bản đồ cụ thể không thay đổi

Hãy xem xét những điều sau:

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

Trong khi bạn thường sẽ không phải lo lắng về việc thực hiện cụ thể của bản đồ Clojure, bạn nên biết rằng các chức năng đó phát triển một bản đồ - như assoc hoặc conj - có thể mất một PersistentArrayMap và trả về một PersistentHashMap , mà thực hiện nhanh hơn cho bản đồ lớn hơn.


Sử dụng một hàm làm điểm đệ quy thay vì một vòng lặp để cung cấp các ràng buộc ban đầu

Khi tôi bắt đầu, tôi đã viết rất nhiều hàm như thế này:

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

Trong thực tế, vòng lặp thực tế sẽ ngắn gọn và dễ hiểu hơn cho hàm cụ thể này:

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

Lưu ý rằng tôi đã thay thế đối số trống, thân hàm "hàm tạo mặc định" (p3 775147 600851475143 3) bằng một vòng lặp + ràng buộc ban đầu. Việc lặp lại bây giờ quay lại các ràng buộc của vòng lặp (thay vì tham số fn) và nhảy trở lại điểm đệ quy (vòng lặp, thay vì fn).


Tham chiếu các vars "ma"

Tôi đang nói về loại var mà bạn có thể xác định bằng REPL - trong quá trình lập trình khám phá của bạn - sau đó tham chiếu vô tình trong nguồn của bạn. Mọi thứ hoạt động tốt cho đến khi bạn tải lại không gian tên (có thể bằng cách đóng trình chỉnh sửa của bạn) và sau đó khám phá ra một loạt các ký hiệu không liên kết được tham chiếu trong mã của bạn. Điều này cũng thường xuyên xảy ra khi bạn đang cấu trúc lại, di chuyển một var từ vùng tên này sang vùng tên khác.


Xử lý việc hiểu danh sách for giống như một vòng lặp for bắt buộc

Về cơ bản, bạn đang tạo một danh sách lười biếng dựa trên các danh sách hiện có thay vì chỉ thực hiện một vòng lặp có kiểm soát. Liều lượng của Clojure thực sự tương tự hơn với các cấu trúc vòng lặp foreach bắt buộc.

Một ví dụ về cách chúng khác nhau là khả năng lọc các yếu tố mà chúng lặp lại bằng cách sử dụng các vị từ tùy ý:

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)

Một cách khác mà chúng khác biệt là chúng có thể hoạt động trên chuỗi lười biếng vô hạn:

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

Chúng cũng có thể xử lý nhiều hơn một biểu thức ràng buộc, lặp lại biểu thức ngoài cùng bên phải trước và làm việc theo cách sang trái:

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

Cũng không có nghỉ hoặc tiếp tục thoát sớm.


Lạm dụng quá nhiều cấu trúc

Tôi đến từ một nền tảng OOPish nên khi tôi bắt đầu Clojure, bộ não của tôi vẫn đang suy nghĩ về các đối tượng. Tôi thấy mình đang mô hình hóa mọi thứ như một cấu trúc vì nhóm các "thành viên" của nó, tuy lỏng lẻo, nhưng lại khiến tôi cảm thấy thoải mái. Trong thực tế, cấu trúc chủ yếu nên được coi là một tối ưu hóa; Clojure sẽ chia sẻ chìa khóa và một số thông tin tra cứu để tiết kiệm bộ nhớ. Bạn có thể tiếp tục tối ưu hóa chúng bằng cách định nghĩa accessors để đẩy nhanh quá trình tra cứu chủ chốt.

Nhìn chung, bạn không thu được gì từ việc sử dụng cấu trúc trên bản đồ ngoại trừ hiệu suất, do đó, sự phức tạp thêm vào có thể không đáng.


Sử dụng các hàm tạo BigDecimal không được kiểm soát

Tôi cần rất nhiều BigDecimals và đang viết mã xấu xí như thế này:

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

trong khi thực tế, Clojure hỗ trợ các ký tự BigDecimal bằng cách thêm M vào số:

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

Sử dụng phiên bản có đường sẽ giúp loại bỏ rất nhiều khối phồng. Trong các nhận xét, twils đã đề cập rằng bạn cũng có thể sử dụng các hàm bigdecbigint để rõ ràng hơn nhưng vẫn ngắn gọn.


Sử dụng chuyển đổi đặt tên gói Java cho không gian tên

Đây thực sự không phải là một sai lầm, mà là một cái gì đó đi ngược lại cấu trúc thành ngữ và cách đặt tên của một dự án Clojure điển hình. Dự án Clojure quan trọng đầu tiên của tôi có khai báo không gian tên - và cấu trúc thư mục tương ứng - như thế này:

(ns com.14clouds.myapp.repository)

điều này làm tăng các tham chiếu chức năng đủ điều kiện của tôi:

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

Để làm mọi thứ phức tạp hơn nữa, tôi đã sử dụng cấu trúc thư mục Maven chuẩn :

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

phức tạp hơn cấu trúc Clojure "tiêu chuẩn" của:

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

đó là mặc định của các dự án Leiningen và chính Clojure .


Bản đồ sử dụng các hàm equals () của Java thay vì Clojure's = để khớp khóa

Được báo cáo ban đầu bởi chouser trên IRC , việc sử dụng equals () của Java này dẫn đến một số kết quả không trực quan:

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

Vì cả hai phiên bản Số nguyênDài của 1 đều được in giống nhau theo mặc định, có thể khó phát hiện tại sao bản đồ của bạn không trả về bất kỳ giá trị nào. Điều này đặc biệt đúng khi bạn chuyển khóa của mình qua một hàm mà có lẽ bạn không biết, trả về lâu.

Cần lưu ý rằng việc sử dụng equals () của Java thay vì = của Clojure là điều cần thiết để bản đồ tuân theo giao diện java.util.Map.


Tôi đang sử dụng Clojure Lập trình của Stuart Halloway, Clojure Thực tế của Luke VanderHart, và sự trợ giúp của vô số tin tặc Clojure trên IRC và danh sách gửi thư để giúp tôi trả lời.


1
Tất cả các macro của trình đọc đều có phiên bản chức năng bình thường. Bạn có thể làm (#(hash-set %1 %2) :a 1)hoặc trong trường hợp này (hash-set :a 1).
Brian Carper

2
Bạn cũng có thể 'loại bỏ' ngoặc thêm đậm đà bản sắc: (# (sắc {% 1% 2}): 1)

1
Bạn cũng có thể sử dụng do: (#(do {%1 %2}) :a 1).
Michał Marczyk

@ Michał - Tôi không thích giải pháp này càng nhiều càng tốt những người trước đây vì làm ngụ ý rằng một tác dụng phụ đang diễn ra, trong khi thực tế đây không phải là trường hợp ở đây.
Robert Campbell

@ rrc7cz: Chà, trên thực tế, không cần thiết phải sử dụng một hàm ẩn danh ở đây, vì sử dụng hash-maptrực tiếp (như trong (hash-map :a 1)hoặc (map hash-map keys vals)) sẽ dễ đọc hơn và không ngụ ý rằng một cái gì đó đặc biệt và gần như chưa được thực hiện trong một hàm được đặt tên đang diễn ra ( #(...)tôi thấy việc sử dụng does ngụ ý). Trên thực tế, việc lạm dụng quá nhiều fns ẩn danh là một điều cần phải suy nghĩ. :-) OTOH, đôi khi tôi sử dụng dotrong các hàm ẩn danh siêu ngắn gọn mà không có tác dụng phụ ... Rõ ràng là chúng chỉ nhìn thoáng qua. Tôi đoán là một vấn đề về hương vị.
Michał Marczyk

42

Quên bắt buộc đánh giá seq lười biếng

Các seq lười biếng sẽ không được đánh giá trừ khi bạn yêu cầu chúng được đánh giá. Bạn có thể mong đợi điều này để in một cái gì đó, nhưng nó không.

user=> (defn foo [] (map println [:foo :bar]) nil)
#'user/foo
user=> (foo)
nil

Cái mapkhông bao giờ được đánh giá, nó âm thầm bị loại bỏ, vì nó lười biếng. Bạn phải sử dụng một trong những doseq, dorun, doallvv để buộc đánh giá chuỗi lười biếng cho tác dụng phụ.

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

Sử dụng trần map tại REPL trông có vẻ như nó hoạt động, nhưng nó chỉ hoạt động vì REPL buộc đánh giá chính các seq lười biếng. Điều này có thể làm cho lỗi thậm chí khó nhận thấy hơn, bởi vì mã của bạn hoạt động ở REPL và không hoạt động từ tệp nguồn hoặc bên trong một hàm.

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

1
+1. Điều này khiến tôi khó chịu, nhưng theo cách thâm hiểm hơn: Tôi đang đánh giá (map ...)từ bên trong (binding ...)và tự hỏi tại sao các giá trị ràng buộc mới không được áp dụng.
Alex B

20

Tôi là một noob Clojure. Người dùng cao cấp hơn có thể gặp nhiều vấn đề thú vị hơn.

cố gắng in chuỗi lười biếng vô hạn.

Tôi biết mình đang làm gì với các chuỗi lười biếng của mình, nhưng vì mục đích gỡ lỗi, tôi đã chèn một số lệnh gọi print / prn / pr, tạm thời quên rằng mình đang in gì. Thật nực cười, tại sao PC của tôi lại bị treo?

cố gắng lập trình Clojure một cách ẩn ý.

Có một số cám dỗ để tạo ra rất nhiều refhoặcatom mã và viết mã liên tục thay đổi trạng thái của chúng. Điều này có thể được thực hiện, nhưng nó không phù hợp. Nó cũng có thể có hiệu suất kém và hiếm khi được hưởng lợi từ nhiều lõi.

cố gắng lập trình Clojure 100% về chức năng.

Mặt trái của điều này: Một số thuật toán thực sự muốn có một chút trạng thái có thể thay đổi. Tôn giáo tránh trạng thái có thể thay đổi bằng mọi giá có thể dẫn đến các thuật toán chậm hoặc khó xử. Cần có óc phán đoán và một chút kinh nghiệm để đưa ra quyết định.

cố gắng làm quá nhiều điều trong Java.

Bởi vì việc tiếp cận với Java rất dễ dàng, đôi khi bạn có thể sử dụng Clojure như một trình bao bọc ngôn ngữ kịch bản xung quanh Java. Chắc chắn bạn sẽ cần thực hiện chính xác điều này khi sử dụng chức năng thư viện Java, nhưng có rất ít ý nghĩa trong việc (ví dụ) duy trì cấu trúc dữ liệu trong Java hoặc sử dụng các kiểu dữ liệu Java chẳng hạn như bộ sưu tập mà có tương đương tốt trong Clojure.


13

Rất nhiều thứ đã được đề cập. Tôi sẽ chỉ thêm một cái nữa.

Clojure nếu xử lý các đối tượng Java Boolean luôn là true ngay cả khi giá trị của nó là false. Vì vậy, nếu bạn có một hàm java land trả về giá trị java Boolean, hãy đảm bảo rằng bạn không kiểm tra trực tiếp (if java-bool "Yes" "No") mà thay vào đó (if (boolean java-bool) "Yes" "No") .

Tôi đã bị cháy bởi điều này với thư viện clojure.contrib.sql trả về các trường boolean cơ sở dữ liệu dưới dạng đối tượng Boolean của java.


8
Lưu ý rằng (if java.lang.Boolean/FALSE (println "foo"))không in foo. (if (java.lang.Boolean. "false") (println "foo"))Tuy nhiên, (if (boolean (java.lang.Boolean "false")) (println "foo"))không , trong khi không ... Thực sự khá khó hiểu!
Michał Marczyk

Nó có vẻ hoạt động như mong đợi trong Clojure 1.4.0: (khẳng định (=: false (nếu Boolean / FALSE: true: false)))
Jakub Holý

Tôi cũng đã bị cháy bởi cái này gần đây khi thực hiện (filter: mykey coll) where: mykey's values ​​where Booleans - hoạt động như mong đợi với các bộ sưu tập do Clojure tạo, nhưng KHÔNG phải với các bộ sưu tập được deserialized, khi được tuần tự hóa bằng cách sử dụng tuần tự hóa Java mặc định - bởi vì các Boolean đó được deserialized là Boolean mới () và thật đáng buồn (Boolean mới (true)! = java.lang.Boolean / TRUE)
Hendekagon

1
Chỉ cần nhớ các quy tắc cơ bản của giá trị Boolean trong Clojure - nilfalselà sai, và mọi thứ khác đều đúng. Java Booleankhông phải nilvà nó không phải false(vì nó là một đối tượng), vì vậy hành vi là nhất quán.
erikprice

13

Giữ đầu của bạn trong vòng lặp.
Bạn có nguy cơ hết bộ nhớ nếu bạn lặp lại các phần tử của một chuỗi lười có khả năng rất lớn hoặc vô hạn trong khi vẫn giữ tham chiếu đến phần tử đầu tiên.

Quên là không có TCO.
Các cuộc gọi đuôi thông thường tiêu tốn không gian ngăn xếp và chúng sẽ tràn nếu bạn không cẩn thận. Clojure đã 'recurvà đang 'trampolinexử lý nhiều trường hợp mà các cuộc gọi đuôi được tối ưu hóa sẽ được sử dụng trong các ngôn ngữ khác, nhưng các kỹ thuật này phải được áp dụng một cách có chủ ý.

Trình tự không khá-lười biếng.
Bạn có thể xây dựng một chuỗi lười với 'lazy-seqhoặc 'lazy-cons(hoặc bằng cách xây dựng dựa trên các API lười cấp cao hơn), nhưng nếu bạn bọc nó vào 'vechoặc chuyển nó qua một số hàm khác nhận ra chuỗi, thì nó sẽ không còn lười nữa. Cả stack và heap đều có thể bị tràn bởi điều này.

Đưa những thứ có thể thay đổi trong giới thiệu.
Về mặt kỹ thuật, bạn có thể làm điều đó, nhưng chỉ tham chiếu đối tượng trong bản thân tham chiếu mới được điều chỉnh bởi STM - không phải đối tượng được giới thiệu và các trường của nó (trừ khi chúng là bất biến và trỏ đến các tham chiếu khác). Vì vậy, bất cứ khi nào có thể, chỉ nên chọn các đối tượng không thay đổi trong refs. Điều tương tự cũng xảy ra đối với các nguyên tử.


4
nhánh phát triển sắp tới đi một chặng đường dài hướng tới việc giảm mục đầu tiên bằng cách xóa các tham chiếu đến các đối tượng trong một hàm khi chúng không thể truy cập cục bộ.
Arthur Ulfeldt

9

sử dụng loop ... recurđể xử lý trình tự khi bản đồ sẽ thực hiện.

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

so với

(map do-stuff data)

Chức năng bản đồ (trong nhánh mới nhất) sử dụng trình tự phân đoạn và nhiều cách tối ưu hóa khác. Ngoài ra, vì chức năng này thường xuyên được chạy, Hotspot JIT thường tối ưu hóa nó và sẵn sàng hoạt động sau bất kỳ "thời gian khởi động" nào.


1
Hai phiên bản này thực tế không tương đương. workChức năng của bạn tương đương với (doseq [item data] (do-stuff item)). (Bên cạnh thực tế, rằng vòng lặp trong công việc không bao giờ kết thúc.)
kotarak

vâng, cái đầu tiên phá vỡ sự lười biếng đối với các lập luận của nó. seq kết quả sẽ có cùng giá trị mặc dù nó không còn là một seq lười biếng nữa.
Arthur Ulfeldt

+1! Tôi đã viết nhiều hàm đệ quy nhỏ chỉ để tìm một ngày khác mà tất cả những hàm này có thể được tổng quát hóa bằng cách sử dụng mapvà / hoặc reduce.
n person325681

5

Các loại tập hợp có các hành vi khác nhau đối với một số hoạt động:

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]

Làm việc với các chuỗi có thể gây nhầm lẫn (tôi vẫn chưa hiểu lắm). Cụ thể, các chuỗi không giống như chuỗi các ký tự, mặc dù các hàm chuỗi hoạt động trên chúng:

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

Để lấy lại một chuỗi, bạn cần làm:

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

3

quá nhiều tham số, đặc biệt là với lệnh gọi phương thức void java bên trong dẫn đến NPE:

public void foo() {}

((.foo))

kết quả là NPE từ các thông số bên ngoài vì các thông số bên trong đánh giá bằng 0.

public int bar() { return 5; }

((.bar)) 

dẫn đến việc gỡ lỗi dễ dàng hơn:

java.lang.Integer cannot be cast to clojure.lang.IFn
  [Thrown class java.lang.ClassCastException]
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.