基于对象的编程在SWI-Prolog中

15

我在某处读到,你可以将Prolog中的模块视为对象。我正在试图理解它,并确定它是否是一种好的编程方式。

如果我有两个文件,一个定义了一个类别dog,然后另一个文件使用这个类别来创建两个dog对象。

:- module(dog,
      [ create_dog/4,bark/1 ]).

create_dog(Name,Age,Type,Dog):-
   Dog = dog(name(Name),age(Age),type(Type)).

bark(Dog):-
   Dog = dog(name(_Name),age(_Age),type(Type)),
   Type = bassethound,
   woof.
bark(Dog):-
   Dog = dog(name(_Name),age(_Age),type(Type)),
   Type \= bassethound,
   ruff.

woof:-format("woof~n").

ruff:-format("ruff~n").

第二个文件

use_module(library(dog)).

run:-
   dog:create_dog('fred',5,bassethound,Dog),
   forall(between(1,5,_X),
       dog:bark(Dog)
      ),
   dog:create_dog('fido',6,bloodhound,Dog2),
   dog:bark(Dog2).

这段代码创建了一个名为Dog的猎犬对象,让它吠叫5次。接着,又创建了另一个名为Dog2的大猎犬对象并使其也吠叫。我知道在面向对象编程中,对象拥有状态和行为。因此,现在我有了两个根据自身状态表现不同行为的对象,但目前我将对象的状态存储在Dog变量中,以便主程序中的代码可以看到它们。是否有一种方法可以隐藏对象的状态,即有私有变量吗?

例如,我可能想要为每个狗对象存储状态has_barked,如果它之前已经吠过了,则为true,否则为false,然后根据此更改bark /1的行为。

此外,您如何处理继承和覆盖方法等问题?欢迎提供任何相关阅读材料。谢谢。


如果这不是你想要的答案,那我会尝试写一个更长的答案,但是...我认为你正在错误地使用Prolog。它是一个逻辑引擎,而不是面向对象编程语言。这个问题在命令式/面向对象编程语言如Python或Java中可以更简单地解决。 - Tom
1
我只是在做实验。这只是一个微不足道的例子,我有很多Prolog代码,我想知道使用组合的面向对象和逻辑风格是否是一种好的做事方式。很高兴听到您的想法,我知道如何在Java和Python中编程,因此熟悉标准的面向对象编程,但不熟悉Prolog中的面向对象编程。 - user27815
我认为需要注意的重要一点是面向对象编程(OOP)是一种范式,逻辑编程也是如此。在Prolog中,我认为它们并不特别搭配。如果你_需要_结合这两个学科,可以考虑使用这样一个包:https://code.google.com/p/pyswip/ - Tom
1
谢谢,但我不想使用Python等语言,我想坚持使用SWI进行学习。虽然它们是两种不同的编程范式,但将它们结合起来并不罕见。例如SICStus https://sicstus.sics.se/sicstus/docs/3.7.1/html/sicstus_35.html - user27815
这确实很不寻常,但正如你所说,也不是闻所未闻的。我的建议是,虽然有可能,但这并不是 Prolog 的用途。然而,如果您想继续,有人刚刚发布了一个可能有用的答案 :) - Tom
我的感觉是最好将两种范式结合起来,通过查询逻辑编程引擎并从中生成对象...但对于面向对象的语言,例如Java。再进一步,但在我看来,答案集编程与DLV有一个很好的集成,您可以在此处查看代码片段:http://www.dlvsystem.com/jdlv/ - Dr. Thomas C. King
7个回答

9

这只是您的示例代码在Logtalk中可能的重新实现之一。它使用原型以简化代码,但仍说明了一些关键概念,包括继承,默认谓词定义,静态和动态对象以及参数化对象。

% a generic dog
:- object(dog).

    :- public([
        create_dog/3, bark/0, name/1, age/1
    ]).

    create_dog(Name, Age, Dog) :-
        self(Type),
        create_object(Dog, [extends(Type)], [], [name(Name),age(Age)]).

    % default definition for all dogs
    bark :-
        write(ruff), nl.

:- end_object.


:- object(bassethound,
    extends(dog)).

    % bark different
    bark :-
        write(woof), nl.

:- end_object.


:- object(bloodhound,
    extends(dog)).

:- end_object.


% support representing dogs as plain database facts using a parametric object
:- object(dog(_Name,_Age,_Type),
    extends(dog)).

    name(Name) :-
        parameter(1, Name).

    age(Age) :-
        parameter(2, Age).

    bark :-
        parameter(3, Type),
        [Type::bark].

:- end_object.


% a couple of (static) dogs as parametric object proxies
dog(fred, 5, bassethound).
dog(fido, 6, bloodhound).


% another static object
:- object(frisbee,
    extends(bloodhound)).

    name(frisbee).
    age(1).

:- end_object.

一些示例查询:

$ swilgt
...
?- {dogs}.
% [ /Users/foo/dogs.lgt loaded ]
% (0 warnings)
true.

?- bassethound::bark.
woof
true.

?- bloodhound::bark.
ruff
true.

?- bassethound::create_dog(boss, 2, Dog).
Dog = o1.

?- o1::bark.
woof
true.

?- {dog(Name, Age, Type)}::bark.
woof
Name = fred,
Age = 5,
Type = bassethound ;
ruff
Name = fido,
Age = 6,
Type = bloodhound.

?- dog(ghost, 78, bloodhound)::(bark, age(Age)).
ruff
Age = 78.

?- forall(between(1,5,_X), {dog(fred,_,_)}::bark).
woof
woof
woof
woof
woof
true.

一些笔记。 ::/2 是发送消息的控制结构。目标 {Object}::Message 简单地使用普通的Prolog数据库证明了 Object,然后将消息 Message 发送到结果。目标 [Object::Message] 委托 一个对象处理消息,同时保留原始发送者。


不,我的意思是希望能够在普通的 .pl 文件中“嵌入” Logtalk 结构并进行扩展。至少在没有完整项目编译的情况下尽可能地进行扩展。 - Erik Kaplun
1
@ErikKaplun 感谢您的澄清。Logtalk源文件可以包含普通Prolog代码和Logtalk代码。请注意,唯一可移植的术语扩展机制是Logtalk中的机制。没有Prolog标准规定它。因此,这些文件(可以使用.pl扩展名)仍需要使用Logtalk的“logtalk_load/1-2”谓词进行编译和加载。 - Paulo Moura
那么我不能先从Logtalk中将后端特定的术语扩展加载到Prolog中,然后直接编写Logtalk结构吗?或者这将需要避免在加载代码中使用任何已经激活的基本术语扩展,以避免两个竞争的术语扩展机制同时处于活动状态? - Erik Kaplun
@ErikKaplun Logtalk编译器不基于术语扩展。有关详细信息,请参见https://logtalk.org/manuals/userman/programming.html#multi-pass-compiler。在源文件编译期间可能会发生术语和目标扩展,但这只是编译器支持的*钩子*机制。 - Paulo Moura
@ErikKaplun 你总是需要使用Logtalk编译器编译Logtalk源文件。但是你也可以从同一个Logtalk加载器文件中加载Prolog文件,并使用Logtalk的加载谓词。 - Paulo Moura
显示剩余5条评论

7
Prolog模块可以轻易地被解释为对象(具体而言,是原型)。Prolog模块可以动态创建,具有可以视为其标识的名称(因为它在运行会话中必须是唯一的,所以模块命名空间是平面的),并且可以具有动态状态(使用局部于模块的动态谓词)。然而,在大多数系统中,它们提供了弱封装,因为你通常可以使用明确的限定调用任何模块谓词(也就是说,至少有一个系统ECLiPSe,允许你锁定模块以防止这种方式破坏封装性)。此外,没有支持将接口与实现分离或具有相同接口的多个实现的支持(你可以以某种方式进行黑客操作,具体取决于Prolog模块系统,但这并不美观)。
如其他答案所述,Logtalk是一个高度可移植的基于对象的Prolog扩展,支持大多数系统,包括SWI-Prolog。Logtalk对象综合了Prolog模块的概念和实际角度。Logtalk编译器支持一组共同的模块特性。你可以使用它在没有模块系统的Prolog实现中编写模块代码。Logtalk可以将模块编译为对象,并支持对象和模块之间的双向调用。
请注意,逻辑编程中的对象最好被看作是一种代码封装和代码重用机制。就像模块一样。面向对象(OO)概念可以(并已经)成功地应用于其他编程范例,包括函数式和逻辑。但这并不意味着必然会带上命令/过程式概念。例如,一个实例和它的类之间或原型和父级之间的关系可以解释为指定代码重用模式而不是从动态/状态角度来看(实际上,在从命令/过程式语言派生的面向对象编程(OOP)语言中,实例只是一个更加完美的动态数据结构,其规范分布在其类和其类的超类之间)。
考虑到你的示例代码,你可以轻松地在Logtalk中重新编码,接近你的表述方式,也可以采用其他方式,其中最有趣的是不使用动态特性。存储状态(如动态状态)有时是必要的,甚至可能是特定问题的最佳解决方案(Prolog有动态谓词的原因!),但应谨慎使用,仅在真正需要时使用。使用Logtalk不会改变(也不打算改变)这一点。
我建议您查阅广泛的Logtalk文档和其众多编程示例。在那里,你将找到如何优雅地分离接口和实现、如何使用组合、继承、特化或覆盖继承谓词等信息。

2
几年前,我曾尝试将“动态模块”实现弯曲成实际使用的东西,遵循文档的指导,但是我有一个不好的经历,所以我不愿意建议OP尝试这种可能性。 - CapelliC

3

Logtalk是目前最流行的面向对象Prolog语言。Paulo把它作为一个发布,安装非常容易。

模块不太适合面向对象编程。它们更类似于命名空间,但没有嵌套。此外,ISO标准有点争议。

SWI-Prolog v7引入了dicts,这是一种扩展,至少解决了语言的一个历史性问题,并通过名称提供“字段”和“方法”的语法。但仍然没有继承...

编辑

我在这里添加了一个小例子,介绍了如何在SWI-Prolog中使用面向对象编程。它是我关于创建家谱树的测试应用程序的进化版本。

比较genealogy.pl源代码,您可以欣赏到最新版本如何使用模块说明符,而不是指令:- multifile,然后可以处理多个树。

您可以看到,调用模块通过图形构建代码传递下来,并具有可选或强制性谓词,这些谓词通过模块资格进行调用:

make_rank(M, RPs, Rp-P) :-
    findall(G, M:parent_child(P, G), Gs),
    maplist(generated(M, Rp, RPs), Gs).

可选谓词必须像这样被调用:

...
catch(M:female(P),_,fail) -> C = red
...

请注意,谓词不被应用程序模块导出。据我所知,导出它们会破坏面向对象编程。
另一个更为琐碎的面向对象示例可能是模块pqGraphviz_emu,其中我编写了一个简单的系统级对象替代品。
我解释一下:pqGraphviz是一个在Qt上编写的微小层,位于Graphviz库之上。Graphviz虽然使用C语言编写,但具有面向对象的接口。实际上,该API允许创建相关对象(图形、节点、链接)并对其进行属性分配。我的代码尝试保持API与原始API最相似。例如,Graphviz使用以下代码创建一个节点:
Agnode_t* agnode(Agraph_t*,char*,int);

然后我使用C++接口编写了代码

PREDICATE(agnode, 4) {
    if (Agnode_t* N = agnode(graph(PL_A1), CP(PL_A2), PL_A3))
        return PL_A4 = N;
    return false;
}

我们交换指针,我已经设置了Qt元类型功能来处理类型...但由于接口相当低级,所以通常我有一个小的中间层,它公开更适用的视图,正是这个中间层接口被genealogy.pl调用:
make_node(G, Id, Np) :-
    make_node(G, Id, [], Np).
make_node(G, Id, As, Np) :-
    empty(node, N0),
    term_to_atom(Id, IdW),
    N = N0.put(id, IdW),
    alloc_new(N, Np),
    set_attrs(Np, As),
    dladd(G, nodes, Np).

在这个片段中,您可以看到 SWI-Prolog v7 字典的一个示例:
...
N = N0.put(id, IdW),
...

内存分配方案由allocator.pl处理。


对于初学者来说,使用提供的安装程序(它们适用于大多数操作系统)安装Logtalk可以提供更好的用户体验。该软件包使安装变得非常容易,但也有点隐藏了文档、示例、工具等所在的安装目录(请参见软件包描述)。 - Paulo Moura

2

1
谢谢,但我主要有兴趣使用SWI Prolog。 - user27815
1
由于这只是建议一个链接,所以应该是一个注释,而不是一个答案。 - lurker
2
@user27815 Logtalk使用/需要Prolog系统作为后端编译器。SWI-Prolog是其中一个完全支持的系统。 - Paulo Moura

2
SWI-Prolog中的PCE系统也是Prolog中面向对象编程的一种选择。它通常与GUI系统xpce相关联,但实际上它是一个通用的基于类的面向对象系统。

0

现在,SWI Prolog具有与模块良好交互的字典功能。请参阅SWI Prolog手册页面上的dicts,特别是5.4.1.1节:用户定义的dicts函数。

这使您能够定义看起来完全像方法的东西,直到返回值(在Prolog中不寻常但非常有用)。

与其他答案中讨论的不同,我个人认为逻辑编程和OOP范例彼此正交:使用OOP模块化结构化逻辑代码绝对没有坏处...


0

有一个叫做context.pl的东西,作为另一个不相关的项目的一部分实现。与Logtalk不同,它不需要编译,但它肯定只有Logtalk功能的一小部分:

Context是Prolog的面向对象编程范例。它实现了上下文(命名空间)、类和实例。它支持各种继承机制。成员谓词的访问通过public/protected和private元谓词进行调节。我们使类数据成员具有声明性静态类型。

/.../

CONTEXT实现了一种声明性上下文逻辑编程范例,旨在促进Prolog软件工程。简短描述如下:

  1. 我们将全局Prolog命名空间拆分为上下文,每个上下文都有自己的事实和规则。
  2. 我们创建了一种元语言,允许您在上下文中声明有关事实和规则的元数据。
  3. 我们实现了类和实例、公共、保护和私有元谓词。我们实现了(多重)继承和克隆。我们实现了操作符,使其能够与上下文交互。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接