A pure logic negation operator for miniKanrenHave you ever noticed that some examples from "The Reasoned Schemer" are not so reasonable? If so, you may want to read this post. MotivationminiKanren is an educational logic programming language designed by Dan Friedman, William Byrd and Oleg Kiselyov. For teaching logic programming, they also co-authored the book "The Reasoned Schemer (TRS)." As a person hugely benefitted from this book (and also every other book of the "little book" series), I highly recommend TRS to you. While elegantly designed, miniKanren hasn't a "pure" negation operator. There is a 'conda' operator which is similar to Prolog's cut, but it is not pure. That means once it is used, we may miss possible answers even if they exist. Thus although the 'conda' operator exists, it is not recommended for serious use. But now we have a problem, if we can't have a 'cond' operator with implicit negation of the conditions of the previous lines, we will have trouble interpreting the results from some code from "The Reasoned Schemer (TRS)". For example, on "Frame 30" of TRS, we have the following program, which invokes rembero, a "logic function" for deleting an item from a list. The definition of rembero is (notice the 'conde' operator):
If it is used this way:
Running it and we have the following 7 answers:
Have you ever been surprised that there are 7 answers? Is it really possible that y fails to remove itself, but goes beyond and removes 'd', 'peas' and 'e' (Answers 4, 5 and 6), or fails to remove all of them (Answer 7)? Have you noticed that only the the first 3 answers are reasonable, and the last 4 answers shouldn't really happen? For this particular example, the result from "The Reasoned Schemer" was not so reasonable. a pure negation operatorAs a student in Dan's class (B521), I was puzzled by the above results. I asked Dan and Will why this happens. They told me that this is because 'conde' operator of miniKanren is not exactly like 'cond' of Scheme. In Scheme, every line in the 'cond' expression implicitly negates all the previous conditions. This is to say, we execute the second line only if the first condition fails, and we execute the third line only if the conditions on the first and second lines both fail, and so on. On the other hand, the 'conde' operator of miniKanren doesn't implicitly insert the negation of the conditions on the previous lines. The reason that miniKanren doesn't do this is because there is no easy way of doing "negation" in logic programming. According to Will Byrd, this is a thorny subject that has been researched for over 30 years. As a dare devil who never believes how difficult things are, I thought: "Why not try my luck and see how far I can go competing with these 30 years of research?" Out of this evil-minded motivation, I independently reimplemented miniKanren and added a negation operator in it (named "noto", naturally). Different from 'conda' and Prolog's cut, noto is pure in the sense that it doesn't cut out possible answers if they exist. Using noto, I defined a new conditional construct named 'condc', which implicitly inserts negations of all previous conditions on each line. It is designed to behave as an exact logic counterpart of Scheme's 'cond'. If we use the 'condc' operator to redefine remebero (only one character is changed), we will have the following (more reasonable) results:
Notice that we got only 3 answers (instead of 7), plus two constraints for the third answer. In fact each answer is paired with a constraint list, but the constraint lists are empty for the first two answers. This is why they are displayed as Now I briefly describe what these three answers mean. The first answer The third answer is more interesting. It not only has an answer
(Here #1(y) is a special notation to say that y is a logic variable.) These constraints are in conjunctive form. They are saying: If we are to have this answer, neither We have no more answers beyond the third, because under no condition should y be able to remove 'd', 'peas' or 'e', because the logic variable y will definitely remove y, no matter what it is! The iteration will definitely stop at the point where "y meets y". Clear? How does it work?The principle behind the negation operator ("noto") is to propagate the negation of goals as constraints (as in constraint logic programming) down the execution paths of the miniKanren program. Before I tell you further details, I want to describe the intuition behind its design. Looking at the details without knowing the design principles will not be very useful. To see how you can design a negation operator, just think about how you can make the goal So now we can proceed to look at the details how this works:
From the above mechanism, can you see why the example of rembero produces these results?
Why hasn't the second answer a constraint which says "y is not a"? This is because for the second answer, we already know that "y is b", which implies "y is not a". The system is intelligent enough to omit "y is not a" from the answer's constraints because it knows that it is subsumed under the current substitution ("y is b"). LimitationsNested negations does not work properly, so if you have After several years of this experiment, I had an interesting discussion with Oleg Kiselyov on this topic. An excerpt of our conversation is included as comments at the bottom of the code. In his words, although the implementation of noto works to some degree, it is not perfect. To the best of his knowledge, no negation operators work perfectly until this day. So, did I beat 30 years of hardcore research? Probably not. But consider this - it took me less than a month to think of and implement all this. I worked completely independently, day and night. This happened in 2008 when I first learned miniKanren and logic programming. Today as a mature programming languages researcher, I'd like to take it as an amusement to revisit and see how far I can go down the path I have started exploring 5 years ago. CodeThe reimplemented miniKanren, together with the negation operator, has been available from my GitHub for years without being noticed. Now I made it an independent project and hope to have time (and public pressure) to develop it further. I also hope to gather ideas from real logic programming gurus about other ways of implementing pure negation operators. If you are interested in playing with it, or you want to research on this topic, my code is here for free: |