It makes the same analogy that Prolog (or logic programming languages in general) have been strongly influenced by the resolution algorithm. In practice that means that if you write a non-trivial program, if performance is not right you'll need to understand the execution model and adapt to it, mainly with the pruning operator (!). So while the promise is to "declare" values and not think about the implementation details, you're railroaded to think in a very specific way.
I personally found that frustrating to find good solutions essentially unworkable because of this, in comparison with either imperative or functional paradigms that are significantly more flexible. As a result, Prolog-style programming feels limited to the small problems for which it is readily a good fit, to be integrated into a general program using a general-purpose language. I may be wrong on this, but of the 50 people that learned Prolog around the same time as me, none kept up with it. Meanwhile, other niche languages like Ocaml, Haskell and Scheme had good success.
Rethinking the language foundation could remove these barriers to give the language a broader user base.
The argument is basically that Prolog is not 100% declarative and that if we jump through a few hoops, and translate it all to functional notation, we can make it "more declarative". But let's instead compare the incomplete declarativeness of Prolog to a fully-imperative, zero-declarative language like Python or C#. We'll find I believe that most programmers are perfectly fine programming completely non-declaratively and don't have any trouble writing very complex programs in it, and that "OMG my language is not purely declarative" is the least of their problems. I hear some old, wizened nerds even manage to program in C where you actually can drop to the hardware level and push bits around registers entirely by hand O.o
result(World0, move(robot(R), Dir), World) :-
dissoc(World0, at(robot(R), X0), World1),
direction_modifier(Dir, Modifier),
X #= X0+Modifier,
conj(World1, at(robot(R), X), World).
result(World0, drop_rock(robot(R), Place), World) :-
dissoc(World0, capacity(Place, Capacity0), World1),
dissoc(World1, carring_rock(robot(R)), World2),
Capacity #= Capacity0 + 1,
conj(World2, capacity(Place, Capacity), World).
result(World0, pickup_rock(robot(R), Place), World) :-
dissoc(World0, capacity(Place, Capacity0), World1),
Capacity #= Capacity0 - 1,
conj(World1, capacity(Place, Capacity), World2),
conj(World2, carrying_rock(robot(R)), World).
See if you can spot the bug....
...
...
carrying_rock vs carring_rock
Because the typo was in a functor (not predicate or singleton variable) there was no IDE or language support, Prolog assumed that I wanted an reported the wrong answer.
of course the snippet I showed was part of a larger example.
In other languages it would've taken me 5 minutes to bisect the program or debug and find the error but it took me 3-4 hours.
I ended up needing to write a lot of error correcting code, basically a half-assed type system, and that code ended up being more substantial than the actual program logic.
Is this common? Am I "doing it wring"?Right now this seems to have all the downsides of programming exclusively with "magic strings", and I haven't been able to find any cure for it or even seen this problem discussed elsewhere.
*Edit:*
I even rewrote it for SICStus and downloaded their IDE and taught myself Eclipse just to use their IDE plugin, and found that setting breakpoints didn't help the problem, because naturally due to the fact that the functor is in the predicate signature, the predicate is never stepped into in the first place!
I could linearize the arguments and include them in the body but this destroys the indexing and can put me into "defaulty representation" territory.
The cure is to not try to program with "magic strings". You don't need to, and if you really want to, then you should try to understand what exactly it is that you're doing, and do it right.
Specifically, what you call "magic strings" are what we call "functions" in First Order Logic, and that Prolog calls compound terms, like carring_rock(robot(R)) which is in fact two compound terms, nested. Prolog lets you nest compound terms infinitely and if you really want to hurt yourself, one great way to do it is to nest terms everywhere.
The alternative is to understand that when you use compound terms as arguments to predicates (or other terms) you are really _typing_ those arguments. Above, "carring_rock(_)" is the type of the second argument of result/3, and "robot(_)" is the type of the single argument of carring_rock/1. The catch is, of course, that Prolog is not a typed language and it doesn't care if you want to hurt yourself. So if you need to have types like that, then you should write some code to explicitly handle types and do some type checking. For instance, right out the top of my head:
result_type(T):-
T =.. [carring_rock,R]
,robot_type(R)
,!.
result_type(T):-
throw('Unknwon result type':T)
robot_type(T):-
T =.. [robot,R]
, % ... further typing of R
,!.
robot_type(T):-
throw('Unknown robot type':T)
Note that this is not the right way to throw errors but I can't now.A simpler thing I'd advise, but that's going into style territory, is to keep variable names as short as possible. One reason it's hard to spot the mistake in the code above is that the source code is all cluttered with long variable names and the nesting of compound terms makes it even more cluttered. An IDE with good syntax highlighting can also help. Perversly, I find that there are many people who code in Prolog either without syntax highlighting at all or in IDEs that are not aware of common results of typos, like singleton variables. The SWI-Prolog IDE is good for this and Sweep for Emacs has a good reputation also (although I haven't tried it):
https://eshelyaron.com/sweep.html
Edit: it just occurred to me that the project I'm working on currently, in my post-doc, involves quite a bit of typing using compound terms as arguments, like you do above. I've opted for a program synthesis approach where the predicates I need with typing are automatically generated from a specification where I define the arguments of predicates' types. Doing the same thing by hand is probably my number two recommendation of how not to code in Prolog. Number one is "the dynamic database is evil", but does anyone listen to me? Never.
Edit 2: Covington et al's Coding Guidelines for Prolog make the same point:
5.13 Develop your own ad hoc run-time type and mode checking system.
Many problems during development (especially if the program is large and/or there
are several developers involved) are caused by passing incorrect arguments. Even
if the documentation is there to explain, for each predicate, which arguments are
expected on entry and on successful exit, they can be, and all too often they are,
overlooked or ignored. Moreover, when a “wrong” argument is passed, erratic be-
havior can manifest itself far from where the mistake was made (and of course,
following Murphy’s laws, at the most inconvenient time).
In order to significantly mitigate such problems, do take the time to write your
own predicates for checking the legality of arguments on entry to and on exit from
your procedures. In the production version, the goals you added for these checks
can be compiled away using goal_expansion/2.
https://arxiv.org/abs/0911.2899>> I could linearize the arguments and include them in the body but this destroys the indexing and can put me into "defaulty representation" territory.
Sorry, I don't know what either of those are: "linearize the arguments" and "defaulty representation".
Prolog only has first-argument indexing normally, although some implementations let you change that and use your own indexing scheme. Is that what you did?
On mobile and I want to dig into this some more tomorrow but let me start by addressing the last two points.
"Defaulty representation" is described here by Marcus Triska (towards the bottom of the page): https://www.metalevel.at/prolog/data
The terminology of linearizing the arguments I lifted from the SICStus IDE features page: https://sicstus.sics.se/spider/index.html
Thanks again and I'm reviewing your thoughtful comments, thank you again.
The "defaulty representation" Markus Triska discusses is indeed something to be avoided, but if compound terms start to proliferate the chance of typos causing problems like the one you had increases. The solution is ad-hoc typing as recommended by Covington et al.
The balance I think, between a "defaulty representation" and an explosion of "magic strings" is to make sure there are only a handful of predicates that expect their arguments to be typed as compound terms and that those predicates are at the base of a hierarchy of calls that pass those arguments around. Those predicates are responsible for reasoning about those typed arguments and they raise errors if something is off. That way you know quickly when there's a mistake, and where it is.
Another thing to keep in mind is that it's easy to test your Prolog predicates in isolation and check that they do what you think they do. This is the best way to catch errors that have to do with argument structure. What I mean is that you can run each predicate on its own, without having to start from the top of your program. You go to your repl and make some queries to the predicate you want to test with different arguments and see how it behaves. That will help catch errors. Unit tests can also help, if you have the patience to write them.
To be honest, I'm not going to defend Prolog for making this kind of thing easy to hurt yourself with. I love Prolog but it's not friendly to newcomers, exactly because of things like that, which you only learn with experience. Even now, after using it for ... 14 years or so, it still finds ways to hurt me. You gotta be careful with it.