在OO Prolog中通过多方法碰撞恒星物体?

时间:2016-01-23 14:42:28

标签: prolog logtalk

我想知道如何在Prolog中将统一和OO结合起来。我想在术语对象上实现多方法调度。

如果没有术语对象和简单术语,我会执行以下操作,并且可以从多参数索引中获益:

def attack(self, player):
    #defend = randint(0,10)

    damage = randint(0, self.strength)
    if damage == 0:
        print "You missed"
    elif damage < self.strength:
        print "Hit for", damage,"damage."
        player.health = player.health - damage
    elif damage == self.strength:
        print "Critical hit!"
        player.health = player.health - (damage + 2) 
    player.check_dead(player)

但上面只给出了一个确切的类型匹配。

如果我想要一个子类类型匹配,我该怎么办(可能还有更多的太空船子类,如excelsior,galaxy等......在情况2,3和4中也应该匹配)。

我还可以使用统一和索引吗?

再见

P.S。:这个例子来自这里没有Prolog解决方案:
https://en.wikipedia.org/wiki/Multiple_dispatch

3 个答案:

答案 0 :(得分:3)

你带着你的问题到处都是:术语对象,多方法调度等.Prolog没有术语对象或派遣,真的,但我认为这个问题的精神很有趣

在我们可以进行多方法和多次派遣之前,我们需要,然后派遣。我认为你担心的是你希望能够编写一个如下所示的程序:

frob(spaceship(X, Y...)) :- % do something with spaceships
frob(asteroid(X, Y...))  :- % do something with asteroids

然后你想能够说出frob(excelsior(X, Y, ...)),并以某种方式将它放在第一个条款中。这显然不会开箱即用,但这并不意味着你无法使其发挥作用。以下是我尝试的方法:

选择更简单的仿函数形状

不要试图让它与excelsior(...)一起使用,而是更改您的表示以便更容易进行内省。一种非常通用的方法可能如下所示:

object(Type, Properties...)

如果您不关心继承,这可能会有用,但您确实如此。那么,如果您为子类型信息创建一个插槽怎么办?然后你可以在你关心的情况下匹配,否则忽略它。你的结构看起来像这样:

type(SubtypeInfo, Properties...)

然后你可以这样写frob:

frob(spaceship(_, X, Y)) :- % stuff

如果您使用Excelsior调用它,它可能如下所示:

?- frob(spaceship(excelsior(SpecialProperties...), X, Y)).

换句话说,让你的术语在外面具有最一般的类型,并在内部包装中包含更多特定信息。

frob2(spaceship(excelsior(_, ...), X, Y)) :- % do something with excelsiors

使用Metainterpreter

编写自己的Prolog方言是可能的。如果您向数据库添加一些关于您的类型是哪些子类型的事实,您自己的元解释器可以拦截评估过程并使用父类型重试。

不幸的是,我对此并不擅长,以下的元解释器应该被视为一个错误的草图/概念验证,而不是一个可以遵循的模型。

:- op(500, xfx, is_kind_of).

excelsior is_kind_of spaceship.

frob(spaceship(X, Y)) :- !, write('I frobbed a spaceship'), nl.
frob(_) :- write('I frobbed something else'), nl.

execute(true).
execute((A,B)) :- execute(A), execute(B).
execute(A) :-
    predicate_property(A, built_in)
       -> call(A)
       ;  execute_goal(A).

execute_goal(Goal) :- clause(Goal, Body), call(Body).
execute_goal(Goal) :- supertype_goal(Goal, NewGoal), execute_goal(NewGoal).

supertype_goal(Goal, NewGoal) :-
    Goal =.. [Head, Term],
    Term =.. [Type|Args],
    Type is_kind_of Supertype,
    NewTerm =.. [Supertype|Args],
    NewGoal =.. [Head, NewTerm].

这里的想法是尝试按原样执行目标,然后重新执行重写其中一部分的目标。虽然supertype_goal不是很一般,但替换例程并不全面,但它可以说明意图:

?- execute(frob(excelsior(this,that))).
I frobbed something else
true ;
I frobbed a spaceship
true ;
I frobbed something else
true ;
false.

是的,所以,不是很好,但是比我更熟练的Prolog用户可能会清理它并使其工作。

讨论

Prolog中只有两个数据可以存在:它可以存在于调用堆栈中,也可以存在于数据库中。我展示的第一种方法实际上是第一种方法:找到重新包装的方法&#34; subtyping&#34;为了您的目的,它可以存在于调用堆栈中而不会干扰(某些)统一。如果您仔细构建术语(并仔细编写代码),您可能可以完成这项工作,而且调试也不会太糟糕。但这可能有点难以阅读。

第二种方法在数据库中使用单独的关系来重新确定不同的子类型之间的关系。&#34;完成后,您需要修改解释器以使用它。这说起来容易做起来有点棘手,但我并不认为这是世界上最糟糕的想法。虽然,在考虑它时,你想要做的那种统一必须由元解释器设计。

你会发现Logtalk在&#34;参数对象&#34;之间也有类似的二分法,其标识符基本上是完整的Prolog术语,而普通对象创建了一个完整的命名空间,它们就像封装一样一个单独的数据库对于非参数对象,对象的结构不会像对术语那样进行统一。

表现关注

假设我在某个方法中将两个对象作为参数。如果我使用第一种方法,我认为如果它可用,我可以从索引中获益,而且我没有深入研究这个术语 - 我认为通用编程应该更好。我不知道Prolog系统如何响应统一到一些结构;我认为他们做得很好,但我不知道论证索引。感觉就像是充满了感情。

第二种方法并不能很好地保持这种状态。如果我的层次结构可能是N级深,我可能会尝试N ^ 2种不同的组合。这听起来没有成效。显然,Paulo已经在Logtalk中找到了一些东西,它似乎没有这个性能问题。

双重调度转移

当我学习Smalltalk时,这对我来说是一个很大的启示,如果你已经知道,请原谅我。您可以使用&#34; double dispatch以单一调度语言获得多次调度的类型优势。&#34;基本上,您所有的对象都实现了collide_with,其他的&#34;其他&#34;对象作为参数,因此您拥有Asteroid::collide_with(Other)Ship::collide_with(Other)以及Bullet::collide_with(Other)。然后,这些方法中的每一个都调用其他collide_with_type,传入自我。您获得了许多方法(许多方法将委托给另一方),但您可以在运行时安全地重新创建所有缺少的类型信息。

我前段时间写了crappy Asteroids clone in Lua,你可以看到它是如何运作的:

-- double dispatch for post collision handling
function Asteroid:collideWith(other)
   return other:collideWithAsteroid(self)
end

function Asteroid:collideWithShot(s) 
   -- remove this asteroid from the map
   if index[self] then
      table.remove(asteroids, index[self])
      index[self] = nil
      s:remove()
   end
end

function Asteroid:collideWithPlayer(p) 
   p:collideWithAsteroid(self)
end

function Asteroid:collideWithAsteroid(ast) end

所以你可以在那里看到一些东西:Asteroid:collideWithShot从游戏中移除小行星,但它将Asteroid:collideWithPlayer(p)委托给Player:collideWithAsteroid(a),并且两个小行星相撞不起作用任何东西。

Logtalk中可能出现的基本草图如下:

:- protocol(physical).

  :- public(collides_with/1).

:- end_protocol.

:- object(asteroid, implements(physical)).

  collides_with(Other) :- self(Self), Other::collides_with_asteroid(Self).

  collides_with_asteroid(AnotherAsteroid).
  collides_with_ship(Ship) :- % do stuff with a ship

:- end_object.

忍受我,我很少使用Logtalk!

更新:痛心地说,扬金库(Jekejeke序言的作者)曾指出,晋级运营商将肆虐双调度。这并不是必然意味着带有子类型的多个调度与统一不兼容,但它 意味着作为变通方法的双重调度与剪切不兼容,这将是使非确定性复杂化并可能破坏这种方法。有关更多讨论,请参阅以下评论。

结论

我不认为子类型和统一是互斥的,因为Logtalk同时拥有它们。我不认为使用参数索引进行子类型和多次调度也是相互排斥的,但Logtalk没有多次调度,所以我无法确定。在大多数情况下,即使在Java中我也避免使用子类型,所以我可能有偏见。多次发送虽然是一种100美元的语言功能;我不能说很多语言都有它,但你可以通过双重调度非常有效地伪造它。

如果你对这个东西感兴趣的话,我会大量调查Logtalk。 The parametric example尤其引人注目。

我有点怀疑这真的回答了你的问题,甚至落在了同一个球场,但我希望它有所帮助!

答案 1 :(得分:2)

在CLOS中,用于多个分派的泛型函数不是封装在类中,而是按函数名称分组。因此,这里的等价物将是普通的Prolog规则。此外,假设多个参数索引,规则头中的参数必须充分实例化为我们想要执行多个分派的“类型”,以便每次都选择正确的规则而没有虚假的选择点。如OP所示:

collide_with(asteroid(_), asteroid(_)) :-
    ...
collide_with(asteroid(_), spaceship(_)) :-
    ...
collide_with(spaceship(_), asteroid(_)) :-
    ...
collide_with(spaceship(_), spaceship(_)) :-
    ...

考虑到Prolog中的统一是如何运作的,如果我们想要对基本小行星和宇宙飞船“类型”进行专业化,并遵循Daniel的建议,我们可以使用复合词asteroid/1spaceship/1作为包装对于定义“类型”和“子类型”的实际对象。那么缺少的是一种使用单一调度的方法,例如,在Logtalk中,重定向到正确的规则。 Daniel已经描述了如何使用双重调度作为可能的解决方案。另一种方法是定义参数对象,例如:

:- object(collide_with(_, _)).

    :- public(bump/0).
    bump :-
        % access the object parameters
        this(collide_with(Obj1, Obj2)),
        % wrap the object parameters
        wrap(Obj1, Wrapper1), wrap(Obj2, Wrapper2),
        % call the plain Prolog rules
        {collide_with(Wrapper1, Wrapper2)}. 

    wrap(Obj, Wrapper) :-
        wrap(Obj, Obj, Wrapper).

    wrap(Obj, Wrapper0, Wrapper) :-
        (   extends_object(Wrapper0, Parent) ->
            wrap(Obj, Parent, Wrapper)
        ;   Wrapper =.. [Wrapper0, Obj] 
        ).

:- end_object.

我们还有所有必要的对象来表示小行星和星舰的层次结构(为了简单起见,我在这里使用原型而不是类/实例)。例如:

:- object(spaceship).
    ...
:- end_object.

:- object(galaxy, extends(spaceship)).
    ...
:- end_object.

:- object(asteroid).
    ...
:- end_object.

:- object(ceres, extends(asteroid)).
    ...
:- end_object.

典型用法是:

?- collide_with(ceres, galaxy)::bump.
...

由于collide_with/2谓词的普通Prolog规则将接收(包装的)对象标识符,因此对于它们发送到那些对象的消息是微不足道的,这些消息请求任何必要的信息来实现当两个对象碰撞时我们想要的任何行为。

collide_with/2参数对象抽象出此多重调度解决方案的实现细节。与Daniel描述的双重调度解决方案相比,一个优点是我们不需要为碰撞消息挑出一个对象。一个缺点是我们需要在代码中使用额外的消息bump/0来触发计算。

答案 2 :(得分:0)

刚刚有了以下时髦的想法。假设我们有一个谓词 isinst / 2,instof / 2的镜像。如果我们想要 检查X是否是小行星。飞船 我们会这样做:

 isinst(asteroid, X). /* checks whether X is an asteroid */
 isinst(spaceship, X). /* checks whether X is a spaceship */

所以Prolog代码很简单:

 collide_with(X, Y) :- isinst(asteroid, X), isinst(asteroid, Y), /* case 1 */
 collide_with(X, Y) :- isinst(asteroid, X), isinst(spaceship, Y), /* case 2 */
 collide_with(X, Y) :- isinst(spaceship, X), isinst(asteroid, Y), /* case 3 */
 collide_with(X, Y) :- isinst(spaceship, X), isinst(spaceship, Y), /* case 4 */

现在假设我们的Prolog系统提供属性变量,以及属性变量的可读概念,例如X {...}。然后我们可以继续,并定义:

 collide_with(X{isinst(asteroid)}, Y{isinst(asteroid)}) :- /* case 1 */
 collide_with(X{isinst(asteroid)}, Y{isinst(spaceship)}) :- /* case 2 */
 collide_with(X{isinst(spaceship)}, Y{isinst(asteroid)}) :- /* case 3 */
 collide_with(X{isinst(spaceship)}, Y{isinst(spaceship)}) :- /* case 4 */

这可能会导致代码略快,因为属性变量会直接帮助统一,而且不是主体必须检查。

目前我还不清楚它是否也能带来更好的索引,问题是继承层可能会在运行时发生变化,这可能会影响索引并需要重新索引。如果我们可以保证继承hierarchie不是开放世界,例如将类标记为final,这也是成立的。如果Prolog系统被视为动态系统,也可能会发生变化。

除此之外,如果继承hierarchie不是开放世界,即如果可以枚举子类,则有一些明显的索引思想。这里唯一的问题是如果可能的话,由同一个机构有效地分享不同的头。否则,条款可能会爆炸。

再见

P.S。:从身体检查到属性变量时会有轻微的语义转换,因为属性变量可能会推迟钩子,

所以我们可能会在使用正文检查时得到collide_with(X,Y)失败,因为X和Y是未实例化的,另一方面,当使用属性变量时,collide_with(X,Y)成功。

但是当collide_with / 2的参数被实例化时,结果应该或多或少相同。