Common Lisp コーディングスタイルについて

一つの文章は、一つ若しくは幾つかの單語から成り立つてゐるのでありますから、 單語の選擇のよしあしが根本であることは、申す迄もありません。そこで、 その選び方についての心得を申しませうなら、

 異を樹てようとするな

と云ふことに歸着するのであります。それを、もう少し詳しく、 箇條書きにして申しますと、

 一 分り易い語を選ぶこと

 二 成るべく昔から使ひ馴れた古語を選ぶこと

 三 適當な古語が見付からない時に、新語を使ふやうにすること

 四 古語も新語も見付からない時でも、造語、─ 自分で勝手に新奇な言葉を拵へることは愼しむべきこと

 五 據り所のある言葉でも、耳遠い、むづかしい成語よりは、耳馴れた外來語や俗語の方を選ぶべきこと

等であります。

        谷崎潤一郎 『文章讀本』

はじめに

先づ始めにお断りしておきますが、 ここで述べるコーディングスタイルを コーディング規則のやうな馬鹿げたものと同じにしないでもらひたい。 スタイルはあくまで指針であつて規則ではありません。 規則とはそれを強制するだけでなく、それが何故さうあるか考へることを禁ずるものです。 スタイルは、それが何故さうあるかを理解しない者に押し付けることが不可能な種類のものです。

自分で考へようとしない人にいくらスタイルを説いたところで無駄である。 このことを踏まへた上で次に進むことにしませう。

何故にスタイルが問題となるのか

わたしも含め、たれもがつい忘れがちなことは、「プログラムは人に讀まれる」といふことです。 コンピューターに向かつてゐると仕事とは本來「人」相手のものだといふ、 あたりまへの意識が薄れてきてしまふ。 しかしプログラマーが相手をするべきは、コンピューターよりもむしろ人だと考へるべきです。 あなたが今日書いたプログラムは明日たれか別の人が讀まんとも限らない。 たとへあなたしか讀まないプログラムであつても、 一年もたてばその「あなた」もまた「別人」になつてゐるのだ、 とたしか G.M. ワインバーグが何かの本で言つてました。 プログラムを書く上で留意すべき事項は多々ありますが、 人が讀んでわかるやうに書くこと、これが一番基本なことでせう。

文章だらうが、プログラムだらうが、人に讀んでわからせるためには、 そこに何かしらの型や樣式が必要となります。 プロトコルのないところに通信がなりたたないやうに、 型や樣式のないところに相互理解などありえない。 スタイルとは「型にはまる」ための手段と考へてもらふのが良い。

語彙

文章におけると同じやうに、プログラムをわかりやすく書くためには、 語彙力が大切なことは言ふまでもありません。幸ひ、Common Lisp はその仕樣が大きい。 つまり語彙が豊富に定義されてゐます。ANSI によれば Common Lisp があらかじめ定義し、 その使ひ道が定められてゐる語彙は個數にして 978 個あります。 これらを洩れなく理解し、適材適所に使ひわけることが、 わかりやすい Common Lisp プログラムを書くための第一の條件となります。

よく目にするのが、いくつか自分に覺えやすい語彙だけを覺えて、 その組合せだけでプログラムを書かうとする人が以外に多い、といふことです。 スタイルとしては最悪と言ふほかありません。 一時期「ボキャ貧」といふ單語がとりざたされたことがありましたが、 これはなにも夏電車の中で足の爪にマニキュアを塗るのに忙しいお姉さんだけのためにあるわけではありません。

まづ語彙を豊富にする。それには上に述べた 978 個の基本語彙を覺えることが出發點です。 當用漢字の 1850 個を書けないまでも讀める人ならば何といふこともない量でせう。

名前

基本語彙を覺えたならば、それを使つて自分のプログラムを書き始めることができますが、 プログラミング言語の最大の御利益は、レジスターやアドレスといつた無味乾燥なものに、 名前を付けることのできる點でせう。

名付けは抽象化の最も基本です。 これができないで構造化プログラミングもオジェクト指向もあつたもんぢやない。 バルザックでしたか、小説の主人公の名前に良いのがみつからないので、 パリ中の標札を歩いて見てまはつた、といふ話がありますが、 これほど拘ることはなくても、 せめて變數なり函數なりの役割を直感させるやうな、できるだけわかりやすい名前をつけませう。 面倒臭くなつて a とか aaa とかタイプの樂な短い名前をつけるなどはもつてのほかです。 無理に英語の名前を付ける必要はないので困つたときはローマ字で充分です。 本當に意味の無い名前ならば foo, bar, baz, quz など 「この名前には意味の無い」ことが誰にでもわかる名前をつけませう。

comp.lang.lisp で Kent M. Pitman が 「プログラムは電話口で讀み上げても相手がわかるやうな書き方をしろ」 と言つてゐたのを覺えてゐますが、 これも名前付けが適切に行なはれてこその話で、けだし名言といふべきでせう。

Common Lisp であらかじめ定義されてゐる中には CARCDR のやうに歴史的 経緯から來たもの以外は、 WITH-OPEN-FILE だの MULTIPLE-VALUE-BIND だの UPDATE-INSTANCE-FOR-REDEFINED-CLASS だの、ずいぶん冗長な名前が多いと 感じる人もゐることでせうが、省略されない長い名前は讀み易さにずいぶん貢献します。

なほ、まさかとは思ひますが、長い名前はタイプするときに不便ぢやないのか、 といふ意見があるかもしれませんので、「カードパンチの時代ではあるまいし、 エディターの補完機能くらいあるでせうに」と、念のため申し添へておきます。

條件分岐

Common Lisp には條件分岐 (if-then-else) を表すための基本操作子として IF が用意されてゐます。加へて、似たやうな表現手段に、 WHEN, UNLESS, COND, CASE がありますが、これらはすべて IF を拡張したマクロとして定義されてゐます。 つまり WHEN, UNLESS, COND, CASE は全て IF さへ知つていれば同等の表現ができるものであり、 言語仕樣としてはある意味冗長なものです。

しかし、これらを使ひ分けることは、可讀性を考へるとき重要となります。

例へば IFWHEN の syntax は次のやうに定義されてゐます。

    IF test-form then-form [else-form]   → {result}*

    WHEN test-form {form}*   → {result}*

IFtest-form がきて次に then-form が來、その次の else-form は書いても書かないでも良いことになつてゐます。 WHEN では IFthen-form にあたる form だけを書き、 else-form は書くことができません。 かういつた事實をふまへれば、プログラムを讀む人の自然な理解として、 IF を使ふ限りは、このコードの先には else-form があるはずだ、 と考へる。なぜなら else-form を書かないですむのなら WHEN を使へばよいわけだから。 つまり else-form の無い IF は讀者の直感を裏切るおそれが高い、 といふことです。なので、一般に、 IF を使ふのであれば then-formelse-form の両方を書き、 else-form を書く必要が無いの であれば WHEN を使ひませう、といふことが言へます。

また、

(if A X (if B Y (if C Z)))

のやうに、 test-form をいくつも竝べたい ときには COND を使つたはうがわかりやすい。

(cond ((A X) (B Y) (C Z)))

COND の syntax は以下、

        COND {clause}*   →  {result}*

        clause::= (test-form {form}*)

讀み手は COND と來れば、ああ、いくつか條件があるんだな、と思ひながら讀み進める。 逆に test-form が一つしかないときに COND を使ふのは、 これも讀者の直感を裏切るおそれが高い。

また列なる test-form が一つの變數の値にのみ着目するやうな場合、

(if (= a 0) X (if (= a 1) Y (if (= a 2) Z))) 

ならば、

(case a (0 X) (1 Y) (2 Z)) 

のやうにしてあげたほうが親切。

まとめると、

test-form があつて then-form と else-form の両方があるとき、

    (if test-form then-form else-form)

test-form があつて then-form だけがあるとき、

    (when test-form form)

test-form があつて else-form だけがあるとき、

    (unless test-form form)

test-form が複數續くとき、

    (cond (test-form-0 form0)
          (test-form-1 form1)
          ....)

test が一つの變數や keyform について繰り返されるとき、

    (case keyform
          (key0 form0)
          (key1 form1)
          ....)

としておくと讀み易い。

あくまでおほまかな指針であり、絶對のものと考へないこと。 何を使ふのかは文脈が決定するのであつて機械的な規則があるのではない、といふこと。

例へば、

(unless ...) 

と書くのも

(when (not ...) ...) 

と書くのも計算機にとつては 同じことですし、

(if A X Y) 

(if (not A) Y X)

も同樣です。これを NOT は使ふな、 とばかり、上の例で言ふと、どちらも最初のほうに揃へてしまへ、 といふのがコーディング規則で、そのときの文脈でどちらかに決まるべきだ、 といふのがコーディングスタイルだ、と言へばおわかりになつていただけるでせうか。

Iteration and Recursion

Interation は繰り返し、Recursion は再歸です。

Hackers という本に MIT にでしたか、初めて stack を持つ計算機 PDP がやつてきたとき、 再歸を使つて整數値を十進表記するプログラムがものすごく奇麗に書けた、 という話が載つてゐて、わたしもこの部分に感動したことを覺えてゐますが、 再歸が決まつたときの「氣持ち良さ」というのは プログラミングの愉しみの中でも特に印象に深いものです。

(defun princ-integer (n &optional (base 10))
  (multiple-value-bind (quotient remainder)
      (floor n base)
    (unless (zerop quotient)
      (princ-integer quotient))
    (format t "~X" remainder)
    n))

再歸は繰り返しのより一般的な概念ですから、 理屈の上では「繰り返し」を使つて書けるプログラムは何であれ「再歸」を使つても書ける、 といふことになります。

しかし、これを實踐してしまふのは非常に具合が惡い。

たとへば、「リスト中に現れる數を全て足しこむ」處理を再歸的に考へる人がゐるでせうか。 普通の人なら足し算の繰り返しとして、つまり iterative に考へるはずです。

CLtL2 にある example を實驗臺にしてみます。 以下は、名前のリストと對應する年齡のリストをもらつて、 平均年齡と「名前、年齡」のリストを多値として返すプログラムを、 iterative 版 と recursive 版 の二つ書いてみました。

;;; Iterative version
(defun count-and-collect-names-and-ages-i (name-list age-list)
  (loop for name in name-list
      as age in age-list
      append (list name age) into name-and-age-list 
      count name into name-count 
      sum age into total-age 
      finally 
        (return (values (round total-age name-count) 
                        name-and-age-list))))
=> COUNT-AND-COLLECT-NAMES-AND-AGES-I

;;; Recursive version
(defun count-and-collect-names-and-ages-r (name-list age-list)
  (labels ((count-and-collect-aux (name-list age-list
                                   name-and-age-list name-count total-age)
             (if (or (null name-list) (null age-list))
                 (values (round total-age name-count) 
                         name-and-age-list)
               (let ((name (first name-list))
                     (age (first age-list)))
                 (count-and-collect-aux (rest name-list) (rest age-list)
                                        (append name-and-age-list
                                                (list name age))
                                        (1+ name-count)
                                        (+ total-age age))))))
    (count-and-collect-aux name-list age-list '() 0 0))) 
=> COUNT-AND-COLLECT-NAMES-AND-AGES-R

(count-and-collect-names-and-ages-i '(fred sue alice joe june) '(22 26 19 20 10))
=> 19 and (FRED 22 SUE 26 ALICE 19 JOE 20 JUNE 10)

(count-and-collect-names-and-ages-r '(fred sue alice joe june) '(22 26 19 20 10))
=> 19 and (FRED 22 SUE 26 ALICE 19 JOE 20 JUNE 10)

二つのプログラムは機能は同じで、見掛けも同樣の複雜さに見えますし、 實行效率もほぼ變はりません。それでもかういつた「數へ上げ」などの interative な處理をわざわざ再歸を使つて書くことは、 とても惡いスタイルだと思ひますし、何より素直でない。 見てわかるやうに、interative 版が、 上から下へ素直に讀み下していくことができるのに、recursive 版は、 (末尾再歸になつてゐるために計算機のスタックは使はないで濟むかも知れませんが) count-and-collect-names-and-ages-r の一番下の行で local 函數 count-and-collect-aux が呼び出されてゐることを先づ確認し、 それからまた目を上へ移してその定義内容を讀むといつたことをしなければならず、 ちやうど頭でつかちの文章を讀むのと同じで、 人の頭のスタックをかなり無駄に消費します。

讀む人の身になつて考へることができるのなら、 interative に表現するのが自然な algorithm は繰り返し構文を使つて書くべきですし、 recursive に表現するのが自然な algorithm は再歸構文を使つて書くべきです。

NIL, 'NIL, (), '()

NIL, 'NIL, (), '() は計算機にとつては全て同じ NIL です。

(list nil 'nil () '()) → (NIL NIL NIL NIL)

しかし人にとつてその意味合ひは違ひます。 おほまかな指針として、

nil 

は眞僞の僞

'nil 

は "NIL" といふ名のシンボル

()

は空式

'()

は空リスト

としておけば良いです。

Hyperspec にも次のやうに出てゐます。

| For Evaluation? | Notation | Typically Implied Role      |
|-----------------+----------+-----------------------------|
| Yes             | nil      | use as a boolean.           |
| Yes             | 'nil     | use as a symbol.            |
| Yes             | '()      | use as an empty list        |
| No              | nil      | use as a symbol or boolean. |
| No              | ()       | use as an empty list.       |

Figure 1-1. Notations for NIL

Within this document only, nil is also sometimes notated as false to
emphasize its role as a boolean.

For example:

 (print ())                          ;avoided
 (defun three nil 3)                 ;avoided 
 '(nil nil)                          ;list of two symbols
 '(() ())                            ;list of empty lists
 (defun three () 3)                  ;Emphasize empty parameter list.
 (append '() '()) =>  ()              ;Emphasize use of empty lists
 (not nil) =>  true                   ;Emphasize use as Boolean false
 (get 'nil 'color)                   ;Emphasize use as a symbol

序に、やはり Hyperspec からもう一つ例を擧げておきませう。

 (defun tokenize-sentence (string)
   (macrolet ((add-word (wvar svar)
                `(when ,wvar
                   (push (coerce (nreverse ,wvar) 'string) ,svar)
                   (setq ,wvar nil))))
     (loop with word = '() and sentence = '() and endpos = nil
           for i below (length string)
           do (let ((char (aref string i)))
                (case char
                  (#\Space (add-word word sentence))
                  (#\. (setq endpos (1+ i)) (loop-finish))
                  (otherwise (push char word))))
           finally (add-word word sentence)
                   (return (values (nreverse sentence) endpos)))))
=>  TOKENIZE-SENTENCE

 (tokenize-sentence "this is a sentence. this is another sentence.")
=>  ("this" "is" "a" "sentence"), 19

 (tokenize-sentence "this is a sentence")
=>  ("this" "is" "a" "sentence"), NIL

上で

    (loop with word = '() and sentence = '() and endpos = nil

とあるところ、 wordchar が push されていくためのリスト、 sentence は word が push されていくためのリスト、 であるところから、各々 `() で初期化されてゐます。 一方、 endpos は讀み終へた位置、即ち數値が入るものですから、 初期値として「僞」を與へてゐます。 これを計算機にとつて同じだからだと、

    (loop with word = nil and sentence = nil and endpos = nil

としてしまつたのでは、讀み手に無用の混乱を與へることになります。

なほ、

                  (setq ,wvar nil))))

のところは本來、

                  (setq ,wvar ,'())))

としても好いのですが、無用のカンマを使ふことによる混亂を恐れて、 nil としたのだと思ひます。

CAR と FIRST、 CDR と REST

CAR と FIRST、CDR と REST もまた計算機にとつては同じものです。 讀み手を意識するなら當然使ひ分けられるべきです。 扱ふデータがリストであることを強調したいのなら、その操作子は FIRST, SECOND, … REST となるべきですし、 コンスセルを使つて二進木を表現する場合など、 は CAR, CADR, … CDR となるべきでせう。

よくあるのがリストの一番目の項目を (おそらくこちらのほうがタイプしやすいせゐでせう) CAR で取つて、二番目を SECOND 、三番目を THIRD 、殘りを CDR などと、支離滅裂な使ひかたをする人がゐます。これは愼むべきでせう。

=, /=, <, >, <=, >= などは二項演算子ではない

x = y = z を表現するのに、C 言語などでは、

x == y && y == z

あるいは

x == y && x == z

などと書きます。 Lisp は infix 記述を使はないために、もつとスマートな書き方ができます。

(= x y z)

と書けばすむ。 x < y < z も

(< x y z)

と書けます。これを

(and (< x y) (< y z)) 

などと書いてしまつたのでは、せつかくの prefix 記述が泣くといふものです。

マクロは兩刃の劍

マクロについては、樣々な本も出てゐますので、 ここでは極く手短に Common Lisp プログラマは、マクロとどう向きあふべきか、 について觸れておきます。

先づ、マクロと一口に言つても色々あります。 Spreadsheet のマクロは、一連の操作を簡單な名前やキー操作で置き換へて表すもので、 最も廣く世に浸透してゐる意味でのマクロと言つてよいかもしれません。 C のマクロは文字列の置き換へです。これも Spreadsheet のマクロと同樣、 一連の操作をわかりやすい名前で置き換へるものです。 どちらも使ひ勝手を自分なりに改善することはできても言語仕樣そのものを いぢることは許されません。

例へば、C で

 if (!s) {
   return -1;
 }

と書くべきところを、

 unless (s) {
   return -1;
 }

のやうに書きたいと思つても、處理系そのものに手を入れない限りそれはできない。

Common Lisp のマクロは上に似た「言語仕樣の擴張」を許します。 これは言語に新しい文法を導入することに相當します。 マクロが時に「syntax 抽象」と云はれる由縁です。

言語仕樣が擴張できる、といふことは、 非常に強力ではありますが、 強力であるが故に分別無く使ふと目もあてられないことになるといふことでもあります。

マクロに對するよくある誤解の一つは、それでもつて言語を「カスタマイズ」できる、 とする誤解です。慥かに Common Lisp マクロを使へば言語は「カスタマイズ」できます。 しかし言語とはそもそも「公」のものなのですから、 それぞれが得手勝手に言語をカスタマイズし始めたのでは、 コミュニケーションの基盤といふものが失はれます。 いかなる言語であれ、言語は「カスタマイズ」されるべきものではありません。したがつて、 マクロは言語を「カスタマイズ」するために使はれるべきものではありません。

では、マクロは何をするために使はれるべきものであるか。 それは、ドメインスペシフィック言語を設計するために使はれるべきものだ、 といふのが私の理解です。 で、その最も好い例が CLOS MOP だと思つてゐます。

マクロとその應用について學びたいのであれば、何より先づ AMOP を讀みなさい、 といふのが私の意見なのですが、 AMOP の話は何れまたどこかでしたいと思ひます。

名前空間を有效に使ふ

Consider the names people would have if they had to have globally unique names. That is what a single namespace does to programmers.

Common Lisp には名前空間と呼ばれるものがいくつかあつて、 函數、變數、型、などはそれぞれ獨立の名前空間を持ちます。 一例をあげると list といふ函數名と list といふ變數名は衝突しないことになつてゐます。

要するに、大抵の名前は衝突を氣にせずに、のびのび書けるのですから、 のびのび書きませう、 list と書きたいところを、態々 lst としたり、 おかしな underscore を名前の最初にいくつも竝べたりする必要はないといふ事です。

名前空間とは函數や變數の名前をしまつておく箱のやうなものをイメージしてもらへれば良いのですが、 Common Lisp はこの名前空間に函數は函數用、 變數は變數用と、名前が指す對象の種類によつて別々に用意されてゐます。

実際には七つある。

  • variable
  • function/macro
  • class/type
  • block tag
  • go tag
  • restart
  • catch tag

下は、ごく naive に書いた qsort プログラムですが、 ここで、list, first, rest, elt などは皆函數名としても意味を持つ名前です。 假にこれらが變數名として使へないとしても、適切な換りの名前を持つてくれば、 特にプログラムが讀み難くなつたりするものではありません。 しかし、これらが變數名として使へないことを プログラマーがいつも意識していないといけないとすると、 このことはプログラマーにとつて要らぬ負擔となります。 のみならず、littles や bigs など他の變數名についても一々、 函數名と衝突しないか注意しながらプログラムを書くなどは、 counter productive も甚しいと言はざるをえません。

(defun qsort (list)
  (flet ((partition (first rest)
           (loop for elt in rest
               if (<= elt first)
               collect elt into littles
               else
               collect elt into bigs
               finally (return (values littles first bigs)))))
    (if (null list)
        '()
        (multiple-value-bind (littles first bigs)
            (partition (first list) (rest list))
          (append (qsort littles)
                  (list first)
                  (qsort bigs))))))
→ QSORT

(qsort '(3 2 1 4 0 5 9 6 8 7))
→ (0 1 2 3 4 5 6 7 8 9)

一方、一つの名前が複數の對象を指すことのまぎらはしさに就いてはどうか。

人はこのことに柔軟に對處できてゐることが自然言語を考へてみればよくわかります。 同じ字面の單語が文脈によつて名詞になつたり動詞になつたりといふのは、 極めて普通のことで、例へば英語の場合だと love, help, like … といくらでも出てきます。 中には advice と advise のやうな例もありますが、ほんの少しです。 獨逸語は名詞の頭を capital にすることで、 文脈によらない名詞動詞の區別がされてるやうに見えますが、 これも萬能ではなく、Musikerleben が Musik Erleben なのか Musiker Leben なのかは、 文脈のみぞ知る、です。

以下は、block, variable, go, restart の各名前に使へる限り foo を用いた nonsense program です。 (かういふ使ひ方が御薦めできないのは言ふまでもありません。)

(block foo
  (let ((foo nil))
    (tagbody
      (restart-bind
          ((foo #'(lambda (&rest temp)
                    (setq foo temp)
                    (go foo))))
        (return-from foo
          (handler-bind ((error #'(lambda (foo)
                                    (declare (ignore foo))
                                    (invoke-restart 'foo 7))))
            (error "Foo."))))
     foo
      (return-from foo (apply #'(lambda (&optional foo) foo) foo)))))
→ 7

他の言語を(とりあへず)忘れる

これは、言ふは易し、行ふは難しで、なかなかできるものではありません。 ですが心に留めておくことは大事だと思ひ擧げてみました。

わたしの經驗した限り、Common Lisp を學ぶ上で、 機械語、アセンブラ、C など、Von Neumann 型アーキテクチャにべつたりの プログラミング言語を知つてゐることは大変爲になる。

一方、 抽象度の高い高級言語を知つてゐることは寧ろ害になることのほうが多いやうです。 これは、知識が學習の邪魔をするといふよりも、 自分には知識があるといふ意識が學習の邪魔をするやうです。 新しいものを學ばうとするときに、 何かのアナロジーをもつてそれを捉へようとすると、 往々にして物事の本質からずれたところで、そいつを理解、つまり誤解してしまふ、 といつたことがないでせうか。

「どんなプログラミング言語のアナロジーをもつてしても Common Lisp を 学ぶことはできない」わたしはこの命題が眞であると主張するつもりはありません。 しかし、この命題をとりあへず信じておくことは、Common Lisp を學ぶ上で 決つして損はないと言へます。

Author: KURODA Hisao

Date: 2013-04-23T14:33+0900

Generated by Org version 7.9.2 with Emacs version 24

© Mathematical Systems Inc. 2012