I wonder how one would combine unification and OO in Prolog. I would like to implement a multimethod dispatch on term objects.
Without term objects and simple terms I would do the following and could profit from multi-argument indexing:
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 only gives an exact type match.
What should I do if I want a sub class type match (there could be further spaceship subclasses such as excelsior, galaxy, etc.. that should also match in case 2,3 and 4).
Can I still use unification and indexing?
Bye
P.S.: The example is from here which doesn't have a Prolog solution:
https://en.wikipedia.org/wiki/Multiple_dispatch
You're kind of all over the place with your question: term objects, multimethod dispatch, etc. Prolog doesn't have term objects or dispatch, really, but I think the spirit of the question is interesting.
Before we can have multimethods and multiple dispatch we will need, well, dispatch. I take it what you're worried about is that you want to be able to write a procedure that looks like this:
And then you want to be able to say,
frob(excelsior(X, Y, ...))
and have it wind up in the first clause, somehow. This obviously isn't going to work out of the box, but that doesn't mean you can't make it work. Here are the approaches I would try:Choose a simpler functor shape
Instead of trying to make it work with
excelsior(...)
, change your representation to make it easier to introspect. An extremely general approach might look like this:This might work if you don't care about inheritance, but you do. Well, what if you made a slot for subtype information? Then you could match on that in the cases where you care and ignore it otherwise. Your structure would look like this:
Then you could write frob like this:
If you call it with Excelsior, it might look like this:
In other words, make your term have the most general type at the outside, and wrap more specific information in inner stuff.
Use a Metainterpreter
Writing your own dialect of Prolog is possible. If you add some facts to the database about what your types are subtypes of, your own metainterpreter can intercept the evaluation process and retry with parent types.
Unfortunately, I am not great at this and the following metainterpreter should be regarded as a buggy sketch/proof-of-concept, and not exactly a model to be followed.
The idea here is to try and execute the goal as-is, and then re-execute the goal having rewritten part of it. The
supertype_goal
is not very general though, and the replacement routine is not comprehensive, but it can illustrate the intent:Yeah, so, not great, but a more skilled Prolog user than me could probably clean it up and make it work.
Discussion
There are really only two places data can go in Prolog: it can live on the call stack, or it can live in the database. The first method I show is really an example of the first: find a way to repackage "subtyping" for your purposes so that it can live in the call stack without interfering with (some) of the unification. If you structure the terms carefully (and code carefully) you can probably make this work, and it will not be hell to debug. But it may be a bit harder to read.
The second method uses a separate relationship in the database to reify the relationship between the different "subtypes." Once you have that, you need to modify the interpreter to make use of it. This is easier said than done and a bit tricky, but I don't think it's the worst idea in the world. Although, in thinking about it, the kind of unification you want to do has to be engineered by the metainterpreter.
You'll find that Logtalk also has a similar dichotomy between "parametric objects", whose identifiers are essentially full Prolog terms, and ordinary objects, that create a whole namespace that they encapsulate as if in a separate database. With non-parametric objects, unification does not happen on the structure of the object the way it does with a term.
Performance Concerns
Suppose I take two objects as parameters in some method. If I use the first method, I think I benefit from indexing if it's available and I'm not digging in too deep into the term—general programming should be better, I think. I don't know how Prolog systems respond to unifying deep into some structure; I would imagine they do well, but I don't know about argument indexing. Feels like it would be fraught.
The second approach doesn't hold up that well at all. If my hierarchy could be N classes deep, I might try N^2 different combinations. This sounds unproductive. Clearly Paulo has figured something out in Logtalk, which doesn't seem to have this performance issue.
Diversion on Double Dispatch
This was quite a revelation to me when I was learning Smalltalk, so forgive me if you already know it. You can get the type benefit of multiple dispatch in a single-dispatch language using "double dispatch." Basically, you have all your objects implement
collide_with
, taking an "other" object as a parameter, so you haveAsteroid::collide_with(Other)
andShip::collide_with(Other)
andBullet::collide_with(Other)
. Then, each of these methods calls Other'scollide_with_type
, passing in self. You get a bunch of methods (and many you will delegate to the other side) but you can recreate all the missing type information safely at runtime.I wrote a crappy Asteroids clone in Lua some time ago, in which you can see how it works:
So you can see a little of everything there:
Asteroid:collideWithShot
removes the asteroid from play, but it delegates theAsteroid:collideWithPlayer(p)
toPlayer:collideWithAsteroid(a)
, and two asteroids colliding doesn't do anything.A basic sketch of how this might look in Logtalk would be:
Bear with me, I use Logtalk very rarely!
Update: sad to say, Jan Burse (author of Jekejeke Prolog) has pointed out that the cut operator will wreak havoc with double dispatch. This doesn't necessarily mean that multiple dispatch with subtyping is incompatible with unification, but it does mean that double dispatch as a workaround is incompatible with the cut, which will complicate nondeterminism and may ruin this approach. See the comments below for more discussion.
Conclusion
I don't think subtyping and unification are mutually exclusive, because Logtalk has them both. I don't think subtyping and multiple dispatch with argument indexing are mutually exclusive either, but Logtalk doesn't have multiple dispatch, so I can't be certain. I avoid subtyping even in Java, for the most part, so I'm probably biased. Multiple dispatch is kind of a $100 language feature though; I can't say many languages have it, but you can fake it quite effectively with double dispatch.
I would investigate Logtalk heavily if you are interested in this stuff though. The parametric example in particular is pretty compelling.
I have some doubt that this really answered your question or even landed in the same ballpark but I hope it helps!