Previous: , Up: オペレーション   [Contents][Index]


7.1.2 新しいオペレーションを定義する

ASDFは、オブジェクト指向の手法で拡張可能なようにデザインされています。プログラマはoperationのサブクラスを作って必要な挙動を実装することで、ASDFに新しい機能を追加することができます。

ASDFの組み込みのオペレーションは、ユーザー定義のオペレーションに対して「特権的」な地位にあるわけではありませんが、開発者の方には、独自のオペレーションを追加するときに決してasdfパッケージを使わないようにお願いします。というのも、名前の衝突は避けるべきですし、かといって我々は「ASDFのオペレーション名を管理するグローバルなレジストリ」をいちいち作りたくもないからです。

[訳者補足]

また、ASDFにはオペレーション・アクション間の依存関係を指定するためのサブクラスとして、downward-operationupward-operationsideway-operationselfward-operationnon-propagating-operationの5つが備わっており、すべてのオペレーションはこの内の少なくとも1つを継承するべきです。オペレーションの依存関係を指定する必要がないときも、non-propagating-operationを継承するようにしてください。operationのみを継承すると、そのオペレーションを呼んだ時に警告が通知されるでしょう。 例として、組み込みのload-opを簡略化したmy-load-opを定義してみましょう:

(defclass my-load-op (downward-operation selfward-operation)
  ((selfward-operation :initform '(prepare-op compile-op) :allocation :class)
   ;; (downward-operation :initform nil :allocation :class)
   ))

non-propagating-operationを除く4つのクラスはすべて、同名のスロットをクラス変数として備えています。selfward-operationは「同一コンポーネントに対する別のオペレーション」に依存するオペレーションです。上の例では、コンポーネントfooに対してmy-load-opが呼ばれた時、先だってfooprepare-opcompile-opが適用されます。次に、downward-operationは「子コンポーネントに対するオペレーション」に依存するオペレーションです。上の例ではdownward-operationスロットを省略していますが、デフォルトのnilは同じオペレーションmy-load-opを意味します。つまり、コンポーネントfooに対してmy-load-opが呼ばれた時、先だってfooのすべての子コンポーネントに対してmy-load-opが適用されます。

オペレーション: downward-operation (downward-operation :type operation-designator :allocation :class)
オペレーション: upward-operation (upward-operation :type operation-designator :allocation :class)
オペレーション: sideway-operation (sideway-operation :type operation-designator :allocation :class)
オペレーション: selfward-operation (selfward-operation :type (or operation-designator list) :allocation :class)
オペレーション: non-propagating-operation

asdf::operation-designator型は、オペレーションオブジェクト・シンボル・クラスのいずれかです。多くの場合、シンボルが使われるでしょう。その際、nilは「同じオペレーション」を意味します。

downward-operationは子コンポーネントについての依存関係を持つオペレーションです。baz-opfoo-opdownward-operationスロットに指定されているとき、ASDFはコンポーネントparentに対するアクション(foo-op . parent)を実行する前に、まずはparentのすべての子コンポーネントchildについてアクション(baz-op . child)を実行します。

upward-operationは親コンポーネントについての依存関係を持つオペレーションです。baz-opfoo-opupward-operationスロットに指定されているとき、ASDFはコンポーネントchildに対するアクション(foo-op . child)を実行する前に、まずはchildの親コンポーネントであるparentについてアクション(baz-op . parent)を実行します。

sideway-operationは依存コンポーネントについての依存関係を持つオペレーションです。baz-opfoo-opsideway-operationスロットに指定されているとき、ASDFはコンポーネントcに対するアクション(foo-op . c)を実行する前に、まずはcが依存している(:depends-on)すべてのコンポーネントdについてアクション(baz-op . d)を実行します。

selfward-operationは同一コンポーネントについての依存関係を持つオペレーションです。baz-opfoo-opselfward-operationスロットに指定されているとき、ASDFはコンポーネントcに対するアクション(foo-op . c)を実行する前に、まずはアクション(baz-op . c)を実行します。selfward-operationスロットには、複数のオペレーションをリストとして指定することができます。

non-propagating-operationは依存関係を持たない、単独で実行されるオペレーションです。

上で例示したように、これらのオペレーションは多重継承することができます。(もっとも、non-propagating-operationは単一継承するべきでしょう。)また、依存関係が循環しないようにするのはプログラマの責任に委ねられています。

[訳者補足終わり]

新しく作られたオペレーションには、多くの場合、次の総称関数の内の少なくとも1つについてメソッドを定義しなければならないでしょう。

総称関数: perform operation component

performは何らかの入力ファイルから出力ファイルを作り出すための総称関数です。prepare-opのように依存関係の伝搬だけするようなオペレーションは別にしても、たいていのオペレーションについてはperformメソッドが最も重要でしょう。performが呼ばれると、(すべての依存関係についてperformが実行されたあと)ターゲットのコンポーネントに対して処理が実行されます。

performメソッドは、その入力と出力のパスを決めるのにinput-filesoutput-files(下述)を呼ばなければなりません。ユーザーが、このメソッドを上書きしたり、アウトプットトランスレーションを変更したりできるようにするためです。output-filesは多値を返しますが、performは最初の値――出力パス名のリスト――のみを使うべきです。出力ファイルがただ1つと決まっている場合は、代わりに関数output-fileが使えます。output-fileは出力パス名が1つだけであることを保証し、そのパス名を返します。

総称関数: output-files operation component

performメソッドが何らかの出力ファイルを伴うならば、その出力先をASDFが定められるようにこのメソッドも定義しなくてはなりません。

output-filesは2つの値を返します。1つ目はパス名のリストであり、2つ目はブーリアンです。ブーリアンがnilの場合(返り値が1つしかなく、2つ目が暗黙にnilとなる場合も含む)、:aroundメソッドがアウトプットトランスレーションに従ってパス名を変換します。例えば、コンパイルされたオブジェクトファイルが処理系依存のキャッシュとして保存される場合などは、アウトプットトランスレーションが使われています。ブーリアンがtの場合は:aroundメソッドで変換されることはありません。

総称関数: component-depends-on operation component

この総称関数は、アクションを引数に取り、依存するアクションをまとめたリストを返します。リストのフォーマットについては下で説明します。

[訳者補足]

上述の5つのサブクラスを継承してオペレーション間の依存関係を指定すれば、component-depends-onメソッドは適切に構成されるので、通常は明示的にこのメソッドを定義する必要はありません。また、特定のコンポーネント、システムに対して依存関係を記述したい場合も、:in-order-toオプションを使えば事足りるでしょう。(→ アクションの依存関係) しかし、それらを超えるような複雑な依存関係を指定したい場合――例えば特定のコンポーネント型に対するオペレーションに限って依存関係を追加したい場合など――は、メソッド定義が必要になるかもしれません。

なお、名前の印象に反して、コンポーネント間の依存関係を返す関数ではないことに気を付けましょう。システムの:depends-onオプションに与えた依存コンポーネントを調べたい場合は、代わりにsystem-depends-onという総称関数があります。

[訳者補足終わり]

自分でこのメソッドを定義する場合は、メソッドで追加したい内容と(call-next-method)で返るリストを常にappendして返すことを強く推奨します。あなたがASDFの内部を熟知しているのでない限り、そうしないと「興味深い」エラーが起こる可能性が高いです。これは概念的には、メソッドコンビネーションのappendを手動でやっているのと同じことになります。(残念ながら、標準のメソッドコンビネーションを互換的に導入するのは、手遅れでした。)

それでは、返り値について説明します。component-depends-onが返すリストの各エントリもまたリストです。

各エントリの最初の要素(car)はオペレーション指示子、つまりオペレーションオブジェクトそのものかオペレーションクラスのシンボル(例えばload-opcompile-opprepare-opなど)であり、後者はmake-operationでインスタンス化されるでしょう。

各エントリの2番目以降(cdr)はコンポーネント指示子のリストです。コンポーネント指示子はコンポーネントオブジェクトそのものか、find-componentでコンポーネントオブジェクトに変換される識別子(identifier)です。find-componentは現在の親コンポーネントを第1引数に、識別子を第2引数にして呼び出されるでしょう。識別子は一般的には、文字列か、シンボル(coerce-nameで小文字の文字列になる)か、あるいは文字列・シンボルのリストです。特にnilは親コンポーネントそのものを指します。

新しく作られたオペレーションには、次の総称関数についてメソッドを定義することができます

総称関数: input-files operation component

input-filesは、performに対する入力ファイルのパス名のリストを返します。

デフォルトのinput-filesのメカニズムは賢くできているので、あなたが自らメソッドを定義する必要はないことのほうが多いでしょう。メソッド定義が必要なのは、最終的な入力ファイル(のまとまり)が複数通り考えられるケースのみです。ほとんどのオペレーションはselfward-operationを継承していて、selfward-operationはソースファイルを含むように入力ファイルを適切に設定します。

総称関数: operation-done-p operation component

この総称関数は、アクション(オペレーションとコンポーネントの対)がもう一度performされなければならない場合にnilを返します。デフォルトは常にtです。

operation-done-pにメソッドを定義する必要があるのは、ファイルシステム上のタイムスタンプが変わっていなくても前回のオペレーションの実行を無効にしてやりなおすべき状況がある、という場合に限ります。そしてその場合、このメソッドはnilを返すべきでしょう。

例えば、このメソッドはtest-opについては常にnilを返します。テストというのは決して完了する性質のオペレーションではないからです。もちろん、単にテストのリポートファイルから結果を読み出すだけのオペレーションとしてtest-report-opのようなものを定義し、システム定義の:in-order-toオプションでtest-opをこのオペレーションに委ねて、何度繰り返しても同じになるようにすることは可能です。その場合はこのメソッドがtを返すようにしても良いでしょう。

何らかのメッセージを表示するオペレーションは、Lispのコンパイラやローダと同様に、出力先を標準の*standard-output*ストリームにするべきです。