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!