読者です 読者をやめる 読者になる 読者になる

レガシーコード生産ガイド

私に教えられることなら

SWI-Prologでメタプログラミング(述語の作成)

Prolog

Prologでのメタプログラミングのやり方が少しわかったのでメモしておきます。

Prolog-KABA入門という本の、リストを使ったデータベース例を元に進めていきます。

version1

リストを使った、順不同・長さ可変のデータベースについて考えます。それぞれのデータは次のようなものです。

specs(specs(taro, [height(170), age(31)]).
specs(hanako, [age(unknown), height(281), power(255)]).

各項目、例えばtaroのageは、memberとファンクタを使ってアクセスできます。

?- specs(taro, S), member(age(X), S).
S = [height(170), age(31)],
X = 31.

毎回このようなコードを打つのは面倒なので、アクセサ用の述語を定義してみます。

height(Name, X) :-  specs(Name, S), member(height(X), S).
age(Name, X) :- specs(Name, S), member(age(X), S).

これを使えば簡単にアクセスできます。

?- height(taro, Height).
Height = 170 .

?- height(Name, Height), Height > 180.
Name = hanako,
Height = 281 .

しかし、項目が増えてくるとアクセサの定義が面倒です。データ構造を変えたら大量の同じ箇所を変更する必要があります。そこで、Prologプログラムで自動的にアクセサを定義する方法を考えてみます。

version2

アクセサの定義を縦に揃えて眺めてみます。

height(Name, X) :-  specs(Name, S), member(height(X), S).
   age(Name, X) :-  specs(Name, S), member(   age(X), S).

結局、heightageしか違いはありません。

まずは、body部を一つの述語にしてみます。次のように定義できるよう、specという述語を考えてみます。

height(Name, X) :- spec(Name, height, X).
age(Name,X)     :- spec(Name, age, X).

アクセサのmemberの第一引数は、height(X)age(X)のように複合項です。SWI-Prologでは、(=..)/2という述語(オペレータ)によって引数付きの複合項を生成することができます。

オンラインマニュアル/2)を見ると、変数もそのまま引数にできるようです。

?- foo(hello, X) =.. List.
List = [foo, hello, X]

?- Term =.. [baz, foo(1)].
Term = baz(foo(1))

例えばheight(X)が欲しい場合は、F =.. [height, X]でFから求めることができます。最終的に目当ての述語specの定義は次のようになります。

spec(Name, Spec, X) :-
    specs(Name, S),
    F =.. [Spec, X],
  member(F, S).

これで、上記のようにheight(Name, X) :- spec(Name, height, X).という定義ができます。

version3

version2でheight(Name, X) :- spec(Name, height, X).という定義ができるようになりました。次はこの定義自体も自動化します。また、データベースにどういう項目があるか一覧できると嬉しいので、spec(height)のような事実も自動で定義します。

まず左辺のheight(Name, X)は、上記の(=..)/2を使ってAccessor = [height, Name, X]と定義できます。heightの部分を与えられれば自動化できます。

あとはAccesor :- specs(Name, S), member(Functor, S)の形でassertすれば良さそうです。ただ注意点ですが、assertで:-の右辺は括弧で囲む必要があります。

最終的に定義は以下になりました。

reg_accessor(Spec) :-
    Functor  =.. [Spec, X],
    Accessor =.. [Spec, Name, X],
    % 述語の作成
    assertz(Accessor :- (specs(Name, S), member(Functor, S))),
    % スペック一覧に登録
    assertz(spec(Spec)).

変数が単一化されるタイミングがややこしく感じますが、これで動きます。ソースコード中でreg_accessor/1を使うことでアクセサを定義できます。

:- reg_accessor(height).
:- reg_accessor(age).

読み込んだあとは、例えばfindallでデータベースの項目一覧を取得することができます。

?- findall(X, spec(X), LIST).
LIST = [height, age].

version4

細かいことですが、最後はreg_accessorの記述も減らします。アクセサ/項目名のリストに適用します。

:- maplist(reg_accessor, [height, age, power]).

version4時点でのコード全体です。

:- dynamic specs/2. % データベースレコードを動的に追加するため

%% サンプルデータ
specs(taro, [height(170), age(31)]).
specs(hanako, [age(unknown), height(281), power(255)]).

reg_accessor(Spec) :-
    Functor  =.. [Spec, X],
    Accessor =.. [Spec, Name, X],
    % 述語の作成
    assertz(Accessor :- (specs(Name, S), member(Functor, S))),
    % スペック一覧に登録
    assertz(spec(Spec)).

%% :- reg_accessor(height).
%% :- reg_accessor(age).
%% :- findall(X, spec(X), LIST).

:- maplist(reg_accessor, [height, age, power]).

メタプログラミングについて

メタプログラミングの方法として、ソースコードをそのまま操作するUnix的な(?)もの、Lispみたいにコードをデータとして扱えるものがあります。もう一つ、Smalltalkで経験して理解したのがコードではなくプログラムをデータとして扱うものでした。完全に動的な環境内で開発できる場合は、前者2つより強力に感じることもあります。特にコード生成だけでなく、プログラムについて調査したり、クラスなどの構成を利用する際に非常に便利です。

Prologについては、2番目のコードをデータとして扱うものかと思っていましたが、今回やったことはどちらかというと3番目だと思います。述語のデータベースを操作している感覚と、クラスオブジェクトやメタクラスオブジェクトを操作している感覚がなんとなく似ていると感じました。

Prologでも、イメージベースの動的な環境があったりするのでしょうか。あったら面白そうです。

広告を非表示にする