zlacker

[parent] [thread] 9 comments
1. YeGobl+(OP)[view] [source] 2024-10-13 20:20:14
>> 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.

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?

replies(3): >>upghos+hQ >>upghos+jb2 >>thesz+Wbh
2. upghos+hQ[view] [source] 2024-10-14 05:29:23
>>YeGobl+(OP)
First of all let me say a big thank you for this extremely thoughtful and well crafted response, I genuinely appreciate it.

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.

replies(1): >>YeGobl+Ip1
◧◩
3. YeGobl+Ip1[view] [source] [discussion] 2024-10-14 11:54:34
>>upghos+hQ
Thanks for clarifying. I didn't know those terms, they're probably Markus Triskas' and Sicstus' inventions. Nothing wrong with that. I couldn't find anything about linearized arguments in Sicstus' pages though, they probably changed their docs since you 've seen it.

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.

replies(1): >>upghos+fA1
◧◩◪
4. upghos+fA1[view] [source] [discussion] 2024-10-14 13:23:16
>>YeGobl+Ip1
Apologies, it was on the tips and tricks page. I'll repost here:

  Linearize Arguments
  Ensures that each argument position is a variable that does not occur elsewhere in the clause head. As an example,
  
  foo(a, p(X1), X) :-
        body(a, X2, X2).

  would become:

  foo(A, A1, X) :-
        A = a,
        A1 = p(X1),
        body(a, X2, X2).
https://sicstus.sics.se/spider/tips.html

I'm really enjoying the convington et al read, cool to see that O'Keefe is an author too!

replies(1): >>YeGobl+jN1
◧◩◪◨
5. YeGobl+jN1[view] [source] [discussion] 2024-10-14 14:56:40
>>upghos+fA1
Ah, thanks. There's nothing wrong with that except that it makes code more verbose. An alternative is flattening, where you remove the compound arguments from the heads of predicates by defining them as new predicates.

Let me see if I can apply flattening to the example above:

  % Flattened version:
  foo(a, A1, X) :-
        q(A1)
        ,body(A, X2, X2).

  q(p(X1)):-
    % ... code that binds X1
    .
With flattening only compound terms are replaced, not constants so "a" remains unchanged. It's similar to an ad-hoc type system again, but the cool thing is that it can be done automatically. There's an algorithm for it known to the Inductive Logic Programming (ILP) community. Let me see if I can find that paper... Yep:

Flattening and Saturation: Two Representation Changes for Generalization, Céline Rouveirol, 1994.

https://link.springer.com/content/pdf/10.1023/A:102267821728...

The example is a bit unusual though because it only checks that A1 = p(X1) and doesn't do anything with X1, it even leaves it dangling as a singleton; same with X and X2. That's very unlikely for real-world code where you want to use unification as a mechanism to pass the state of the computation around your program. With flattening you want to have an extra argument in the new predicate that "returns" a value that you want to process further. I think maybe they meant to write it like this:

  foo(a, p(X1), X) :-
        body(a, X1, X).

  foo(A, A1, X) :-
        A = a,
        A1 = p(X1),
        body(a, X1, X).
In which case the flattened version would be like:

  foo(a, A1, X) :-
        q(A1,X1),
        body(a, X1, X).

  q(p(X1),X2):-
    % ... code that binds X2
    .

So, I guess, we see that this is a known gotcha and the logic programming community has different ways to work around it. In the ILP community there's a different motivation (to make a language finite, and therefore decidable, by removing "functions").

>> I'm really enjoying the convington et al read, cool to see that O'Keefe is an author too!

Yeah. I haven't heard much from O'Keefe lately and I miss his advice. He was a regular contributor to the SWI-Prolog mailing list when I was doing my MSc in 2014. I think there was a bit of a problem with the SWI-Prolog community moving to Google and he hasn't returned since, even though the community is now on Discourse.

6. upghos+jb2[view] [source] 2024-10-14 17:18:22
>>YeGobl+(OP)
Let me thank you also for sharing those resources and acknowledging this is a tricky problem for newcomers. I do love Prolog and I used to think I was a very careful coder with significant attention to detail, but Prolog is exposing what a clumsy buffoon I am if I don't have significant IDE support or "crash on typo" support.

I am particularly grateful because most of the books as you know were written a long time ago and tend to focus more on the beauty of the language and tend to gloss over the software engineering or composing large reliable problems. It is very rare to meet someone with significant experience in Prolog that can still remember what it was like to struggle with issues like that. Most "criticism" I read about Prolog are very superficial and are distracting from the more pressing best practices, so I'm extremely grateful that you shared this paper and I am really surprised I've never encountered it before.

I am actually really shocked to hear that the advice is to roll your own type system, and it's really interesting. I wonder if that is also the case for other expressive languages such as Forth/PostScript, etc. Most of the languages I work with more often (Python/JS/TypeScript/Clojure/C# ... but PARTICULARLY Python) tend to view runtime assertions and type-checking as an *anti-pattern*.

So I am really pleased and surprised to hear that a language as expressive and eloquent as Prolog, at least some folks find that writing that sort of code to be acceptable.

I realize this comment is getting long but the technique you mentioned of using goal_expansion/2 to compile away the assertions --

the dang thing about Prolog is the more you look into it, the more powerful you realize it is, but I'll be damned if it's not impossible to find these things out without someone telling you about them. Most languages seem to be possible to self-teach, but if I were to self-teach Prolog I never would've learned about the concept of monotonicity and I'd be using cut operators willy-nilly etc, lucky that I stumbled on Markus's blog/videos and lucky that I bumped into you!

Just like Python dislikes defensive runtime type checking, another interesting thing is that HEAVY use of metaprogramming tends to be viewed disfavorably in lisp, at least in Clojure! It's typically "don't write macros unless you have no other choice, because no one wants to read/learn your damn macros". I assumed it would be the same way in Prolog, but perhaps I'm wrong?

Anyway thanks again for the really patient response and for sharing your experience.

replies(1): >>YeGobl+gH4
◧◩
7. YeGobl+gH4[view] [source] [discussion] 2024-10-15 14:57:01
>>upghos+jb2
I'm glad it was of help.

>> the dang thing about Prolog is the more you look into it, the more powerful you realize it is, but I'll be damned if it's not impossible to find these things out without someone telling you about them. Most languages seem to be possible to self-teach, but if I were to self-teach Prolog I never would've learned about the concept of monotonicity and I'd be using cut operators willy-nilly etc, lucky that I stumbled on Markus's blog/videos and lucky that I bumped into you!

I hear you. When I first learned Prolog, in the second year of my CS degree course, the first thing I did was to go to my university's library and gather up all the textbooks I could find about it. I literally came home bent over from the sheer weight of books. It still took me a very long time to learn the ins and outs of it. It doesn't help that Prolog has a small and probably shrinking community and many of the people with long backgrounds and deep knowledge of its practice are starting to retire from public fora; like O'Keefe.

There are only a few resources on the internet that can help you understand Prolog. Markus Triska's as you found out is one of the best (although I have a very different view of many things than he does, there's no doubt he is an expert in the language). When I was learning I got a lot of mileage out of a tutorial on the Amzi Prolog website:

https://amzi.com/AdventureInProlog/index.php

The best thing you can do of course is to keep working with it as much as you can. Things eventually make sense and you learn what works and what doesn't with experience. That's not always easy to do of course. As I say in another comment I had to leave (my lucrative career in) the industry and enter academia (which pays peanuts) to find a place where I could really work as much as I wanted with Prolog.

>> I am actually really shocked to hear that the advice is to roll your own type system, and it's really interesting. I wonder if that is also the case for other expressive languages such as Forth/PostScript, etc. Most of the languages I work with more often (Python/JS/TypeScript/Clojure/C# ... but PARTICULARLY Python) tend to view runtime assertions and type-checking as an anti-pattern.

I don't want to sound even more like a zealot than I already do, but the thing is you can do all that in Prolog more easily than in other languages. In general the way I program in Prolog is that I create a language to describe my problem, and then solve my problem in that language. I don't mean that I necessarily design a new programming language and write a parser for it, although you can do that very easily in Prolog with Definite Clause Grammars (you get a decent parser and generator that way though not optimal). I mean that I make up a language to talk about the domain, and use it to describe my problems and their solutions. You do that too, in your solution/3 predicate above, where as far as I can tell you describe resources and actions of a robot etc. Again Prolog lets you do that easily: you can define the abstractions you need. That goes all the way up to the abstractions used in other programming paradigms, like functions or objects etc, and types.

>> I am particularly grateful because most of the books as you know were written a long time ago and tend to focus more on the beauty of the language and tend to gloss over the software engineering or composing large reliable problems.

Yeah, I tend to ignore those things. I love Prolog but I recognise it's not to everyone's taste. It's quite obvious. It's nice to hear you love it too :)

8. thesz+Wbh[view] [source] 2024-10-20 18:14:17
>>YeGobl+(OP)

  > Develop your own ad hoc run-time type and mode checking system.
"Any sufficiently complicated List or Prolog program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Haskell type inference and checking system."

Modelled after Greenspun's Tenth Rule [1].

[1] https://en.wikipedia.org/wiki/Greenspun%27s_tenth_rule

replies(1): >>illogi+Dlr
◧◩
9. illogi+Dlr[view] [source] [discussion] 2024-10-24 15:17:02
>>thesz+Wbh
I suppose you could hack a bug-ridden implementation of Prolog unification in Prolog but why?

Hindley-Milner type inference is unification over types and unification is built-in to Prolog. Functional programmers ignore this because their textbooks never refer to the original description of unification by Robinson. Wait who? Unification was probably invented by Damas, Hindley or Milner right? Or maybe Haskell Curry? Hey maybe it was John McCarthy?

replies(1): >>thesz+4ZB
◧◩◪
10. thesz+4ZB[view] [source] [discussion] 2024-10-28 22:35:34
>>illogi+Dlr
Prolog was introduced into general public in 1972, 4 years after Hindley's algorithm that used unification and substitution.

In any case, what was suggested is, frankly, a development of the (tailored up) type system.

My experiece suggests that this development will not be without caveats, especially related to performance.

[go to top]