コーディングスタイル


コーディングスタイルは人それぞれですが、ある程度、こうした方が良い、という緩やかなコンセンサスが、CLerの間でできているものもあります。定番のものから意見の分かれるものまで、コーディングスタイルについて書くページです。



コメント

Tutorial on Good Lisp Programming Styleの「3. Tips on Near-Standard Tools」では、

Obey comment conventions
; for inline comment
;; for in-function comment
;;; for between-function comment
;;;; for section header (for outline mode)

と紹介されています。GNU Emacs Lisp Reference ManualにおけるAppendix DD.7 Tips on Writing Commentsではコード例を交えて、また、WiLiKi:Lisp:コメントでは日本語で、同じ趣旨のことが書いてあります。後者に簡潔で分かりやすいコード例があるので引用します。

;;;; ファイル全体に対するコメント

;;; いくつかの関数グループに対するコメント

(defun foo (args)
  (let ((a 0) (b 1))
    ;; 関数内コードブロックに対するコメント
    (apply #'+ a b args)    ; 一つの式に対するコメント
  ))

ひとつの行をあまり長くしない

(書きかけ)


命名規則

(書きかけ)


関数は簡潔に

Maximize LOCNW: lines of code not written.
"Shorter is better and shortest is best."
- Jim Meehan

これはTutorial on Good Lisp Programming Styleからの引用ですが、別にコードゴルフの勧めというわけではなく、無駄なコードは書かないようにしましょう、ということです。他の言語でも良く言われることと同じです。

FAQ: Lisp Frequently Asked QuestionsHow can I improve my Lisp programming style and coding efficiency?にも同じことが書いてあります。

Write short functions, where each function provides a single, well-defined operation. Small functions are easier to read, write, test, debug, and understand.

標準ライブラリにあるものを再発明したり、同じ計算を何度もしたり、関数に機能をいくつも詰め込んだりせずに、関数を簡潔に保つことで、書くのも簡単で、読みやすく、テストもデバッグもしやすくなります。


関数や変数の名前は分かりやすく

(書きかけ)


条件式の使い分け

(書きかけ)


省略可能な引数とキーワード引数を一緒に使わない

省略可能な引数とキーワード引数を一緒に使うのは避けるべきです。キーワード引数の指定のつもりで、省略可能な引数に値を渡してしまって、見付けにくいバグにつながる事があります。

次の例を見てください。

(defun f (x &optional y z &key k)
  (list x y z k))

(f 0)
;=> (0 NIL NIL NIL)
(f 0 1)
;=> (0 1 NIL NIL)
(f 0 1 2)
;=> (0 1 2 NIL)
(f 0 :z 1)
;=> (0 :Z 1 NIL)
(f 0 1 :z 2)
;>> Error

(f 0 :z 1)のケースが特に危険で、省略可能な引数の値としてキーワードシンボルが使えてしまえる場合、一見問題なさそうに見えるが、動作がおかしい、ということになりかねません。

そのような関数は定義しない、と思うかもしれませんが、実は標準ライブラリにも存在します。read-from-stringの定義を見てください。まさにこのパターンです。利用するときには注意しましょう。FAQ: Lisp Frequently Asked Questionsでも、Why does (READ-FROM-STRING "foobar" :START 3) return FOOBAR instead of BAR?で取り上げられています。


真偽値の偽と空リストを区別する

これは賛否が分かれるのではないでしょうか。

FAQ: Lisp Frequently Asked QuestionsHow can I improve my Lisp programming style and coding efficiency?では、可読性を良くする方法のひとつとして、

When NIL is used as an empty list, use () in your code. When NIL is used as a boolean, use NIL. Similarly, use NULL to test for an empty list, NOT to test a logical value. Use ENDP to test for the end of a list, not NULL.

のように、真偽値の偽と空リストを区別して扱うスタイルが提案されています。

Common Lisp HyperSpecでは、26.1 Glossarynilの項

nil n. the object that is at once the symbol named "NIL" in the COMMON-LISP package, the empty list, the boolean (or generalized boolean) representing false, and the name of the empty type.

と定義されています。nilには真偽値の偽としての意味と、空リストとしての意味、型の名前としての意味があるわけです。これは、Lispではリストの処理を良くするので都合が良く、コードを簡潔にするのに一役買っています。

;; nilと空リストが同じものと仮定したコード
(labels ((rec (rest r)
           (if rest
               (rec (cdr rest) (+ r (car rest)))
               r)))
  (rec '(1 2 3 4 5 6 7 8 9) 0))
;=> 45

;; nilと空リストが違うものと仮定したコード
(labels ((rec (rest r)
           (if (null rest)      ; 空リストも真になるので条件式に直接渡せない
               r
               (rec (cdr rest) (+ r (car rest))))))
  (rec '(1 2 3 4 5 6 7 8 9) 0))

ですが、真偽値の偽と空リストという、本来は違う概念をひとつの同じものとして扱っていることへの反対意見もあります。その考えを推し進めたものが、Schemeでの真偽値の偽と空リストの扱いですが(偽は#fで空リストは()と別物)、nilと()の使い分けというのは、ややこちら寄りの考えと言えます。

;; 空リストは()
(reduce (lambda (r x) (cons x r))
        '(1 2 3)
        :initial-value '())
;=> (3 2 1)

;; 真偽値はnil
(let ((*read-eval* nil))
  (read-from-string "#.(+ 1 2)"))
;>> Error

cddddrなどはなるべく使わない

(書きかけ)


循環リスト以外のリストの長さはlengthで

リストが循環リストでないなら、list-lengthよりもlengthの方が効率的です。lengthはシーケンスに対する汎用の関数のため、遅く思えますが、

;; Clozure CL 1.6 on Windows XP
(let ((l (make-list 100000)))
  (time (dotimes (n 10000) (list-length l)))
  (time (dotimes (n 10000) (length l))))
;-> (DOTIMES (N 10000) (LIST-LENGTH L)) took 5,375 milliseconds (5.375 seconds) to run 
;                       with 2 available CPU cores.
;   During that period, 5,141 milliseconds (5.141 seconds) were spent in user mode
;                       0 milliseconds (0.000 seconds) were spent in system mode
;    48 bytes of memory allocated.
;   (DOTIMES (N 10000) (LENGTH L)) took 2,265 milliseconds (2.265 seconds) to run 
;                       with 2 available CPU cores.
;   During that period, 2,156 milliseconds (2.156 seconds) were spent in user mode
;                       0 milliseconds (0.000 seconds) were spent in system mode
;    48 bytes of memory allocated.

実際にはlengthの方が効率的です。

これは、list-lengthが循環リストも扱えないといけないように決められているからです。Common Lisp HyperSpeclist-lengthによると、

Returns the length of list if list is a proper list. Returns nil if list is a circular list.

となっています。実際に循環リストに適用してみても、

(defparameter *circular* '#1=(a b . #1#))

(list-length *circular*)
;=> NIL
(length *circular*)
;>> Error

list-lengthはきちんと動作しますが、lengthはエラーになります。


連想リストへの要素の追加

FAQ: Lisp Frequently Asked QuestionsHow can I improve my Lisp programming style and coding efficiency?では、

When adding an entry to an association list, use ACONS, not two calls to CONS. This makes it clear that you're using an alist.

と書かれています。連想リストに要素を追加するときは、consを使うよりもaconsを使った方が、少しだけ簡潔になり、コードを読む人に連想リストを使っていることを伝えやすくなります。

(defparameter *alist* '((:a . 1) (:b . 2)))

; 少し冗長
(cons (cons :c 3) *alist*)
; 簡潔で連想リストを扱っていることも分かりやすい
(acons :c 3 *alist*)
;=> ((:C . 3) (:A . 1) (:B . 2))

リストの要素に関数を適用して戻り値を捨てるとき

副作用を目的として、リストの要素に関数を適用したい、つまり、Schemefor-eachと同じことをしたい場合、Common Lispではmapcを使うと良いとされています。これは、mapcが最初に渡されたリストをそのまま返し、新たなメモリの割り当てが起きないからです。

FAQ: Lisp Frequently Asked QuestionsHow can I improve my Lisp programming style and coding efficiency?には、

If you like using MAPCAR instead of DO/DOLIST, use MAPC when no result is needed -- it's more efficient, since it doesn't cons up a list.

と書かれています。

(mapc #'print '(0 1 2 3 4))
;=> (0 1 2 3 4)
;-> 0
;   1
;   2
;   3
;   4

ちなみに、

(dolist (x '(0 1 2 3 4))
  (print x))
;=> NIL
;-> 0
;   1
;   2
;   3
;   4

と書いても同じです。mapcの方が少しだけ簡潔で、関数的な書き方ですが、処理を追加したくなったとき、mapcでは関数合成やlambdaを使わなければなりません。どちらを選ぶかは趣味の問題でしょう。


*-if-notや:test-notは使わない

FAQ: Lisp Frequently Asked QuestionsHow can I improve my Lisp programming style and coding efficiency?では、

If using REMOVE and DELETE to filter a sequence, don't use the :test-not keyword or the REMOVE-IF-NOT or DELETE-IF-NOT functions. Use COMPLEMENT to complement the predicate and the REMOVE-IF or DELETE-IF functions instead.

のように、find-if-notremove-if-notなどの、条件が成り立たない場合に処理をするための関数や、:test-notを使うのは避け、*-ifや:testとcomplementを組み合わせるべきだと書かれています。

; find-if-notを使う場合
(find-if-not #'zerop '(0 0 3))
; complementを使って書く場合
(find-if (complement #'zerop) '(0 0 3))
;=> 3

なお、HyperSpecにも書いてありますが、*-if-notや:test-notは「deprecated(廃止予定)」です。

で詳しい内容が書かれています。complementがあれば*-if-notや:test-notは不要になり、:testと:test-notを一緒に使ったときの分かりにくい動作もなくせる、という理由によるものです。


openやcloseはできるだけ直接使わない

Use WITH-OPEN-FILE instead of OPEN and CLOSE.

FAQ: Lisp Frequently Asked QuestionsHow can I improve my Lisp programming style and coding efficiency?からの引用です。

opencloseを必要もないのに直接使うのは避けた方が良いと言われます。with-open-fileというマクロがあるからで、これはファイルを自動的に開き、閉じてくれるほか、エラーが発生したときでも確実にファイルを閉じてくれるという優れものです。

(defparameter *csv*
  (with-open-file (s "13tokyo.csv")
    (loop for line = (read-line s nil)
          while line
          collect line)))

プログラマはファイルに対する処理だけに集中できます。

このwith-*という形は何かと便利なので、リソースを確保して処理を行い、リソースを解放する、というパターンで幅広く使われます。APIのデザインをするときのために、覚えておくと良いでしょう。


evalの使用は慎重に

evalは慎重に使うべきだと言われています。

Tutorial on Good Lisp Programming Styleでは、「any use of eval」は「red flag(危険信号)」であるとされ、FAQ: Lisp Frequently Asked QuestionsHow can I improve my Lisp programming style and coding efficiency?では、

Novices almost always misuse EVAL. When experts use EVAL, they often would be better off using APPLY, FUNCALL, or SYMBOL-VALUE. Use of EVAL when defining a macro should set off a warning bell -- macro definitions are already evaluated during expansion. See also the answer to question 3-12. The general rule of thumb about EVAL is: if you think you need to use EVAL, you're probably wrong.

と書かれています。evalを使うのが正しいケースについては、When is it right to use EVAL?で書かれていますが、例えば、REPLをアプリケーションから提供するケースです。


defpackageなどで使われるstring designatorについて

requiredefpackageなどに渡す、モジュール名やパッケージ名として使われるstring designatorですが、この値として何を渡すか、たまに話題になることがあります。(例としてはA Question about DEFPACKAGE syntaxを見てください)

文字列

大文字を入力したり、ダブルクォートで囲む手間があるので少数派です。

> (require "ITERATE")
"ITERATE"
NIL
> (use-package "ITERATE")
T

普通にインターンされるシンボル

こちらも少数派です。意図しないシンボルの衝突が起きる危険があるので避けた方が良いでしょう。

> (require 'iterate)
ITERATE
NIL
> (use-package 'iterate)
Error: Using #<Package "ITERATE"> in #<Package "ITER-TEST"> 
       would cause name conflicts with symbols already present in that package: 
       ITERATE  ITERATE:ITERATE

キーワード

最近の多数派です。大文字を入力しないで済みますし、クォートする必要もありません。シンボルはキーワードパッケージにインターンされるので、意図しない衝突は起きません。

> (require :iterate)
:ITERATE
NIL
> (use-package :iterate)
T

インターンされないシンボル

多数派ではないですが、少数派ほど少ないわけでもありません。タイプ数はキーワードに比べて多くなります。シンボルがどのパッケージにもインターンされないのが利点です。衝突は起きません。

> (require '#:iterate)
#:ITERATE
NIL
> (use-package '#:iterate)
T

参考文献


Last modified : 2011/08/20 19:30:02 JST
CC0 1.0
Powerd by WiLiKi 0.6.1 on Gauche 0.9