SKILL for the Skilled: Continued Introduction to SKILL++

この記事は、
Continued Introduction to SKILL++
超訳です。

By Jim Newton on January 25, 2011

前回の記事で、階層をたどって様々な処理を行う単純だがパワフルな関数(walkCvHier)を使って、SKILL++を紹介した。もう一度、そのコードを載せる。

(defun walkCvHier (cv consume)            ;(1.1)
  (foreach inst cv~>instances             ;(1.2)
    (walkCvHier inst~>master consume))    ;(1.3)
  (consume cv))                           ;(1.4)

重ねて言うが、この関数はあくまでデモンストレーションのためのものである。読者の皆さんであれば、ちょっとした改良で、もっと複雑な階層関係を扱うことのできるロバストな関数を作ることができるだろう。この記事が改良の手助けになれば幸いである。もし、提案や質問があれば、このブログのコメントに記載して欲しい。

次からのパラグラフでは、walkCvHierの有効な使い方について説明したい。この記事で使っているソースコードは、全てSKILL++のものであり、拡張子.ilsファイル内で記述している。

Local (private) function -- 局所(Private)関数

前回の記事では、グローバル関数_reportCvを使ったreportCvHierの実装の方法を説明してた。すぐに分かることだが、SKILL++を使用すると、大域領域の名前空間を汚さずに、関数を記述することができる。すなわち、このクライアント関数を世界に見えるような形で作らなくても、walkCvHierの関数として渡すだけで実現できる。

もし、IC6.1.5を使用しているならば、新しく利用可能になったfletマクロをしようすることで、さらに簡潔にプライベート(ローカル)関数を記述できる。

(defun reportCvHier (top_cv)                  ; (4.1)
  (flet ((reportCv (cv)                       ; (4.2)
          (println (list cv~>libName          ; (4.3)
                         cv~>cellName         ; (4.4)
                         cv~>viewName))))     ; (4.5)
    (walkCvHier top_cv reportCv)))            ; (4.6)

fletマクロは、1つもしくは複数のローカル関数を定義できるマクロである。このローカル関数は、fletマクロ本体内で名前を渡すことで利用できる。このサンプルコードでは、(4.6)行が名前で参照しているところである。このreportCv関数は、flet本体内でのに、参照したり呼んだりすることができる(つまりfletが囲んでいるカッコ内)。このローカル関数は、引数として一つcvを持っている。これはSKILL++なので、(グローバル、ローカルに関わらず)関数名と変数名を(4.6)行目のように、同じように使用できる。

グローバル関数にはないローカル関数の優位点として、スコープ内にあるレキシカル変数に安全にアクセスできる、と言う特徴がある。例えば、ローカル関数reportCvから、top_cvを参照することができる。reportCv関数は、reportCvHier関数の定義内で定義されているため、reportCvHier関数が束縛しているローカル変数(引数であるtop_cvも同様)にアクセスすることができる。後に示すコード(6.4), (7.4)にも同様の例を示している。

もし、IC6.1.5が使用できない場合、下記のコードを使うと、同じことができる。この例では、無名関数を作って、reportCvHierの引数として渡している。

(defun reportCvHier (cv)                              ;(5.1)
  (walkCvHier cv (lambda (cv)                         ;(5.2)
                   (println (list cv~>libName         ;(5.3)
                                  cv~>cellName        ;(5.4)
                                  cv~>viewName)))))   ;(5.5)

Functions that manipulate state -- 状態を扱う関数

次に示す例(6.1-6.5)は、welkCvHier関数を階層内のcellViewの数を数えるために使用した例で、(7.1-7.5)はハッシュを使って、階層内で同じセルビューが何度現れるかを数えた例である。これらのケースでは、walkCvHier関数に渡されるcosumer関数は、呼ばれた関数の中で状態(この例では、カウンタとハッシュテーブル)を変化させる必要がある。

このコードに対して、伝統的なSKILLの知識でどのように立ち向かえばよいだろうか? このcosumer関数を伝統的なSKILL関数で記述した場合、カウンタを維持するために、グローバル変数を使用する必要がある。しかしながら、この方法では次の二つの問題が生じてしまう。

  1. この関数はリエントラントではない。つまり、walkCvHier関数にconsumer関数としては渡すことができないかもしれない。
  2. もし、consumer関数がエラーに直面した場合、このグローバル変数は不完全な状態のままになってしまい、次にこの関数が呼ばれるときに、どのような状態になるか保証できない。

SKILL++ and state manipulation -- SKILL++と状態のハンドリング

SKILL++では、なぜこのような問題が存在していないのだろうか? それは、グローバル変数が必要ないからである。(6.2)行、(7.2)行にあるように、変数はローカルかつレキシカルのままにできる。

countCvHierは、階層をトラバースするごとに、変数occurencesをインクリメントするために、walkCvHierに渡される。walkCvHierは、ローカル変数occurrencesのことを何も知りません。それどころか、walkCvHierで同じ変数名が使用されていたとしても、countCvHierには何の影響もありません。

(defun countCvHier (cv)                 ; (6.1)
  (let ((occurrences 0))                ; (6.2)
    (walkCvHier cv (lambda (cv)         ; (6.3)
                     occurrences++))    ; (6.4)
    occurrences))                       ; (6.5)

occureHier関数では、ローカル変数occurencesに束縛されているハッシュテーブルを変更するために、walkCvHier関数に渡すという、同じテクニックを使っています。

(defun occureHier (cv)                                             ; (7.1)
  (let ((occurrences (makeTable 'occur 0)))                        ; (7.2)
    (walkCvHier cv (lambda (cv)                                    ; (7.3)
                     occurrences[cv] = (add1 occurrences[cv])))    ; (7.4)
    occurrences))                                                  ; (7.5)

もし、SKILL++ではなくSKILLを使用しているならば、このテクニックは、失敗してしまうことを強調しておく。この問題は、ダイナミックスコープとグローバル変数に終始している。ダイナミックスコープを持った変数は、興味深く、パワフルなものであるが、このようなシチュエーションでは使用することができない。このような微妙な違いに対して、簡単に説明すると、問題は以下のようになる。

  • walkCvHier関数の中ではなく、(6.2), (6.4), (6.5), (7.2), (7.4), (7.5)のように、外側で使用しないといけない。
  • walkCvHier関数に渡されるconsumer関数は、walkCvHier関数内では異なる関数としてコールされるため、同じ変数を使用することはできない。
  • walkCvHierのソースコードを見ないとわからない。しかし、ソースコードがない場合もある。

Purging the hierarchy -- 階層をパージする

階層化にある全セルビューがをパージしようしとした場合、(walkCvHier cv dbPurge)としようとするかもしれないが、これはいい考えではない。なぜか? まず、同じセルビューが何度も現れているかもしれない。階層を下っているときに、同じセルビューに対して、何度もパージしたくはないはずだ。本当に必要なのは、セルビューを一度だけパージして、さらに、子どものセルビューは常に親のセルビューよりも先にパージされるような関数である。これを実現するのが、下の関数だ。

(defun purgeCvHier (cv)                           ;(8.1)
  (let ((visited nil))                            ;(8.2)
    (walkCvHier cv (lambda (cv)                   ;(8.3)
                     (unless (memq cv visited)    ;(8.4)
                       (push cv visited))))       ;(8.5)
    (mapc dbPurge (reverse visited))))            ;(8.6)

これはどのように動作するのか? consumer関数も階層をトラバースするwalkCvHier関数も何もパージしない。consumer関数は訪れたセルビューのリストを作るだけである。walkCvHier関数は、親のセルビューにconsumer関数を適用する前に、子セルビューの階層を下っていくので、purgeCvHier関数では単にSKILLのpushマクロを使用して、親セルが子セルよりも先(左)に並ぶリストを作っている。この問題(親セルが子セルよりも先に並ぶと言う)は、reverse関数を呼ぶだけで解決できる。

walkCvHier関数をもっとロバストに実装するためには、次の2つのconsumer関数を使えば良い。一つは、子セルに階層を下る前に適用する関数で、もう一つは、階層を下った後に適用する関数である。もしくは、consumer関数の適用を階層を下る前か後かを指定するために、walkCvHier関数のオプショナル引数として用意するのもよいだろう。

More about flet -- flet関数についてもう少し

局所関数が定義できるfletには、重要な制約がある。それは、自分自身を再帰的にコールすることができないことと、一つのfletマクロ内で複数の局所関数を定義したときに、お互いをコールできないことである。これについて、最初は制約だと思うかもしれないが、後の記事で説明するが、実際には重要な機能なのである。もし興味があるならば、このブログ欄にコメントして欲しい。

もし、局所関数を再帰的に呼びたいのであれば、labelsを使って、他の関数をコールするような、幾つかの局所関数を書いてやれば良い。シンタックスはfletと同じであるが、スコープのルールは異なっている。

Common LispEmacs LispのようなLispの方言は、fletとlabelsの両方を提供している。

Summary

SKILL++の機能について、基礎と応用例を示した。局所関数の機能を使うと、モジュール化を維持したまま、よりコードに情報を含めることができる。さらに、SKILL++を使用した方が、問題を直接的に記述することができる。

参考資料

Jim Newton