web-dev-qa-db-ja.com

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?onlyは、指定された要素がインデックスの有効範囲内にあることを確認します(ゼロベース):

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

リストを指定した場合、contains?は決してtrueを返しません。

93
fogus

リテラルオクタル

ある時点で、適切な行と列を維持するために先行ゼロを使用したマトリックスを読み取りました。数学的には、先行ゼロは明らかに基礎となる値を変更しないため、これは正しいです。ただし、このマトリックスでvarを定義しようとすると、次のように不思議に失敗します。

Java.lang.NumberFormatException: Invalid number: 08

それは完全に私を困惑させました。その理由は、Clojureは、先行ゼロを含むリテラル整数値を8進数として扱い、8進数には08がないためです。

また、Clojureは従来のJava 16進値をxプレフィックスを介してサポートしていることにも言及する必要があります。「base + r +値の表記法2r101016r16など、42の10進数です。


匿名関数リテラルでリテラルを返そうとしています

これは動作します:

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

マップリテラルをかっこで囲みます。これは最初の要素なので、関数(リテラルマップは実際)として扱われますが、必須の引数(キーなど)は提供されません。要約すると、匿名関数リテラルはnotに展開します

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

そのため、匿名関数の本体としてリテラル値([] 、: a、4、%)を使用することはできません。

コメントには2つの解決策が示されています。 Brian Carperは、次のようなシーケンス実装コンストラクター(配列マップ、ハッシュセット、ベクトル)の使用を提案します。

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

while Danは、 identity 関数を使用して外側の括弧を展開できることを示しています。

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

ブライアンの提案は実際に私の次の間違いに私を連れて来ます...


ハッシュマップ または 配列マップ と考えて、不変の具体的なマップの実装を決定

以下を考慮してください。

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を返します。これは、より大きなマップでより高速に実行されます。


loop ではなく関数を再帰ポイントとして使用して初期バインディングを提供する

私が始めたとき、私はこのような多くの関数を書きました:

; 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 がこの特定の関数に対してより簡潔で慣用的な場合:

; 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を移動します。


for list comprehension を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)

それらが異なるもう1つの方法は、無限の遅延シーケンスを操作できることです。

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

途中で終了するbreakまたはcontinueもありません。


構造体の過剰使用

私はOOPishのバックグラウンドを持っているので、Clojureを始めたとき、私の脳はまだオブジェクトの観点で考えていました。私は自分がすべてをstructとしてモデリングしていることに気付きました。なぜなら、その「メンバー」のグループ化は、どんなにゆるいものであっても、私が安心できるからです。実際には、structsは主に最適化と見なされるべきです。 Clojureはメモリを節約するためにキーといくつかのルックアップ情報を共有します。 accessors を定義してキールックアッププロセスを高速化することにより、さらに最適化できます。

全体的に、パフォーマンスを除いてstructmapよりも使用しても何も得られないため、複雑さを追加しても価値がない場合があります。


糖化されていないBigDecimalコンストラクターの使用

多くの 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-name "foo")

さらに事態を複雑にするために、標準の Maven ディレクトリ構造を使用しました。

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

次の「標準」Clojure構造よりも複雑です。

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

Leiningen プロジェクトおよび Clojure 自体のデフォルトです。


マップはClojureの=ではなくJavaのequals()をキーマッチングに使用します

当初chouser on [〜#〜] 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

IntegerLongの両方のインスタンスがデフォルトで同じように出力されるため、マップが値を返さない理由を検出することは困難です。これは、おそらく知らないうちにlongを返す関数を介してキーを渡すときに特に当てはまります。

Clojureの=の代わりにJavaのequals()を使用することは、マップがJava.util.Mapインターフェースに準拠するために不可欠であることに注意してください。


私は Programming Clojure by Stuart Halloway、 Practical Clojure by Luke VanderHart、および [〜#〜] irc [〜 #〜] およびメーリングリストは私の答えに沿って助けてくれます。

71
Robert Campbell

遅延シーケンスの評価を強制することを忘れる

遅延シーケンスは、評価するように依頼しない限り評価されません。あなたはこれが何かを印刷すると期待するかもしれませんが、そうではありません。

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

mapは決して評価されず、怠zyであるため静かに破棄されます。副作用の遅延シーケンスの評価を強制するには、doseqdorundoallなどのいずれかを使用する必要があります。

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

REPLのように見える裸のmapを使用すると、動作するように見えますが、REPLが遅延シーケンス自体の評価を強制するためにのみ動作しますコードはREPLで動作し、ソースファイルまたは関数内では動作しないため、バグをさらに認識しにくくすることができます。

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

私はClojure初心者です。より上級のユーザーには、より興味深い問題があるかもしれません。

無限の遅延シーケンスを印刷しようとしています。

レイジーシーケンスで何をしていたかはわかっていましたが、デバッグのために、print/prn/pr呼び出しを挿入しました。おかしい、私のPCがすべてハングアップしたのはなぜですか?

clojureを命令的にプログラムしようとしています。

refsまたはatomsを大量に作成し、常に状態を偽造するコードを作成する誘惑があります。これは実行できますが、適切ではありません。また、パフォーマンスが低下し、複数のコアの恩恵を受けることはほとんどありません。

clojureを100%機能的にプログラムしようとしています。

これに対する裏返​​し:一部のアルゴリズムは、実際に少し可変状態が必要です。すべてのコストで宗教的に可変状態を回避すると、アルゴリズムが遅くなったり、扱いにくくなったりする可能性があります。判断を下すには判断と少しの経験が必要です。

javaでやりすぎです。

Javaに簡単にアクセスできるため、ClojureをJavaのスクリプト言語ラッパーとして使用したくなることがあります。確かに、Javaライブラリ機能を使用する場合、これを正確に行う必要がありますが、Javaでデータ構造を維持したり、Java Clojureに相当するものがあるコレクションなどのデータ型。

21
Carl Smotricz

すでに述べたことがたくさんあります。もう1つだけ追加します。

Clojure ifは、Java値がfalseであっても常にブールオブジェクトをtrueとして扱います。したがって、Java Javaブール値、直接チェックしないことを確認してください(if Java-bool "Yes" "No") むしろ (if (boolean Java-bool) "Yes" "No")

これにより、データベースのブール値フィールドをJavaブール値オブジェクトとして返すclojure.contrib.sqlライブラリーで焼かれました。

13
Vagif Verdi

loop ... recurを使用して、mapが実行するときにシーケンスを処理します。

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

vs.

(map do-stuff data)

(最新のブランチの)map関数は、チャンクシーケンスと他の多くの最適化を使用します。また、この関数は頻繁に実行されるため、Hotspot JITは通常、最適化され、「ウォームアップ時間」なしで実行できる状態になっています。

9
Arthur Ulfeldt

コレクションタイプには、一部の操作で異なる動作があります:

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"
5
Matt Fenwick

特にvoid Javaメソッド呼び出しでNPEが発生する場合:

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]
3
miaubiz