Refactoring in common lisp.

Introdunction

Common LispはASTを人間が書く言語です。

そしてASTは本質的に醜くなるものです。 もしそうでないなら今頃あらゆるプログラミング言語がLisp系言語になっていたことでしょう。

では、そのような本質的に醜くなりうるASTを直接書き下す言語(すなわちLisp)としては、どのようにASTをハンドルすれば(すなわちコードを書けば)醜さに陥っていくのを防げるでしょうか。

本稿では著者が知り得ている典型的なリファクタリングパターンを初心者向けに紹介していきます。

Local function.

インデントが深くなりコードが醜くなる一因として、関数に切り出すべき処理を直接書き下してしまっている場合があります。 そのような場合、ローカル/グローバル問わず、独立した関数に書き出すことでコードを美しくできます。

関数に切り出すという行為には、関数呼び出しそのものに実行コストがかかる、という反論が想定されますが、それは実際に呼び出しがボトルネックになるまでは気にするべきではありません。

また、コメントとともに処理を書いている場合、そのコメント内容を関数名にして切り出したほうが良い場合があります。 もしコメントの内容が「何をしているのか」についてのものであるなら関数への切り出しを検討しましょう。 コメントの内容が「なぜしているのか」の場合はこの限りではありません。

また、スコープを区切って変数を定義しようとした結果ネストが深くなるという場合も関数に切り出したほうが良いかもしれません。

では具体例として、ASDFのメンテナでもあらせられるfareさんのコードを見てみましょう。 これはCSVをパースするライブラリのコードですが、ローカル関数が大量に定義され本体ではそのうちのエントリポイントとなる関数を呼び出しているだけです。 大変美しく、処理も追いやすいものです。 大いに参考にするべきです。

Conditional diverging.

条件分岐もコードが醜くなる一因です。

著者の印象では条件分岐は主に三種類に大別できます。 なお、以下の条件分岐名三種は、筆者がかってに名付けたものです。 正式名称をご存知の方はご一報を!

Sequential condition.

もっとも一般的で分かりやすいのは線形条件分岐です。 グラフで表すと以下のようになります。

Graph of sequential condition.

Balanced binary tree condition.

もっとも難解なのは平衡二分木条件分岐です。 グラフで表すと以下のようになります。

Graph of ballanced bynary condition.

Unballanced binary tree condition.

頻出するパターンとして非平衡二分木条件分岐があります。 名前は苦肉の策でつけたものでもっと良い名前があれば良いのですが。。。

グラフで表すと以下のようになります。

Graph of unballanced bynary condition.

これは主に再帰関数で見かけるものです。 一番上のpred1が再帰終了条件で、再帰中は左の枝を辿りますが、終了する場合は右の枝に行きます。 pred3が行っているのは返り値を処理するための分岐です。

IF rather than COND.

もっとも一般的で分かりやすいのが線形条件分岐なのは上に見たとおりです。 通常これを書くのにはCONDが使われます。 先のグラフをCONDを使って書くと以下のようになります。

(cond
  (pred1 then1)
  (pred2 then2)
  (pred3 then3)
  (t finally))

陥りがちな罠として、とりあえず線形条件分岐で書こうとするものです。 中でも本来平衡二分木条件分岐であるものを線形条件分岐として書こうとするのは悪手です。

先の平衡二分木条件分岐をLispコードにするならIFを使って以下のようになるでしょう。

(if pred1
    (if pred2
        then2
        else2)
    (if pred3
        then3
        else3))

上のコードをCONDを使って無理に線形条件分岐に変更すると以下のコードとなります。

(cond
  ((and pred1 pred2) then2)
  ((and pred1 (not pred2)) else2)
  ((and (not pred1) pred2) then3)
  ((and (not pred1) (not pred2)) else3))

IFを使った前者のコードは条件が網羅的である(すべてのIFthen部とelse部が埋まっている)のが簡単に見て取れます。 CONDを使った後者のコードで条件が網羅的であるのを読み取るのは困難です。

条件分岐は通常IFを使って書き始めるべきです。 なお、「通常」というからには例外があって、それはTYPECASEを使う場合などです。

Tips.

IFを書くときのコツはthen部にIFをネストさせないことです。 悪い例を以下に挙げます。

(if pred1
    (if pred2
        (do-a)
        (if pred3
            (do-b)
            (do-c)))
    (do-d))

then部にあるIFNOTを使えば簡単にelse部と入れ替えられます。

(if (not pred1)
    (do-d)
    (if pred2
        (do-a)
        (if pred3
            (do-b)
            (do-c))))

上記のようにelse部にIFが続くコードは線形条件分岐であり、これはCONDに変換したほうが読みやすいかもしれません。

(cond
  ((not pred1) (do-d))
  (pred2 (do-a))
  (pred3 (do-b))
  (t (do-c)))

上の条件分岐三例はどれも同じコードですが、読みやすさは各々異なります。 特に第一の例から条件分岐が線形になっていることを読み解くのは困難です。

IFがネストしてインデントが深くなってしまっているコードは第一の例のようなコードを書いてしまっている場合が考えられます。