PROFESSOR: All right, well,
we've seen how the query
language works.
Now, let's talk about how
it's implemented.
You already pretty much can
guess what's going on there.
At the bottom of it, there's
a pattern matcher.
And we looked at a pattern
matcher when we did the
rule-based control language.
Just to remind you, here are
some sample patterns.
This is a pattern that will
match any list of three things
of which the first is a and the
second is c and the middle
one can be anything.
So in this little
pattern-matching syntax,
there's only one distinction
you make.
There's either literal things
or variables, and variables
begin with question mark.
So this matches any list of
three things of which the
first is a and the
second is c.
This one matches any list of
three things of which the
first is the symbol job.
The second can be anything.
And the third is a list of two
things of which the first is
the symbol computer and the
second can be anything.
And this one, this next one
matches any list of three
things, and the only difference
is, here, the third
list, the first is the symbol
computer, and then there's
some rest of the list. So this
means two elements and this
means arbitrary number.
And our language implementation
isn't even
going to have to worry about
implementing this dot because
that's automatically done
by Lisp's reader.
Remember matchers also have
some consistency in them.
This match is a list of
three things of which
the first is a.
And the second and third can be
anything, but they have to
be the same thing.
They're both called x.
And this matches a list of four
things of which the first
is the fourth and the second
is the same as the third.
And this last one matches any
list that begins with a.
The first thing is a, and the
rest can be anything.
So that's just a review of
pattern matcher syntax that
you've already seen.
And remember, that's implemented
by some procedure
called match.
And match takes a pattern and
some data and a dictionary.
And match asks the question is
there any way to match this
pattern against this data object
subject to the bindings
that are already in
this dictionary?
So, for instance, if we're going
to match the pattern x,
y, y, x against the data a, b,
b, a subject to a dictionary,
that says x equals a.
Then the matcher would say,
yes, that's consistent.
These match, and it's consistent
with what's in the
dictionary to say
that x equals a.
And the result of the match is
the extended dictionary that
says x equals a and
y equals b.
So a matcher takes in pattern
data dictionary, puts out an
extended dictionary if it
matches, or if it doesn't
match, says that it fails.
So, for example, if I use the
same pattern here, if I say
this x, y, y, x match a, b, b, a
with the dictionary y equals
a, then the matcher would
put out fail.
Well, you've already seen the
code for a pattern matcher so
I'm not going to go over it, but
it's the same thing we've
been doing before.
You saw that in the system
on rule-based control.
It's essentially the
same matcher.
In fact, I think the syntax is
a little bit simpler because
we're not worrying about
arbitrary constants and
expressions and things.
There's just variables
and constants.
OK, well, given that, what's
a primitive query?
Primitive query is going to be
a rather complicated thing.
It's going to be--
let's think about the query
job of x is d dot y.
That's a query we
might type in.
That's going to be implemented
in the system.
We'll think of it as
this little box.
Here's the primitive query.
What this little box is going
to do is take in two streams
and put out a stream.
So the shape of a primitive
query is that it's a thing
where two streams come in
and one stream goes out.
What these streams are
going to be is
down here is the database.
So we imagine all the things
in the database sort of
sitting there in a stream and
this thing sucks on them.
So what are some things that
might be in the database?
Oh, job of Alyssa is
something and some
other job is something.
So imagine all of the facts in
the database sitting there in
the stream.
That's what comes in here.
What comes in here is a stream
of dictionaries.
So one particular dictionary
might say y equals programmer.
Now, what the query does when
it gets in a dictionary from
this stream, it finds all
possible ways of matching the
query against whatever is coming
in from the database.
It looks at the query as a
pattern, matches it against
any fact from the database or
all possible ways of finding
and matching the database with
respect to this dictionary
that's coming in.
So for each fact in the
database, it calls the matcher
using the pattern, fact,
and dictionary.
And every time it gets a good
match, it puts out the
extended dictionary.
So, for example, if this one
comes in and it finds a match,
out will come a dictionary that
in this case will have y
equals programmer and
x equals something.
y is programmer, x is
something, and d
is whatever it found.
And that's all.
And, of course, it's going to
try this for every fact in the
dictionary.
So it might find lots of them.
It might find another one that
says y equals programmer and x
equals, and d equals.
So for one frame coming
in, it might put out--
for one dictionary coming in,
it might put out a lot of
dictionaries, or it might
put out none.
It might have something
that wouldn't match
like x equals FOO.
This one might not match
anything in which case nothing
will go into this stream
corresponding to this frame.
Or what you might do is put in
an empty frame, and an empty
frame says try matching
all ways--
find all possible ways of
matching the query against
something in the database
subject to no previous
restrictions.
And if you think about what that
means, that's just the
computation that's done when you
type in a query right off.
It tries to find all matches.
So a primitive query sets
up this mechanism.
And what the language does, when
you type in the query at
the top level, it takes this
mechanism, feeds in one single
empty dictionary, and then for
each thing that comes out
takes the original query and
instantiates the result with
all the different dictionaries,
producing a new
stream of instantiated
patterns here.
And that's what gets printed
on the terminal.
That's the basic mechanism
going on there.
Well, why is that
so complicated?
You probably can think of a lot
simpler ways to arrange
this match for a primitive query
rather than having all
of these streams floating
around.
And the answer is--
you probably guess already.
The answer is this thing extends
elegantly to implement
the means of combination.
So, for instance, suppose I
don't only want to do this.
I don't want to say who to be
everybody's job description.
Suppose I want to say AND the
job of x is d dot y and the
supervisor of x is z.
Now, supervisor of x is z is
going to be another primitive
query that has the same shape
to take in a stream of data
objects, a stream of initial
dictionaries, which are the
restrictions to try and use when
you match, and it's going
to put out a stream
of dictionaries.
So that's what this primitive
query looks like.
And how do I implement
the AND?
Well, it's simple.
I just hook them together.
I take the output of this one,
and I put that to the
input of that one.
And I take the dictionary
here and I fan it out.
And then you see how that's
going to work, because what's
going to happen is a frame will
now come in here, which
has a binding for x, y, and d.
And then when this one gets
it, it'll say, oh, gee,
subject to these restrictions,
which now already have values
in the dictionary for y and
x and d, it looks in the
database and says, gee, can I
find any supervisor facts?
And if it finds any, out will
come dictionaries which have
bindings for y and x
and d and z now.
And then notice that because the
frames coming in here have
these restrictions, that's the
thing that assures that when
you do the AND, this x will mean
the same thing as that x.
Because by the time something
comes floating in here, x has
a value that you have to match
against consistently.
And then you remember from the
code from the matcher, there
was something in the way the
matcher did dictionaries that
arrange consistent matches.
So there's AND.
The important point to notice
is the general shape.
Look at what happened: the AND
of two queries, say, P and Q.
Here's P and Q. The AND
of two queries, well,
it looks like this.
Each query takes in a stream
from the database, a stream of
inputs, and puts out a
stream of outputs.
And the important point to
notice is that if I draw a box
around this thing and say this
is AND of P and Q, then that
box has exactly the same
overall shape.
It's something that takes in
a stream from the database.
Here it's going to get fanned
out inside, but from the
outside you don't see that.
It takes an input stream and
puts out an output stream.
So this is AND.
And then similarly, OR
would look like this.
OR would--
although I didn't show
you examples of OR.
OR would say can I find all ways
of matching P or Q. So I
have P and Q. Each will
have their shape.
And the way OR is implemented
is I'll
take my database stream.
I'll fan it out.
I'll put one into P and one into
Q. I'll take my initial
query stream coming
in and fan it out.
So I'll look at all the answers
I might get from P and
all the answers I might get
from Q, and I'll put them
through some sort of thing that
appends them or merges
the result into one stream, and
that's what will come out.
And this whole thing from
the outside is OR.
And again, you see it has the
same overall shape when looked
at from the outside.
What's NOT?
NOT works kind of
the same way.
If I have some query P, I take
the primitive query for P.
Here, I'm going to implement NOT
P. And NOT's just going to
act as a filter.
I'll take in the database and
my original stream of
dictionaries coming in, and what
NOT P will do is it will
filter these guys.
And the way it will filter it,
it will say when I get in a
dictionary here, I'll find all
the matches, and if I find
any, I'll throw it away.
And if I don't find any matches
to something coming in
here, I'll just pass
that through, so
NOT is a pure filter.
So AND is--
think of these sort
of electoral
resistors or something.
AND is series combination and
OR is parallel combination.
And then NOT is not going
to extend any
dictionaries at all.
It's just going to filter it.
It's going to throw away
the ones for which it
finds a way to match.
And list value is sort
of the same way.
The filter's a little
more complicated.
It applies to predicate.
The major point to notice here,
and it's a major point
we've looked at before, is
this idea of closure.
The things that we build as a
means of combination have the
same overall structure
as the primitive
things that we're combining.
So the AND of two things when
looked at from the outside has
the same shape.
And what that means is that this
box here could be an AND
or an OR or a NOT or something
because it has the same shape
to interface to the
larger things.
It's the same thing that allowed
us to get complexity
in the Escher picture language
or allows you to immediately
build up these complicated
structures just out of pairs.
It's closure.
And that's the thing that
allowed me to do what by now
you took for granted when I
said, gee, there's a query
which is AND of job and salary,
and I said, oh,
there's another one,
which is AND of
job, a NOT of something.
The fact that I can do that is
a direct consequence of this
closure principle.
OK, let's break and
then we'll go on.
AUDIENCE: Where does the
dictionary come from?
PROFESSOR: The dictionary
comes initially from
what you type in.
So when you start this up, the
first thing it does is set up
this whole structure.
It puts in one empty
dictionary.
And if all you have is one
primitive query, then what
will come out is a bunch
of dictionaries with
things filled in.
The general situation that I
have here is when this is in
the middle of some nest
of combined things.
Let's look at the picture
over here.
This supervisor query gets
in some dictionary.
Where did this one come from?
This dictionary came from the
fact that I'm looking at the
output of this primitive
query.
So maybe to be very specific,
if I literally typed in just
this query at the top level,
this AND, what would actually
happen is it would build this
structure and start up this
whole thing with one
empty dictionary.
And now this one would process,
and a whole bunch of
dictionaries would come out with
x, y's and d's in them.
Run it through this one.
So now that's the input
to this one.
This one would now put
out some other stuff.
And if this itself were buried
in some larger thing, like an
OR of something, then
that would go feed
into the next one.
So you initially get only one
empty dictionary when you
start it, but as you're in the
middle of processing these
compounds things, that's where
these cascades of dictionaries
start getting generated.
AUDIENCE: Dictionaries only
come about as a result of
using the queries?
Or do they become--
do they stay someplace in space
like the database does?
Are these temporary items?
PROFESSOR: They're created
temporarily in the matcher.
Really, they're someplace
in storage.
Initially, someone creates
a thing called the empty
dictionary that gets initially
fed to this match procedure,
and then the match procedure
builds some dictionaries, and
they get passed on and on.
AUDIENCE: OK, so they'll
go way after the match?
PROFESSOR: They'll go
away when no one
needs them again, yeah.
AUDIENCE: It appears that the
AND performs some redundant
searches of the database.
If the first clause matched,
let's say, the third element
and not on the first two
elements, the second clause is
going to look at those first two
elements again, discarding
them because they don't match.
The match is already
in the dictionary.
Would it makes sense to carry
the data element from the
database along with
the dictionary?
PROFESSOR: Well, in general,
there are other ways to
arrange this search, and
there's some analysis
that you can do.
I think there's a problem in the
book, which talks about a
different way that you can
cascade AND to eliminate
various kinds of redundancies.
This one is meant to be--
was mainly meant to be very
simple so you can see how they
fit together.
But you're quite right.
There are redundancies here
that you can get rid of.
That's another reason why this
language is somewhat slow.
There are a lot smarter
things you can do.
We're just trying to show you
a very simple, in principle,
implementation.
AUDIENCE: Did you model this
language on Prolog, or did it
just come out looking
like Prolog?
PROFESSOR: Well, Jerry insulted
a whole bunch of
people yesterday, so I might
as well say that the MIT
attitude towards Prolog is
something that people did in
about 1971 and decided that it
wasn't really the right thing
and stopped.
So we modeled this on the sort
of natural way that this thing
was done in about 1971, except
at that point, we didn't do it
with streams. After we were
using it for about six months,
we discovered that it had all
these problems, some of which
I'll talk about later.
And we said, gee, Prolog must
have fixed those, and then we
found out that it didn't.
So this does about the
same thing as Prolog.
AUDIENCE: Does Prolog
use streams?
PROFESSOR: No.
In how it behaves, it behaves
a lot like Prolog.
Prolog uses a backtracking
strategy.
But the other thing that's
really good about Prolog that
makes it a usable thing is that
there's a really very,
very well-engineered compiler
technology that makes it run
fast. So although you saw the
merge spitting out these
answers very, very slowly, a
real Prolog will run very,
very fast. Because even though
it's sort of doing this, the
real work that went into
Prolog is a very, very
excellent compiler effort.
Let's take a break.
We've looked at the primitive
queries and the ways that
streams are used to implement
the means of combination: AND
and OR and NOT.
Now, let go on to the means
of abstraction.
Remember, the means of
abstraction in this
language are rules.
So z is a boss in division d
if there's some x who has a
job in division d and z is
the supervisor of x.
That's what it means for
someone to be a boss.
And in effect, if you think
about what we're doing with
relation to this, there's the
query we wrote-- the job of x
is in d and the supervisor
of x is z--
what we in effect want to do
is take this whole mess and
draw a box around it and say
this whole thing inside the
box is boss of z
in division d.
That's in effect what
we want to do.
So, for instance, if we've
done that, and we want to
check whether or not it's true
that Ben Bitdiddle is a boss
in the computer division, so if
I want to say boss of Ben
Bitdiddle in the computer
division, imagine typing that
in as query to the system, in
effect what we want to do is
set up a dictionary here, which
has z to Ben Bitdiddle
and d to computer.
Where did that dictionary
come from?
Let's look at the slide
for one second.
That dictionary came from
matching the query that said
boss of Ben Bitdiddle and
computer onto the conclusion
of the rule: boss of z and d.
So we match the query to the
conclusion of the rule.
That gives us a dictionary, and
that's the thing that we
would now like to put into
this whole big thing and
process and see if anything
comes out the other side.
If anything comes out,
it'll be true.
That's the basic idea.
So in general, the way we
implement a rule is we match
the conclusion of the rule
against something we might
want to check it's true.
That match gives us a
dictionary, and with respect
to that dictionary, we process
the body of the rule.
Well, that's really all
there is, except for
two technical points.
The first technical point is
that I might have said
something else.
I might have said who's the boss
in the computer division?
So I might say boss of who
in computer division.
And if I did that, what I would
really like to do in
effect is start up this
dictionary with a match that
sort of says, well, d
is computer and z is
whatever who is.
And our matcher won't
quite do that.
That's not quite matching
a pattern against data.
It's matching two patterns and
saying are they consistent or
not or what ways make
them consistent.
In other words, what we need
is not quite a pattern
matcher, but something
a little bit more
general called a unifier.
And a unifier is a slight
generalization
of a pattern matcher.
What a unifier does is take two
patterns and say what's
the most general thing you can
substitute for the variables
in those two patterns to make
them satisfy the pattern
simultaneously?
Let me give you an example.
If I have the pattern
two-element list, which is x
and x, so I have a two-element
list where both elements are
the same and otherwise I don't
care what they are, and I
unify that against the pattern
that says there's a
two-element list, and the first
one is a and something
in c and the second one is a
and b and z, then what the
unifier should tell me is, oh
yeah, in that dictionary, x
has to be a, b, c, and y has
to be d and z has to be c.
Those are the restrictions I'd
have to put on the values of
x, y, and z to make these two
unify, or in other words, to
make this match x and
make this match x.
The unifier should be
able to deduce that.
But the unifier may--
there are more complicated
things.
I might have said something a
little bit more complicated.
I might have said there's a list
with two elements, and
they're both the same, and
they should unify against
something of this form.
And the unifier should be able
to deduce from that.
Like that y would have to be
b. y would have to be b.
Because these two are the same,
so y's got to be b.
And v here would have to be a.
And z and w can be anything,
but they have
to be the same thing.
And x would have to be b,
followed by a, followed by
whatever w is or whatever
z is, which is the same.
So you see, the unifier somehow
has to deduce things
to unify these patterns.
So you might think there's some
kind of magic deduction
going on, but there's not.
A unifier is basically a very
simple modification of a
pattern matcher.
And if you look in the book,
you'll see something like
three or four lines of code
added to the pattern matcher
you just saw to handle
the symmetric case.
Remember, the pattern matcher
has a place where it says is
this variable matching
a constant.
And if so, it checks
in the dictionary.
There's only one other clause in
the unifier, which says is
this variable matching a
variable, in which case you go
look in the dictionary and see
if that's consistent with
what's in the dictionary.
So all the, quote, deduction
that's in this language, if
you sort of look at it, sort
of sits in the rule
applications, which, if you
look at that, sits in the
unifier, which, if you look at
that under a microscope, sits
essentially in the
pattern matcher.
There's no magic at all
going on in there.
And the, quote, deduction that
you see is just the fact that
there's this recursion,
which is unwinding the
matches bit by bit.
So it looks like this thing is
being very clever, but in
fact, it's not being
very clever at all.
There are cases where a unifier
might have to be clever.
Let me show you one more.
Suppose I want to unify a list
of two elements, x and x, with
a thing that says it's y
followed by a dot y.
Now, if you think of what that
would have to mean, it would
have to mean that x had better
be the same as y, but also x
had better be the same as a list
whose first element is a
and whose rest is y.
And if you think about what that
would have to mean, it
would have to mean that y is
the infinite list of a's.
In some sense, in order to do
that unification, I have to
solve the fixed-point equation
cons of a to y is equal to y.
And in general, I wrote
a very simple one.
Really doing unification might
have to solve an arbitrary
fixed-point equation:
f of y equals y.
And basically, you can't do that
and make the thing finite
all the time.
So how does the logic language
handle that?
The answer is it doesn't.
It just punts.
And there's a little check in
the unifier, which says, oh,
is this one of the hard cases
which when I go to match
things would involve solving
a fixed-point equation?
And in this case, I will
throw up my hands.
And if that check were not in
there, what would happen?
In most cases is that the
unifier would just go into an
infinite loop.
And other logic programming
languages work like that.
So there's really no magic.
The easy case is done
in a matcher.
The hard case is not
done at all.
And that's about the state
of this technology.
Let me just say again formally
how rules work now that I
talked about unifiers.
So the official definition is
that to apply a rule, we--
well, let's start using some
words we've used before.
Let's talk about sticking
dictionaries into these big
boxes of query things as
evaluating these large queries
relative to an environment
or a frame.
So when you think of that
dictionary, what's the
dictionary after all?
It's a bunch of meanings
for symbols.
That's what we've been calling
frames or environments.
What does it mean to do some
processing relevant to an
environment?
That's what we've been
calling evaluation.
So we can say the way that you
apply a rule is to evaluate
the rule body relative to an
environment that's formed by
unifying the rule conclusion
with the given query.
And the thing I want you to
notice is the complete formal
similarity to the net of
circular evaluator or the
substitution model.
To apply a procedure, we
evaluate the procedure body
relative to an environment
that's formed by blinding the
procedure parameters
to the arguments.
There's a complete formal
similarity here between the
rules, rule application, and
procedure application even
though these things are
very, very different.
And again, you have the
EVAL APPLY loop.
EVAL and APPLY.
So in general, I might be
processing some combined
expression that will turn into a
rule application, which will
generate some dictionaries or
frames or environments--
whatever you want to call them--
from match, which will
then be the input to some big
compound thing like this.
This has pieces of it and may
have other rule applications.
And you have essentially the
same cycle even though there's
nothing here at all that
looks like procedures.
It really has to do with the
fact you've built a language
whose means of combination
and abstraction
unwind in certain ways.
And then in general, what
happens at the very top level,
you might have rules in your
database also, so things in
this database might be rules.
There are ways to check
that things are true.
So it might come in here and
have to do a rule check.
And then there's some control
structure which says, well,
you look at some rules, and
you look at some data
elements, and you look at some
rules and data elements, and
these fan out and out and out.
So it becomes essentially
impossible to say what order
it's looking at these things in,
whether it's breadth first
or depth first or anything.
And it's even more impossible
because the actual order is
somehow buried in the delays of
the streams. So what's very
hard to tell from this is the
order in which it's scanned.
But what's true, because you're
looking at the stream
view, is that all of them
eventually get looked at.
Let me just mention one tiny
technical problem.
Suppose I tried saying boss of
y is computer, then a funny
thing would happen.
As I stuck a dictionary with
y in here, I might get--
this y is not the same as that
y, which was the other piece
of somebody's job description.
So if I really only did
literally what I said, we'd
get some variable conflict
problems. So I lied to you a
little bit.
Notice that problem is
exactly a problem
we've run into before.
It is precisely the need for
local variables in a language.
When I have the sum of
squares, that x had
better not be that x.
That's exactly the same
as this y had
better not be that y.
And we know how to solve that.
That was this whole environment
model, and we
built chains of frames and all
sorts of things like that.
There's a much more brutal
way to solve it.
In the query language, we
didn't even do that.
We did something completely
brutal.
We said every time you apply a
rule, rename consistently all
the variables in the rule to
some new unique names that
won't conflict with anything.
That's conceptually simpler,
but really brutal and not
particularly efficient.
But notice, we could have
gotten rid of all of our
environment structures if we
defined for procedures in Lisp
the same thing.
If every time we applied
a procedure and did the
substitution model we renamed
all the variables in the
procedure, then we never would
have had to worry about local
variables because they
would never arise.
OK, well, that would be
inefficient, and it's
inefficient here in the query
language, too, but we did it
to keep it simple.
Let's break for questions.
AUDIENCE: When you started this
section, you emphasized
how powerful our APPLY EVAL
model was that we could use it
for any language.
And then you say we're going to
have this language which is
so different.
It turns out that this language,
as you just pointed
out, is very much the same.
I'm wondering if you're arguing
that all languages end
up coming down to this you can
apply a rule or apply a
procedure or some
kind of apply?
PROFESSOR: I would say that
pretty much any language where
you really are building up these
means of combination and
giving them simpler names and
you're saying anything of the
sort, like here's a general kind
of expression, like how
to square something, almost
anything that you
would call a procedure.
If that's got to have
parts, you have to
unwind those parts.
You have to have some kind of
organization which says when I
look at the abstract variables
or tags or whatever you want
to call them that might stand
for particular things, you
have to keep track of that,
and that's going to be
something like an environment.
And then if you say this part
can have parts which I have to
unwind, you've got to have
something like this cycle.
And lots and lots of languages
have that character when they
sort of get put together
in this way.
This language again really is
different because there's
nothing like procedures
on the outside.
When you go below the surface
and you see the
implementation, of course, it
starts looking the same.
But from the outside, it's a
very different world view.
You're not computing functions
of inputs.
AUDIENCE: You mentioned earlier
that when you build
all of these rules in pattern
matcher and with the delayed
action of streams, you really
have no way to know in what
order things are evaluated.
PROFESSOR: Right.
AUDIENCE: And that would
indicate then that you should
only express declarative
knowledge that's true for
all-time, no-time sequence
built into it.
Otherwise, these things
get all--
PROFESSOR: Yes.
Yes.
The question is this really is
set up for doing declarative
knowledge, and as I presented
it-- and I'll show you some of
the ugly warts under this
after the break.
As I presented it, it's
just doing logic.
And in principle, if it were
logic, it wouldn't matter what
order it's getting done.
And it's quite true when you
start doing things where you
have side effects like adding
things to the database and
taking things out, and we'll see
some others, you use that
kind of control.
So, for example, contrasting
with Prolog.
Say Prolog has various features
where you really
exploit the order
of evaluation.
And people write Prolog
programs that way.
That turns out to be very
complicated in Prolog,
although if you're
an expert Prolog
programmer, you can do it.
However, here I don't think
you can do it at all.
It's very complicated because
you really are giving up
control over any prearranged
order of trying things.
AUDIENCE: Now, that would
indicate then that you have a
functional mapping.
And when you started out this
lecture, you said that we
express the declarative
knowledge which is a relation,
and we don't talk about the
inputs and the outputs.
PROFESSOR: Well, there's a
pun on functional, right?
There's function in the sense
of no side effects and not
depending on what order
is going on.
And then there's functional in
the sense of mathematical
function, which means
input and output.
And it's just that pun that
you're making, I think.
AUDIENCE: I'm a little unclear
on what you're doing with
these two statements, the
two boss statements.
Is the first one building up
the database and the second
one a query or--
PROFESSOR: OK, I'm sorry.
What I meant here, if I
type something like
this in as a query--
I should have given an example
way at the very beginning.
If I type in job, Ben Bitdiddle,
computer wizard,
what the processing will do is
if it finds a match, it'll
find a match to that exact
thing, and it'll type out a
job, Ben Bitdiddle,
computer wizard.
If it doesn't find a match,
it won't find anything.
So what I should have said is
the way you use the query
language to check whether
something is true, remember,
that's one of the things
you want to do in logic
programming, is you type in
your query and either that
comes out or it doesn't.
So what I was trying to
illustrate here, I wanted to
start with a very simple
example before
talking about unifiers.
So what I should have said, if I
just wanted to check whether
this is true, I could type that
in and see if anything
came out
AUDIENCE: And then
the second one--
PROFESSOR: The second one
would be a real query.
AUDIENCE: A real query, yeah.
PROFESSOR: What would come out,
see, it would go in here
say with FOO, and in would go
frame that says z is bound to
who and d is bound
to computer.
And this will pass through, and
then by the time it got
out of here, who would
pick up a binding.
AUDIENCE: On the unifying thing
there, I still am not
sure what happens
with who and z.
If the unifying--
the rule here says--
OK, so you say that you can't
make question mark equal to
question mark who.
PROFESSOR: Right.
That's what the matcher
can't do.
But what this will mean to a
unifier is that there's an
environment with three
variables.
d here is computer.
z is whatever who is.
So if later on in the matcher
routine it said, for example,
who has to be 3, then when I
looked up in the dictionary,
it will say, oh, z is 3 because
it's the same as who.
And that's in some sense the
only thing you need to do to
extend the unifier
to a matcher.
AUDIENCE: OK, because it looked
like when you were
telling how to unify it, it
looked like you would put the
things together in such a way
that you'd actually solve and
have a value for both of them.
And what it looks like now is
that you're actually pass a
dictionary with two variables
and the variables are linked.
PROFESSOR: Right.
It only looks like you're
solving for both of them
because you're sort of looking
at the whole solution at once.
If you sort of watch the thing
getting built up recursively,
it's merely this.
AUDIENCE: OK, so you
do pass off that
dictionary with two variables?
PROFESSOR: That's right.
AUDIENCE: And link?
PROFESSOR: Right.
It just looks like an
ordinary dictionary.
AUDIENCE: When you're talking
about the unifier, is it that
there are some cases or some
points that you are not able
to use by them?
PROFESSOR: Right.
AUDIENCE: Can you just by
building the rules or writing
the forms know in advance if
you are going to be able to
solve to get the unification
or not?
Can you add some properties
either to the rules itself or
to the formula that you're
writing so that you avoid the
problem of not finding
unification?
PROFESSOR: I mean, you can
agree, I think, to write in a
fairly restricted way where
you won't run into it.
See, because what
you're getting--
see, the place where you get
into problems is when you--
well, again, you're trying to
match things like that against
things where these
have structure,
where a, y, b, y something.
So this is the kind of
place where you're
going to get into trouble.
AUDIENCE: So you can do
that syntactically?
PROFESSOR: So you can kind of
watch your rules in the kinds
of things that your writing.
AUDIENCE: So that's the problem
that the builder of
the database has to
be concerned?
PROFESSOR: That's a problem.
It's a problem either-- not
quite the builder of the
database, the person who is
expressing the rules, or the
builder of the database.
What the unifier actually does
is you can check at the next
level down when you actually get
to the unifier and you'll
see in the code where it looks
up in the dictionary.
If it sort of says what
does y have to be?
Oh, does y have to be something
that contains a y as
its expression?
At that point, the unifier and
say, oh my God, I'm trying to
solve a fixed-point equation.
I'll give it up here.
AUDIENCE: You make the
distinction between the rules
in the database.
Are the rules added
to the database?
PROFESSOR: Yes.
Yes, I should have said that.
One way to think about rules
is that they're just other
things in the database.
So if you want to check the
things that have to be checked
in the database, they're kind
of virtual facts that are in
the database.
AUDIENCE: But in that
explanation, you made the
differentiation between database
and the rules itself.
PROFESSOR: Yeah, I probably
should not have done that.
The only reason to do that
is in terms of the
implementation.
When you look at the
implementation, there's a part
which says check either
primitive assertions in the
database or check rules.
And then the real reason why
you can't tell what order
things are going to come out
in and is that the rules
database and the data database
sort of get merged in a kind
of delayed evaluation way.
And so that's what makes the
order very complicated.
OK, let's break.
We've just seen how the
logic language works
and how rules work.
Now, let's turn to a more
profound question.
What do these things mean?
That brings us to the subtlest,
most devious part of
this whole query language
business, and that is that
it's not quite what
it seems to be.
AND and OR and NOT and the
logical implication of rules
are not really the AND and
OR and NOT and logical
implication of logic.
Let me give you an
example of that.
Certainly, if we have two things
in logic, it ought to
be the case that AND of P and Q
is the same as AND of Q and
P and that OR of P and Q is the
same as OR of Q and P. But
let's look here.
Here's an example.
Let's talk about somebody
outranking somebody else in
our little database
organization.
We'll say s is outranked by b or
if either the supervisor of
this is b or there's some middle
manager here, that
supervisor of s is m, and
m is outranked by b.
So there's one way to define
rule outranked by.
Or we can write exactly the
same thing, except at the
bottom here, we reversed the
order of these two clauses.
And certainly if this were
logic, those ought to mean the
same thing.
However, in our particular
implementation, if you say
something like who's outranked
by Ben Bitdiddle, what you'll
find is that this rule will
work perfectly well and
generate answers, whereas
this rule will go
into an infinite loop.
And the reason for that is that
this will come in and
say, oh, who's outranked
by Ben Bitdiddle?
Find an s which is outranked
by b, where b is Ben
Bitdiddle, which is going to
happen in it a subproblem.
Oh gee, find an m such as m is
outranked by Ben Bitdiddle
with no restrictions on m.
So this will say in order to
solve this problem, I solve
exactly the same problem.
And then after I've solved
that, I'll check for a
supervisory relationship.
Whereas this one won't get into
that, because before it
tries to find this outranked by,
it'll already have had a
restriction on m here.
So these two things which ought
to mean the same, in
fact, one goes into
an infinite loop.
One does not.
That's a very extreme case of
a general thing that you'll
find in logic programming that
if you start changing the
order of the things in the
ANDs or ORs, you'll find
tremendous differences
in efficiency.
And we just saw an infinitely
big difference in efficiency
and an infinite loop.
And there are similar things
having to do with the order in
which you enter rules.
The order in which it happens
to look at rules in the
database may vastly change the
efficiency with which it gets
out answers or, in fact, send
it into an infinite loop for
some orderings.
And this whole thing has to do
with the fact that you're
checking these rules
in some order.
And some rules may lead to
really long paths of
implication.
Others might not.
And you don't know a priori
which ones are good and which
ones are bad.
And there's a whole bunch of
research having to do with
that, mostly having to do with
thinking about making parallel
implementations of logic
programming languages.
And in some sense, what you'd
like to do is check all rules
in parallel and whichever
ones get answers,
you bubble them up.
And if some go down
infinite deductive
changed, well, you just--
you know, memory is cheap and
processors are cheap, and you
just let them buzz for as
for as long as you want.
There's a deeper problem,
though, in comparing this
logic language to real logic.
The example I just showed you,
it went into an infinite loop
maybe, but at least it didn't
give the wrong answer.
There's an actual deeper
problem when we start
comparing, seriously comparing
this logic language with real
classical logic.
So let's sort of review
real classical logic.
All humans are mortal.
That's pretty classical logic.
Then maybe we'll continue
in the very
best classical tradition.
We'll say all--
let's make it really
classical.
All Greeks are human, which
has the syllogism that
Socrates is a Greek.
And then what do
you write here?
I think three dots,
classical logic.
Therefore, then the syllogism,
Socrates is mortal.
So there's some real honest
classical logic.
Let's compare that with our
classical logic database.
So here's a classical
logic database.
Socrates is a Greek.
Plato is a Greek.
Zeus is a Greek, and
Zeus is a god.
And all humans are mortal.
To show that something is
mortal, it's enough to show
that it's human.
All humans are fallible.
And all Greeks are humans
is not quite right.
This says that all Greeks who
are not gods are human.
So to show something's human,
it's enough to show it's a
Greek and not a god.
And the address of any Greek
god is Mount Olympus.
So there's a little classical
logic database.
And indeed, that would
work fairly well.
If we type that in and say is
Socrates mortal or Socrates
fallible or mortal?
It'll say yes.
Is Plato mortal and fallible.
It'll say yes.
If we say is Zeus mortal?
It won't find anything.
And it'll work perfectly well.
However, suppose we want
to extend this.
Let's define what it means for
someone to be a perfect being.
Let's say rule: a
perfect being.
And I think this is right.
If you're up on your medieval
scholastic philosophy, I
believe that perfect beings
are ones who were neither
mortal nor fallible.
AND NOT mortal x,
NOT fallible x.
So we'll define this system
to teach it what a
perfect being is.
And now what we're going to do
is he ask for the address of
all the perfect beings.
AND the address of x is
y and x is perfect.
And so what we're generating
here is the world's most
exclusive mailing list. For the
address of all the perfect
things, we might have
typed this in.
Or we might type in this.
We'll say AND perfect of x and
the address of x is y.
Well, suppose we type all that
in and we try this query.
This query is going to
give us an answer.
This query will say, yeah,
Mount Olympus.
This query, in fact, is going
to give us nothing.
It will say no addresses
of perfect beings.
Now, why is that?
Why is there a difference?
This is not an infinite
loop question.
This is a different
answer question.
The reason is that if you
remember the implementation of
NOT, NOT acted as a filter.
NOT said I'm going to take some
possible dictionaries,
some possible frames, some
possible answers, and filter
out the ones that happened to
satisfy some condition, and
that's how I implement NOT.
If you think about what's going
on here, I'll build this
query box where the output of an
address piece gets fed into
a perfect piece.
What will happen is the address
piece will set up some
things of everyone whose
address I know.
Those will get filtered by the
NOTs inside perfect here.
So it will throw out the ones
which happened to be either
mortal or fallible.
In the other order what happens
is I set this up,
started up with an
empty frame.
The perfect in here doesn't find
anything for the NOTs to
filter, so nothing comes
out here at all.
And there's sort of nothing
there that gets fed into the
address thing.
So here, I don't
get an answer.
And again, the reason
for that is NOT
isn't generating anything.
NOT's only throwing
out things.
And if I never started up with
anything, there's nothing for
it to throw out.
So out of this thing, I
get the wrong answer.
How can you fix that?
Well, there are ways
to fix that.
So you might say, well,
that's sort of stupid.
Why are you just doing
all your NOT
stuff at the beginning?
The right way to implement NOT
is to realize that when you
have conditions like NOT, you
should generate all your
answers first, and then with
each of these dictionaries
pass along until at the very
end I'll do filtering.
And there are implementations
of logic languages that work
like that that solve this
particular problem.
However, there's a more profound
problem, which is
which one of these is
the right answer?
Is it Mount Olympus
or is it nothing?
So you might say it's Mount
Olympus, because after all,
Zeus is in that database,
and Zeus was
neither mortal nor fallible.
So you might say Zeus wants to
satisfy NOT mortal Zeus or NOT
fallible Zeus.
But let's actually look
at that database.
Let's look at it.
There's no way--
how does it know that Zeus
is not fallible?
There's nothing in
there about that.
What's in there is that
humans are fallible.
How does it know that
Zeus is not mortal?
There's nothing in
there about that.
It just said I don't have
any rule, which--
the only way I can deduce
something's mortal is if it's
human, and that's all it really
knows about mortal.
And in fact, if you remember
your classical mythology, you
know that the Greek gods were
not mortal but fallible.
So the answer is not
in the rules there.
See, why does it deduce that?
See, Socrates would certainly
not have made
this error of logic.
What NOT needs in this
language is not NOT.
It's not the NOT of logic.
What NOT needs in this language
is not deducible from
things in the database as
opposed to not true.
That's a very big difference.
Subtle, but big.
So, in fact, this is perfectly
happy to say not anything that
it doesn't know about.
So if you ask it is it not
true that Zeus likes
chocolate ice cream?
It will say sure,
it's not true.
Or anything else or anything
it doesn't know about.
NOT means not deducible from
the things you've told me.
In a world where you're
identifying not deducible
with, in fact, not true, this
is called the closed world
assumption.
The closed world assumption.
Anything that I cannot deduce
from what I know
is not true, right?
If I don't know anything about
x, the x isn't true.
That's very dangerous.
From a logical point of view,
first of all, it doesn't
really makes sense.
Because if I don't know anything
about x, I'm willing
to say not x.
But am I willing to
say not not x?
Well, sure, I don't
know anything
about that either maybe.
So not not x is not necessarily
the same as x and
so on and so on and so on, so
there's some sort of funny
bias in there.
So that's sort of funny.
The second thing, if you start
building up real reasoning
programs based on this, think
how dangerous that is.
You're saying I know I'm in a
position to deduce everything
true that's relevant
to this problem.
I'm reasoning, and built into my
reasoning mechanism is the
assumption that anything that I
don't know can't possibly be
relevant to this
problem, right?
There are a lot of big
organizations that work like
that, right?
Most corporate marketing
divisions work like that.
You know the consequences
to that.
So it's very dangerous to start
really typing in these
big logical implication systems
and going on what they
say, because they have
this really limiting
assumption built in.
So you have to be very, very
careful about that.
And that's a deep problem.
That's not a problem about we
can make a little bit cleverer
implementation and do the
filters and organize the
infinite loops to make
them go away.
It's a different kind
of problem.
It's a different semantics.
So I think to wrap this up, it's
fair to say that logic
programming I think is a
terrifically exciting idea,
the idea that you can bridge
this gap from the imperative
to the declarative, that you
can start talking about
relations and really get
tremendous power by going
above the abstraction
of what's my input
and what's my output.
And linked to logic, the problem
is it's a goal that I
think has yet to be realized.
And probably one of the very
most interesting research
questions going on now in
languages is how do you
somehow make a real
logic language?
And secondly, how do you bridge
the gap from this world
of logic and relations to the
worlds of more traditional
languages and somehow combine
the power of both.
OK, let's break.
AUDIENCE: Couldn't you solve
that last problem by having
the extra rules that imply it?
The problem here is you have the
definition of something,
but you don't have the
definition of its opposite.
If you include in the database
something that says something
implies mortal x, something
else implies not mortal x,
haven't you basically
solved the problem?
PROFESSOR: But the issue
is do you put a finite
number of those in?
AUDIENCE: If things are
specified always in pairs--
PROFESSOR: But the impression
is then what do
you do about deduction?
You can't specify NOTs.
But the problem is, in a big
system, it turns out that
might not be a finite
number of things.
There are also sort
of two issues.
Partly it might not be finite.
Partly it might be that's
not what you want.
So a good example would be
suppose I want to do
connectivity.
I want a reason about
connectivity.
And I'm going to tell you
there's four things: a and b
and c and d.
And I'll tell you a is connected
to b and c's
connected to d.
And now I'll tell you
is a connected to d?
That's the question.
There's an example where I would
like something like the
closed world assumption.
That's a tiny toy, but a lot of
times, I want to be able to
say something like anything
that I haven't told you,
assume is not true.
So it's not as simple as you
only want to put in explicit
NOTs all over the place.
It's that sometimes it
really isn't clear
what you even want.
That having to specify both
everything and not everything
is too precise, and then you get
down into problems there.
But there are a lot of
approaches that explicitly put
in NOTs and reason
based on that.
So it's a very good idea.
It's just that then it starts
becoming a little cumbersome
in the very large problems
you'd like to use.
AUDIENCE: I'm not sure how
directly related to the
argument this is, but one of
your points was that one of
the dangers of the closed rule
is you never really know all
the things that are there.
You never really know
all the parts to it.
Isn't that a major problem
with any programming?
I always write programs where I
assume that I've got all the
cases, and so I check for them
all or whatever, and somewhere
down the road, I find
out that I didn't
check for one of them.
PROFESSOR: Well, sure,
it's true.
But the problem here is it's
that assumption which is the
thing that you're making if you
believe you're identifying
this with logic.
So you're quite right.
It's a situation you're
never in.
The problem is if you're
starting to believe that what
this is doing is logic and you
look at the rules you write
down and say what can I deduce
from them, you have to be very
careful to remember that NOT
means something else.
And it means something else
based on an assumption which
is probably not true.
AUDIENCE: Do I understand you
correctly that you cannot fix
this problem without killing
off all possibilities of
inference through
altering NOT?
PROFESSOR: No, that's
not quite right.
There are other--
there are ways to do logic
with real NOTs.
There are actually
ways to do that.
But they're very inefficient
as far as anybody knows.
And they're much more--
the, quote, inference in here is
built into this unifier and
this pattern matching
unification algorithm.
There are ways to automate
real logical reasoning.
But it's not based on that,
and logic programming
languages don't tend to do
that because it's very
inefficient as far
as anybody knows.
All right, thank you.
