Logic programming library development log

The exercise with identifier blocks is a mere experiment for now - I’m not sure if this approach is turning out beneficial on balance. I tried the ? reporter before, but (like you are writing) the upvar name will not automatically be prefixed with “?”, so the “?” needs to be part of the upvar name proper: Logic programming DEVLAB script pic 5; thus the generating block would be like: Logic programming DEVLAB script pic 4, looking odd.

:smirk:

As for the difference Prolog makes between functor and arguments: I agree it seems unnecessary, so the Amalog interpreter doesn’t, really:

Indeed one can ask for various relations between “arguments”:

One step further, though this final step is mostly cosmetic, would be to simply define a goal as a tuple:

In Amalog, the only real difference between item 1 (functor) and the following arguments is that a Snap! script may be used as functor, with the final argument (= item last) as the reported result of the script (function / predicate) having been applied on the other arguments - unless the script is a command, in which case the result is “true” by definition.

Logic programming DEVLAB script pic 9

Don't forget to handle (?relation ?person bill). :~)

There are many optimizations that constant functors enable. And for a particular task if one wanted variable relations then every clause could be (relation ?relation ?arg-1 ... ?arg-n)

Furthermore, constant relations makes it easy to interpret logic programs as first-order logic (really the Horn clause subset).

Which relatives is bill having arguments with? :wink: I'm afraid that's a subject for juicier fora.

Interesting. Such as ... ?

That looks a bit overdone for most practical purposes. OTOH it's probably easy to transform a database to enable it.

What practical advantages does that bring?

Yes. But can't you recognize the common special case of a constant functor and do the optimizations? IIRC SICP does that; its databases of assertions and rules are indexed on the functor, if constant, so you can quickly find relevant facts and rules, but also you can use a variable and search the entire database.

I mean, don't you ever think "Julia is some sort of cousin of mine, but I forget exactly what kind" and then wouldn't you like to follow that with "Let me ask Prolog"? (I used to give implementing "nth cousin k times removed" in logic programming as an exam question, so I have a warm spot in my heart for those obscure relations.)

I hate the name "functor," by the way, because it only encourages students to try to do composition of functions

grandparent(x,y) :- parent(x,(parent(y)) //wrong!

Why aren't they called "relations"?

Although SICP has optimizations for constant ones below the abstraction barrier, in its user-level documentation it treats assertions, rules, and queries as arbitrary token strings, so you can say

(assert! (Brian likes potstickers))

and ask questions such as (?who likes potstickers) and (Brian likes ?what).

P.S. That would also be more Snap!ly, because we have Smalltalk-style title text interspersed with arguments.

P.P.S. And anyway I can never remember the order of arguments to a relation such as PARENT, and it'd be easier for me if the relation were

(parent of ?x is ?y)

I don’t like it either - to me it sounds like a very strict bureaucrat from former East Germany.

Interesting line of thought. Perhaps even something like ... ?

Logic programming DEVLAB script pic (8)

Logic programming DEVLAB script pic (6)

This enables us to style a fact / goal / head as:

  • a postfix relation of 1 argument
  • an infix relation with 2 arguments
  • an infix relation with 1 leading argument, and any number of trailing arguments
    The relation would always be item 1 of the block's output.

A separate block would support calling Snap! functions.

The most obvious is that a goal need only be matched with the heads of those clauses with the same predicate. Either a quick table lookup or a pointer to those clauses.

For many logic programs one can understand them both procedurally and declaratively. Sometimes instead of debugging a predicate procedurally one looks at the code and asks "Is this saying something true?"

But why does the logic have to be first order to do that? Mathematicians evaluate the truth of higher order relations all the time.

Why not allow any number of left arguments, if you're going to distinguish the relation as an upvar?

But in a perfect world I could say

?list items from ?a to ?b are ?result

and you'd make a relation named _ items from _ to _ are _. But the user would refer to it the way I wrote it above. I know, upvars would have to be more flexible.

Because people have figured out how to evaluate the Horn clause subset of first-order logic very efficiently. And I'm pretty sure higher-order is computationally expensive to evaluate.

Yeah. I remember AI people at both MIT and Stanford working on that sort of thing in my even more ignorant youth. But still, one should be able to use easy methods on easy problems and still have hard methods for hard problems. (Not so much conceptually hard, I think, as just exponential time.)

OK. I was going to look into that option anyway.

There’s practical reasoning behind that: dropping a reporter block onto a double arrow ( ◁▷ ) that is followed by at least one slot in the same line, is rather difficult, or close to impossible (granted, one may make a copy of the block, paste that onto the arrow, subsequently delete the original). Therefore I only use multi-inputs at the end of a line - if necessary I’ll insert a line break, which IMO isn’t suitable here. OTOH, it’s not very likely that anyone is going to use an input list here. So I’ll reconsider.

… back to SICP :wink:

In order to make a reasonably efficient logic programming interpreter, I’ll need to implement memoization. I edited some code I used earlier for memoizing recursive operations:


(corrected version; the original contained a bug)

When I’m going to use this for storing solutions to a goal (“relation” will be item 1 of the arguments), I need to make sure that the number of arguments of that type of relation is always the same, for this algorithm to work.. So if a type of relation’s arguments number is variable, I may have to insert the arity as an extra argument for memoization.

The function uses the following simple lookup function:

I would have preferred to use item (key) of (table) - unfortunately that doesn’ t work for numbers, so I made this one up.

You can have the value part of each kv-pair to be itself a pair consisting of a value and a sub-dictionary. Then, if there are no more levels you report the value part, and if there are more levels you do the recursive call on the sub-dictionary part. (Pretty sure this is in Chapter 3. ;~P)

I searched SICP for multilevel memoization, but didn’t find it. Pretty sure that what you suggest is exactly what my code does, though.

Ah, sorry, I didn't make myself clear. Memoization is an application of the table data structure. (This is true in general, not just in SICP-land.) There isn't anything specifically about multilevel memoization, but that's just memoization in a multilevel table. Tables are Section 3.3.3 and n-dimensional tables are ex. 3.25. The detail about allowing both a value and a subtable for the same key is, I guess, one of my exam problems. :~)

Taking into account @bh’s suggestions (or directions, actually), I am manually translating relevant SICP code to Snap! - so I can at least see how it works (not necessarily going to use all of it in the end).

One piece of code I'm not sure I don't understand (at all) is on p. 642 (§ 4.4.4.2):

(define (always-true ignore frame-stream) frame-stream)

I now guess this translates as: a reporter always-true with two arguments (such as another reporter, and a stream), ignoring the first argument and reporting the second.

(put ‘always-true ‘qeval always-true)

And this would make an entry into a table implementing polymorphism, I suppose. The put function (with its sibling get) is a bit hard to find within the SICP text, it's not mentioned in the index and I believe I saw two different implementations but I can't remember where exactly, probably somewhere between pages 150 and 600-something. in § 2.4.3, and § 3.3.3, and perhaps somewhere else too; really confusing is in § 4.4.4.2 it appears as if the arguments have been switched.

Could anyone knowledgeable (@bh, @toontalk?) explain what this means, and how that would translate into Snap!confirm this ?

Also, I’d like to be sure that (p. 656, § 4.4.4.7):

(define (contents exp)
_ (if (pair? exp)
_ _ (cdr exp)
_ _ (error “unknown expression CONTENTS” exp)))

[BTW the “_”s mean nothing, I am merely using them for layout (indentation)]

… can safely be translated into Snap! as:

… or even just:

Logic programming SICP script pic 2

… ?

Uh oh, am I being too controlling?

ALWAYS-TRUE: The general form of a rule is
(rule (conclusion) body)
analogous to
(define (function args...) body)
in Scheme.

How do you try using a rule to satisfy a query? There are two steps: (1) Unify the conclusion of the rule with the query. (2) If that succeeds (i.e., doesn't report FAILED), then qeval the body in the frame-stream it reports. This gives rise to a possibly-extended-or-shortened stream of possibly-extended frames.

But some rules have empty bodies, e.g.,
(rule (append () ?b ?b))
They could have made that a special case in APPLY-A-RULE this way (my added code in capital letters -- this is why symbols should be case-insensitive!):

(define (apply-a-rule rule query-pattern query-frame)
  (let ((clean-rule (rename-variables-in rule)))
    (let ((unify-result
	   (unify-match query-pattern
                        (conclusion clean-rule)
                        query-frame)))
      (if (eq? unify-result 'failed)
          the-empty-stream
          (IF (NULL? (RULE-BODY CLEAN-RULE))       ; no body?
                (SINGLETON-STREAM UNIFY-RESULT)    ; return unify result
                (qeval (rule-body clean-rule)
		               (singleton-stream unify-result)))))))

but instead they are avoiding having to make that test by being extra clever in the selector RULE-BODY:

(define (rule-body rule)
  (if (null? (cddr rule))
      '(always-true)
      (caddr rule)))

so that instead of seeing an empty body, APPLY-A-RULE sees (ALWAYS-TRUE).

The use of PUT here isn't about polymorphism; it's about recognizing special forms in the query language, which mostly means AND, OR, and NOT. But the PUT makes ALWAYS-TRUE also a special form, which means that instead of treating it as a query (remember, bodies are made of queries) and trying to match it against the database of assertions and rules, qeval will just call the Scheme procedure that's PUT into the table as the value associated with the key ALWAYS-TRUE. (The 'QEVAL as the second argument to PUT is just because back in Chapter 2 they defined PUT to use a 2D table, I think. Just take PUT on faith.)

So what happens when the evaluator finds a special form?

(define (qeval query frame-stream)
  (LET ((QPROC (GET (TYPE QUERY) 'QEVAL)))
    (IF QPROC
	    (QPROC (CONTENTS QUERY) FRAME-STREAM)
	    (simple-query query frame-stream))))

This is the code from the book; I haven't changed it. The capitalized part is what handles special forms. We look up the car of the query (that's what TYPE does) in the get/put table, and if we find it, call the procedure in the table value with two inputs: the special form with its keyword removed (CONTENTS is cdr) and the frame stream. They remove the keyword because in the usual case of AND, OR, or NOT, the contents part of the special form is a list of queries (AND query query ...) and so the handler can call itself recursively on the cdr of that list.

But in the case of ALWAYS-TRUE, the CONTENTS will be empty; there aren't any sub-queries to evaluate. So ALWAYS-TRUE has to be written to expect two arguments: an empty list of queries, which it ignores, and a frame stream, which it returns unchanged.


CONTENTS: Yes, your translation is okay, although PAIR? isn't the same as LIST?. It's making sure the EXPRESSION is a nonempty list. That's so that the query language won't accept (AND) etc.; those special forms require at least one sub-query (exactly one sub-query in the case of NOT). I guess also a perverse user could say (AND . FOO) to get a non-list CONTENTS.


I'm not saying you have to exactly copy the SICP code! Just that you should understand the SICP code and in particular how they use the idea of a stream of frames to make the whole thing functional.

Thanks for the explanation!

Yeah, that has been my interpretation, too. The SICP code from this section - and building on code from other sections - is kind of intimidating to me as a non-Schemer. I haven’t gotten around yet to analyzing how streams are to be integrated into my approach of the interpreter, but I will eventually . “Don’t hold your breath” :wink:

Well … perhaps you could help me out a bit more by telling if there are any ready-to-use Snap! equivalents of:

delay
force
stream-ref (would that be: item (n) of show stream?)
display-stream
cons-stream (= in front of stream ?)
the-empty-stream (= list() ??)
singleton-stream (= stream (x))
stream-flatmap (= sum (map over stream) ?)
interleave-delayed (why is it different from interleave, which in itself is supposed to handle streams?)
qeval
flatten-stream

Thx in advance!

BTW while studying SICP’s streams section I tried to implement the pairs function in Snap!, but something is not working well. If both streams are finite, show stream will raise an error if the number of elements to be shown is larger than the length of the stream of pairs:


This is the “project” (I simplified the test case as much as possible).
I’m pretty sure show stream is not causing the issue (while debugging I found it encounters a pair of two empties as item 1 of a Snap! record representing a stream), but I don’t know what is.

Solved it: the pairs function was devised for infinite streams only.

I'm not saying you have to exactly copy the SICP code! Just that you should understand the SICP code and in particular how they use the idea of a stream of frames to make the whole thing functional.

Thanks not what I was trying to do either. I just wanted to get the framework functional so I could get in and examine the innards of snap and javascript in real time. Just a meta-circular evaluator in snap would be incredible. INCREDIBLE, let alone what I could add on top.

The problem is, the claim is that "Snap! is Scheme disguised as scratch" and it's not, because it has features that 1996's scheme will never have, and certain expectations that SICP asks for have been subtly changed and there is a barrier there, and it's not even there intentional, it's the side effect of the book being nearly 30 years old and the next version of the book written for a language that's powerful for all the wrong reasons.