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を確認してください。ドキュメントから:使用法:(includes?coll x)。線形時間でcollがxと等しい(=を含む)何かを含む場合、trueを返します。
ロバートキャンベル

11
あなたはこれらの質問は、コミュニティのWikiであるという事実を見逃しているように見える

3
Perlの質問が他のすべての質問とどのように一致していないかが大好きです:)
Ether

8
コンテナーを探しているClojure開発者には、rcampbellのアドバイスに従わないことをお勧めします。seq-utilsはずっと以前から非推奨であり、その関数はそもそも役に立ちませんでした。Clojureのsome関数を使用することもできますが、より良いのはcontainsそれ自体を使用することです。Clojureコレクションが実装しjava.util.Collectionます。(.contains [1 2 3] 2) => true
Rayne

回答:


70

リテラル8進数

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

java.lang.NumberFormatException: Invalid number: 08

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

Clojureは0xプレフィックスを介して従来のJava 16進値をサポートすることにも言及する必要があります。「base + r + value」表記を使用して、2から36までの任意の基数を使用することもできます(2r101010または42rである36r16など)。


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

これは機能します:

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、%)を持つことはできません。

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

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

しばらくダンはあなたが使用できることを示してアイデンティティ外括弧のラップを解除する機能を:

(#(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のマップの具体的な実装を心配する必要はありませんが、あなたはマップを育てる機能することを知っておくべき連想またはCONJは -取ることができPersistentArrayMapをして返す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ループ構造に類似しています。

それらの違いの1つの例は、任意の述語を使用して反復する要素をフィルターする機能です。

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

また、何もありません破るか、引き続き早期に終了します。


構造体の過剰使用

私はOOPishの出身なので、Clojureを始めたとき、私の脳はまだオブジェクトのことを考えていました。「メンバー」のグループ化は、どのように緩くても快適に感じられるため、すべてを構造体としてモデル化することに気づきました。実際には、構造体は主に最適化と見なされます。Clojureは、メモリを節約するためにキーといくつかのルックアップ情報を共有します。キールックアッププロセスを高速化するアクセサーを定義することで、それらをさらに最適化できます。

全体的に、パフォーマンスを除いて、マップ上で構造体を使用しても何も得られないため、追加された複雑さは価値がないかもしれません。


無糖のBigDecimalコンストラクターの使用

私はたくさんのBigDecimalを必要とし、次のような醜いコードを書いていました:

(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/

これは、ライニンゲンプロジェクトとClojure自体のデフォルトです。


マップは、キーの照合にClojureの=ではなくJavaのequals()を利用します

もともとによって報告chouser上の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

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

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


私はStuart HallowayによるProgramming Clojure、Luke VanderHartによるPractical Clojure、そしてIRCの無数のClojureハッカーの助けと私の回答に役立つメーリングリストを使用しています。


1
すべてのリーダーマクロには、通常の関数バージョンがあります。あなたがすることができます(#(hash-set %1 %2) :a 1)またはこの場合(hash-set :a 1)
ブライアンカーパー、2010年

2
また、IDを使用して追加の括弧を「削除」することもできます:(#(identity {%1%2}):a 1)

1
また、使用することができますdo(#(do {%1 %2}) :a 1)
のMichałMarczyk

@Michał- 実際にはこれが当てはまらない場合でも、副作用が発生していることを意味するため、この解決策は以前の解決策ほど好きではありません。
ロバートキャンベル

@ rrc7cz:まあ、実際には、ここで匿名関数を使用する必要はまったくありません。hash-map直接((hash-map :a 1)またはのように(map hash-map keys vals))を使用する方が読みやすく、名前付き関数にまだ実装されていない特別なものやまだ実装されていないという意味ではありません。が行われている(これはの使用#(...)が意味することだと私は思う)。実際、匿名のfnsを使いすぎることは、それ自体を考える上での落とし穴です。:-) OTOH、私は時々do副作用のない超簡潔な無名関数で使用します...それらが一目であることが明白になる傾向があります。好みの問題だと思います。
のMichałMarczyk

42

レイジーシーケンスの評価を強制するのを忘れる

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

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

mapそれは怠け者だから、それは静かに捨てています、評価されることはありません。次のいずれかを使用する必要があり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

mapREPLでベアを使用すると、それは機能するように見えますが、REPLが遅延seqs自体の評価を強制するためにのみ機能します。コードはREPLで機能し、ソースファイルや関数内では機能しないため、これによりバグをさらに認識しにくくすることができます。

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

1
+1。これは私に噛みついたが、より陰湿な方法であった。私は(map ...)内部から評価していて(binding ...)、なぜ新しいバインディング値が適用されないのか疑問に思っていた。
アレックスB

20

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

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

怠惰なシーケンスで何をしているのかはわかっていましたが、デバッグの目的で、print / prn / pr呼び出しを挿入し、一時的に自分が何を印刷しているかを忘れてしまいました。おかしい、なぜ私のPCはすべてハングアップするのですか?

Clojureを命令的にプログラミングしようとしています。

refsやatomsを大量に作成し、その状態を常に悪用するコードを作成する誘惑があります。これは可能ですが、適切ではありません。また、パフォーマンスが低下し、マルチコアのメリットがほとんどない場合もあります。

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

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

Javaでやりすぎた。

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


13

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

Clojure ifは、値がfalseであっても、Javaブール型オブジェクトを常にtrueとして扱います。あなたはJavaの土地の機能を持っているのであれば返すJavaブール値は、あなたが直接それをチェックしません作ること (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

Clojure 1.4.0で期待どおりに機能するようです:(assert(=:false(if Boolean / FALSE:true:false)))
JakubHolýOct

私は最近(filter:mykey coll)を実行したときにもこの火傷を負いました:where mykey's values where Booleans-Clojureで作成されたコレクションでは期待どおりに機能しますが、デフォルトのJavaシリアル化を使用してシリアル化すると、逆シリアル化されたコレクションでは機能しません-これらのブール値は非シリアル化されますnew Boolean()として、そして悲しいことに(new Boolean(true)!= java.lang.Boolean / TRUE)
Hendekagon

1
Clojureのブール値の基本的なルールを思い出してください– nilそして、それfalseは偽であり、他のすべては真です。Java Booleanは存在せずnil、存在しないfalseため(オブジェクトであるため)、動作は一貫しています。
erikprice 2015年

13

頭を輪にしておく。
最初の要素への参照を維持したまま、潜在的に非常に大きい、または無限の遅延シーケンスの要素をループすると、メモリ不足になる可能性があります。

TCOがないことを忘れています。
通常の末尾呼び出しはスタック領域を消費し、注意しないとオーバーフローします。Clojureは'recur'trampoline最適化された末尾呼び出しが他の言語で使用される多くの場合を処理しますが、これらの手法は意図的に適用する必要があります。

かなり怠惰なシーケンス。orを使用して(または、より高いレベルの遅延APIを使用して)
遅延シーケンスを構築できますが、シーケンスをラップするか、シーケンスを実現する他の関数を介して渡すと、遅延はなくなります。これにより、スタックとヒープの両方がオーバーフローする可能性があります。'lazy-seq'lazy-cons'vec

参考に変更可能なものを置く。
技術的には実行できますが、STMによって管理されるのはref自体のオブジェクト参照のみであり、参照オブジェクトとそのフィールドではありません(それらが不変で他のrefを指している場合を除く)。したがって、可能な場合は常に、参照内の不変オブジェクトのみを優先してください。同じことが原子にも言えます。


4
次の開発ブランチは、関数内のオブジェクトへの参照がローカルで到達できなくなったときにそれを消去することにより、最初の項目を減らすのに大いに役立ちます。
Arthur Ulfeldt

9

loop ... recurマップが行うときにシーケンスを処理するために使用します。

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

(map do-stuff data)

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


1
これら2つのバージョンは実際には同等ではありません。あなたのwork機能は同等です(doseq [item data] (do-stuff item))。(事実を
除けば

はい、最初のものはその引数の怠惰を破ります。結果のシーケンスは同じ値になりますが、レイジーシーケンスではなくなります。
Arthur Ulfeldt

+1!私は多数の小さな再帰関数を作成しましたが、これらすべてがmapandおよび/またはを使用して一般化できる別の日を見つけましたreduce
nperson325681 2011年

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

括弧が多すぎます。特に、内部で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]
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.