Collision of stellar objects using multimethods in OO Prolog?

I wonder how to combine unification and OO in Prolog. I would like to implement multipoint dispatch of term objects.

Without term objects and simple terms, I would do the following and could profit from indexing with a few arguments:

collide_with(asteroid(_), asteroid(_)) :- /* case 1 */ collide_with(asteroid(_), spaceship(_,_)) :- /* case 2 */ collide_with(spaceship(_,_), asteroid(_)) :- /* case 3 */ collide_with(spaceship(_,_), spaceship(_,_)) :- /* case 4 */ 

But the above gives an exact type match.

What if I want to match the type of subclass (there may be additional subclasses of the spacecraft, such as excelsior, galaxy, etc., which should also coincide in cases 2,3 and 4).

Can I use unification and indexing?

Bye

PS: An example from here that does not have a Prolog solution:
https://en.wikipedia.org/wiki/Multiple_dispatch

+5
source share
3 answers

You seem to be everywhere with your question: term objects, multipoint sending, etc. There are no term or submit objects in the prolog, really, but I think the spirit of the question is interesting.

Before we can use multimethods and multiple sending, we need, well, send. I understand what you are worried about is that you want to write a procedure that looks like this:

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

And then you want to be able to say frob(excelsior(X, Y, ...)) and somehow get it in the first sentence. This obviously will not work out of the box, but that does not mean that you cannot make it work. Here are the approaches I would like to try:

Choose a simpler functor shape

Instead of trying to get it to work with excelsior(...) , change your mind to make it easier to understand. A very general approach might look like this:

 object(Type, Properties...) 

It might work if you are not interested in inheritance, but you do it. Well, if you made a slot for subtype information? Then you can compare with it in those cases when you do not care, and ignore them otherwise. Your structure will look like this:

 type(SubtypeInfo, Properties...) 

Then you can write frob as follows:

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

If you call it using Excelsior, it might look like this:

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

In other words, so that your term has the most general type on the outside and wraps more specific information internally.

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

Use Metainterpreter

It is possible to create your own dialect of Prolog. If you add some facts to the database stating that your types are subtypes, your own metainterpreter may intercept the evaluation process and try again with the parent types.

Unfortunately, I am not very good at this, and the next metainterpreter should be considered as an erroneous sketch / proof of concept, and not quite a model to be followed.

 :- 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]. 

The idea here is to try to fulfill the goal as is, and then re-execute the goal by rewriting part of it. supertype_goal not very general, and the replacement procedure is not complete, but can illustrate the intention:

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

Yes, it’s not great, but a more experienced Prolog user than I could possibly clear it and make it work.

Discussion

There are only two places in Prolog that can be written to data: it can live on the call stack or it can live in the database. The first method that I show is really an example of the first: find a way to repack "subtypes" for your purposes so that it can live in the call stack without interfering with (some) unification. If you carefully structure the terms (and the code carefully), you can probably do this work and there will be no hell for debugging. But it can be a little harder to read.

The second method uses a separate link in the database to confirm the relationship between the various "subtypes". After that, you need to change the interpreter to use it. This is easier said than done, and a little complicated, but I don't think this is the worst idea in the world. While thinking about this, the kind of union you want to do should be designed using metainterpreter.

You will find that Logtalk also has a similar dichotomy between “parametric objects”, whose identifiers are essentially complete Prolog terms and ordinary objects that create a whole namespace that they encapsulate, like in a separate database. With nonparametric objects, unification does not occur in the structure of the object as is done with the term.

Performance issues

Suppose in some method I take two objects as parameters. If I use the first method, I think it is useful for me to index if it is available, and I do not go too deep into the term. General programming should be better, I think. I do not know how the Prolog systems react to unification into some structure; I would suggest that they succeed, but I don't know about indexing arguments. It seems like it would be fraught.

The second approach does not withstand this at all. If my hierarchy can be N classes in depth, I can try N ^ 2 different combinations. That sounds unproductive. Obviously, Paulo understood something in Logtalk, which doesn't seem to have this performance issue.

Double Forwarding Forwarding

It was a revelation to me when I studied Smalltalk, so forgive me if you already know this. You can get the benefits of multi-send type on a single mailing list using "double-sending". Basically, you have all your objects that implement collide_with , taking the “other” object as a parameter, so you have Asteroid::collide_with(Other) and Ship::collide_with(Other) and Bullet::collide_with(Other) . Each of these methods then calls Other collide_with_type , passing in itself. You get a bunch of methods (and many of them will delegate to the other side), but you can safely recreate all the information about the missing type at runtime.

I wrote crappy Asteroids clone in Lua some time ago in which you can see how this works:

 -- 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 

So, you can see a little of everything: Asteroid:collideWithShot removes the asteroid from the game, but it delegates Asteroid:collideWithPlayer(p) to Player:collideWithAsteroid(a) , and the two oncoming asteroids do nothing.

A basic example of how this might look in Logtalk would be:

 :- 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. 

Bring me, I rarely use Logtalk!

Refresh . Unfortunately, Jan Burs (author of Jekejeke Prolog) indicated that the cut operator would lead to double-sending chaos. This does not necessarily mean that subtype multiple submission is incompatible with unification, but it does mean that double submission as a workaround is incompatible with a cut, which will complicate non-determinism and can ruin this approach. See comments below for more details.

Conclusion

I do not think that subtyping and unification are mutually exclusive, because Logtalk has both of them. I don’t think that subtyping and multiple sending with argument indexing are also mutually exclusive, but Logtalk does not have multiple sending, so I can’t be sure. I avoid subtypes even in Java, for the most part, so I'm probably biased. Several dispatchers is a kind of language function for $ 100; I can’t say that many languages ​​have this, but you can effectively fake it with double sending.

I would investigate Logtalk strongly if you are interested in this. The parametric example in particular is pretty convincing.

I have some doubts that this really answered your question or even landed in the same stadium, but I hope this helps!

+3
source

In CLOS, common functions used for multiple dispatch are not encapsulated in classes, but are grouped by function name. Thus, the equivalent here will be a simple prologue of rules. Moreover, provided that the arguments are indexed multiple times, the arguments in the rule chapters must be sufficiently instantiated to the “types” over which we want to send multiple, so that each correct rule is selected each time without false selection points. As shown in the OP:

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

Given how unification works in Prolog, if we want to have specializations of the main types of asteroids and spaceships, and after Daniel’s suggestion, we can use the compound terms asteroid/1 and spaceship/1 as wrappers for real objects defining “types” and “subtypes” . What is missing is a way to use a separate submission, as found, for example. in Logtalk, to redirect to the correct rule. Daniel has already described how to use double dispatch as a possible solution. An alternative would be to define a parametric object, for example:

 :- 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. 

We would also have all the necessary objects to represent the hierarchies of asteroids and starships (here, for simplicity, I use prototypes instead of classes / instances). For instance:

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

Typical Usage:

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

Since simple Prolog rules for the collide_with/2 predicate will receive (wrapped) object identifiers, it is trivial for them to send messages to those objects requesting any necessary information to implement any behavior that we want when two objects collide.

The collide_with/2 parametric object abstracts the implementation details of this multiple dispatch solution. One advantage over the dual dispatch solution described by Daniel is that we do not need to allocate one of the objects for the collide message. One of the drawbacks is that we need an extra bump/0 message in the code lock to trigger a calculation.

+2
source

It was just the next funky idea. Suppose we have the predicate isinst / 2, the mirror inst / 2. If we want to check that X is an asteroid, respectively. spaceship we will do:

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

So, the Prolog code is straightforward:

  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 */ 

Now suppose our Prolog system offers attribute variables and a readable concept for attribute variables such as X {...}. Then we could go ahead and determine:

  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 */ 

This can lead to a slightly faster code, since attribute variables will directly contribute to the union, and the point is not that the body has to check something.

Whether this is also the best indexing for me so far is not clear, the problem is that the inheritance hierarchy can change at runtime, and this can affect the index and require a re-index. This also happens if we can guarantee that the hierarchy of inheritance is not an open world, for example, designating classes as final. If the Prolog system is considered dynamic, this can also change.

In addition, there are some obvious ideas for indexing if the inheritance hierarchy is not an open world, that is, if subclasses can be listed. The only problem here would be to effectively share different heads, if possible, of the same body. Otherwise, there could be quite a blast in the sentences.

Bye

PS: There is a slight semantic shift when moving from body checking to attribute variables, since attribute variables can delay the hook,

therefore, we can find that collide_with (X, Y) fails when using body checks, because X and Y are optional, and on the other hand, collide_with (X, Y) succeeds when using variable attributes.

But the result should be more or less the same when creating collide_with / arguments.

0
source

Source: https://habr.com/ru/post/1241309/


All Articles