AI-Prolog

 view release on metacpan or  search on metacpan

lib/AI/Prolog/Article.pod  view on Meta::CPAN

 % who will give what to whom?
 gives(WHO, WHAT, WHOM) :-
     has(WHO, WHAT),
     person(WHOM),
     likes(WHO, WHOM).
 
 gives(tom,book,harry).
 
When starting the shell, you can read in a file by supplying as a name on the
command line:

 $ aiprolog gives.pro

Alternately, you can I<consult> the file from the shell:
 
 $ aiprolog

 Welcome to AI::Prolog v 0.732
 Copyright (c) 2005, Curtis "Ovid" Poe.
 AI::Prolog comes with ABSOLUTELY NO WARRANTY.  This library is free software;
 you can redistribute it and/or modify it under the same terms as Perl itself.

 Type '?' for help.

 ?- consult('gives.pro').

The second notation allows you to consult multiple files and add all of them to
the knowledge base.

After issuing the C<consult/1> command, the shell will appear to hang.  Hit
I<Enter> to continue.  We'll explain this behavior in a moment.

Now that you've loaded the program into the shell, issue the following query:

 ?- gives(X,Y,Z).

The shell should respond:

 gives(tom, book, bob) 

It will appear to hang.  It wants to know if you wish for more results or if
you are going to continue.  Typing a semicolon (;) tells Prolog that you want
it to I<resatisfy> the goal.  In other words, you're asking Prolog if there are
more solutions.  If you keep hitting the semicolon, you should see something
similar to the following:

 ?- gives(X,Y,Z).

 gives(tom, book, bob) ;

 gives(tom, book, alice) ;

 gives(alice, ring, bob) ;

 gives(tom, book, harry) ;

  No
 ?- 

That final "No" is Prolog telling you that there are no more results which
satisfy your goal (query).  (If you hit I<Enter> before Prolog prints "No", it
will print "Yes", letting you know that it found results for you.  This is
standard behavior in Prolog.)

One thing you might notice is that the last result, C<gives(tom, book, harry)>,
does not match the rule we set up for C<gives/3>.  However, we get this result
because we chose to hard-code this fact as the last line of the Prolog program.

=head2 How this works

At this point, it's worth having a bit of a digression to explain how this
works.

Many deductive systems in artificial intelligence are based on two algorithms:
backtracking and unification.  You're probably already familiar with backtracking
from regular expressions.  In fact, regular expressions are very similar to
Prolog in that you specify a pattern for your data and let the regex engine
worry about how to do the matching.

In a regular expression, if a partial match is made, the regex engine remembers
where the end of that match occurred and tries to match more of the string.  If
it fails, it backtracks to the last place a successful match was made and sees
if there are alternative matches it can try.  If that fails, it keeps
backtracking to the last successful match and repeats that process until it
either finds a match or fails completely.

Unification, described in a fit of wild hand-waving, attempts to take two
logical terms and "unify" them.  Imagine you have the following two lists:

 ( 1, 2, undef, undef,     5 )
 ( 1, 2,     3,     4, undef )

Imagine that undef means "unknown". We can unify those two lists because every
element that is known corresponds in the two lists. This leaves us with a list
of the integers one through five.

 ( 1, 2, 3, 4, 5 )

However, what happens if the last element of the first list is unknown?

 ( 1, 2, undef, undef, undef )
 ( 1, 2,     3,     4, undef )

We can still unify the two lists. In this case, we get the same five element
list, but the last item is unknown.

 ( 1, 2, 3, 4, undef )

If corresponding terms of the two lists are both bound (has a value) but not
equal, the lists will not unify:
 
 ( 1, 23, undef, undef, undef )
 ( 1,  2,     3,     4, undef )

Logic programming works by pushing these lists onto a stack and walking through
the stack and seeing if you can unify everything (sort of). But how to unify
from one item to the next? We assign names to the unknown values and see if
we can unify them. When we get to the next item in the stack, we check to see
if any named variables have been unified. If so, the engine will try to unify
them along with the other known variables.

That's a bad explanation, so here's how it works in Prolog. Imagine the

lib/AI/Prolog/Article.pod  view on Meta::CPAN

unifies with I<sally>.  The underscore, C<_>, unifies with I<tom> but since
we stated this unification is unimportant, we can ignore that.

We now have a fact which unifies with the first term in the rule, so we push
this information onto a stack.  Since there are still additional facts we can
try, we set a "choice point" in the stack telling us which fact we last tried.
If we have to backtrack to see a choice point, we move on to the next fact and
try again.

Moving on to the next term in the rule, C<male(Person)>, we know that "sally"
is unified to C<Person>, so we now try to unify C<male(sally)> with all of the
corresponding rules in the knowledge base. Since we can't, the logic engine
backs up to the last item where we could make a new choice and sees
C<parent(bill, tom)>. C<Person> gets unified with I<bill>. Then in moving to
the next rule we see that we unify with C<male(bill)>. Now, we check the first
item in the rule and see that it's C<father(Person)>. and the logic engine
reports that I<bill> is a father.

Note that we can then force the engine to backtrack and by continuously
following this procedure, we can determine who all the fathers are.

And that's how logic programming works. Simple, eh? (Well, individual items can
be lists or other rules, but you get the idea).

=head2 Executing Prolog in Perl

Getting back to Perl, how would we implement that in a Perl program?

The basic process for using C<AI::Prolog> looks something like this:

 use AI::Prolog;
 my $prolog = AI::Prolog->new($prolog_code);

Create a new C<AI::Prolog> object, passing Prolog code as the argument.  If you
prefer, you can wrap the constructor in a C<BEGIN> block:

 my $prolog;
 BEGIN {
   $prolog = AI::Prolog->new(<<'  END_PROLOG');
     % some Prolog code goes here
   END_PROLOG
 }

This is not strictly necessary, but if your Prolog code has a syntax error, it
will be a compile-time error, not a run-time error, and you'll get an error
message similar to:

 Unexpected character: (Expecting: ')'.  Got (.)) at line number 12.
 BEGIN failed--compilation aborted at test.pl line 7.

Note that the line number for "Unexpected character" is relative to the Prolog
code, not the Perl code.

After the contructor, issue your query:

 $prolog->query($some_query);

And do something with the results:

 while ( my $results = $prolog->results ) {
     print "@$results\n";
 }

Results are usually each returned as an array reference with the first argument
being the functor and subsequent arguments being the values.  If any value is a
list, it will be represented as an array reference.  We'll see more on that
later as we cover lists.

Now let's see the full program:

 #!/usr/bin/perl
 use strict;
 use warnings;
 use AI::Prolog;
 
 my $prolog;
 
 # If reading from DATA, we need a CHECK block to ensure that
 # DATA is available by the time the constructor is called
 CHECK {
   $prolog = AI::Prolog->new( do { local $/; <DATA> } );
 }
 
 $prolog->query( 'father(WHO).' );
 while ( my $results = $prolog->results ) {
     print "@$results\n";
 }
 
 __DATA__
 parent(sally, tom).
 parent(bill, tom).
 parent(tom, sue).
 parent(alice, sue).
 parent(sarah, tim).
 
 male(bill).
 male(tom).
 male(tim).
 
 father(Person) :-
     parent(Person, _),
     male(Person).

If you run this program, it will quite happily print out "father bill" and
"father tom."  In fact, if you really want to see what's going on internally,
after you issue the query you can "trace" the execution:

 $prolog->query('father(Who)');
 $prolog->trace(1); # after the query, before the results
 while ( my $result = $prolog->results ) {
   ...

Running the program again produces a lot of output, the beginning of which 
matches our description of how logic programming works internally:

 = Goals: 
         father(A)
 ==> Try:  father(A) :- 
         parent(A, B),
         male(A)
 
 = Goals: 
         parent(A, C),
         male(A)
 ==> Try:  parent(sally, tom) :- null
 
 = Goals: 
         male(sally)
 ==> Try:  male(bill) :- null
  <<== Backtrack: 
 
 = Goals: 
         male(sally)
 ==> Try:  male(tom) :- null
  <<== Backtrack: 
 
 = Goals: 
         male(sally)
 ==> Try:  male(tim) :- null
  <<== Backtrack: 

 [etc.]

Now if you really want to have fun with it, notice how you can rearrange the
clauses in the program at will and Prolog will return the same results (though
the order will likely change).  This is because when one programs in a purely
declarative style, the order of the statements no longer matters.  Subtle bugs
caused by switching two lines of code usually go away.

=head2 Prolog versus Perl

Now that you have a beginning understanding of what Prolog can do and how it
works internally, let's take a look at some of the implications of this.  By
now, you know that the following can be read as "Ovid loves Perl":

 loves(ovid, perl).

Assuming you've consulted a file with that fact in it, querying to find out
what I love is simple:

 ?- loves(ovid, WHAT).

In Perl, it's also pretty simple:

lib/AI/Prolog/Article.pod  view on Meta::CPAN

 append([HEAD|X], Y, [HEAD|Z]) :-
     append(X, Y, Z).

You'll probably want to put that in a file named I<append.pro> and in the
shell C<consult('append.pro')> to follow along with some of the examples
you're about to see.

Explaining how that works would take a bit of time, so I recommend you work it
out on your own.  Instead, I'll focus on its implications.

It looks like a lot of work to define how to append two lists.  Perl is much
simpler:

 my @Z = (@X, @Y);

In fact, that hides quite a bit of complexity for us.  Anyone who has had to
concatenate two arrays in C can appreciate the simplicity of the Perl approach.
So what would the complexity of the C<append/3> predicate gain us?  Naturally,
we can use this to append two lists.  (The following output is similar to what
one would see in regular Prolog systems.  It's been reproduced here for
clarity.)

 ?- append([1,2,3], [4,5], Z).

 Z = [1,2,3,4,5]

Of course, by now you should know that we can reuse this.  We can use it to
figure out which list to append to C<X> to create C<Z>.

 ?- append([1,2,3], Y, [1,2,3,4,5]).

 Y = [4,5]

We can also use this to figure out all combinations of two lists can be
appended together to create C<Z>.
 
 ?- append(X, Y, [1,2,3,4,5]).

 X = [], Y = [1,2,3,4,5] 
 X = [1],  Y = [1,2,3,4]
 X = [1,2],  Y = [1,2,3]
 X = [1,2,3],  Y = [1,2]
 X = [1,2,3,4],  Y = [1]
 X = [1,2,3,4,5], Y = []

And the Perl equivalent:

  use AI::Prolog;
  
  my $prolog = AI::Prolog->new(<<"END_PROLOG");
      append([], X, X).
      append([W|X], Y, [W|Z]) :- append(X, Y, Z).
  END_PROLOG
  
  my $list = $prolog->list( qw/1 2 3 4 5/ );
  
  $prolog->query("append(X,Y,[$list]).");
  while ( my $results = $prolog->results ) {
      my ( $x, $y, $z ) = @{ $results }[ 1, 2, 3 ];
      $" = ', '; # Array separator
      print "[@$x],     [@$y],     [@$z]\n";
  }

As you can see, Prolog lists will be returned to Perl as array references.
C<< $results->[0] >> is the name of the predicate, C<append>, and the next three
elements are the successive values generated by Prolog.

=head1 Problems with Prolog

This article wouldn't be complete without listing some of the issues that have
hampered Prolog.

Perhaps the most significant is the depth-first exhaustive search algorithm
used for deduction.  This is slow and due to the dynamically typed nature of
Prolog, many of the optimizations that have been applied to databases cannot be
applied to Prolog.  Many Prolog programmers, understanding how the language
works internally, use the "cut" operator, C<!> (an exclamation point), to prune
the search trees.  This leads to our next problem.

The "cut" operator is what is known as an "extra-logical" predicate.  This,
along with I/O and predicates that assert and retract facts in the database
are a subject of much controversy.  They cause issues because they frequently
cannot be backtracked over and the order of the clauses sometimes becomes
important when using them.  They can be very useful and simplify many problems,
but they are more imperative in nature than logical and can introduce subtle
bugs.

Math is also handled poorly in Prolog.  Consider the following program:

  convert(Celsius, Fahrenheit) :-
       Celsius is (Fahrenheit - 32) * 5 / 9.

You can then issue the query C<convert(Celsuis, 32)> and it will dutifully
report that the celsius value is zero.  However, C<convert(0, 32)> fails.  This
is because Prolog is not able to solve the right hand side of the equation
unless C<Fahrenheit> has a value at the time that Prolog starts examining the 
equation.  One simplistic way of getting around this limitation is with multiple
predicates and testing which argument is an unbound variable:

 convert(Celsius, Fahrenheit) :-
     var(Celsius),
     not(var(Fahrenheit)),
     Celsius is (Fahrenheit - 32) * 5 / 9.

 convert(Celsius, Fahrenheit) :-
     var(Fahrenheit),
     not(var(Celsius)),
     Fahrenheit is (Celsius * (9/5)) + 32.

This has a variety of problems, not the least of which is that it offers no
advantages over imperative programming.

ISO-Prolog does not define it, but many Prolog implementations support
constraints and these, though beyond the scope of this article, can get around
this and other issues.  They can allow "logical" math and ensure that
"impossible" search branches are never explored.  This can help to alleviate
many of the aforementioned concerns.

=head1 Conclusion

We've barely scratched the surface of what the Prolog language can do.  The



( run in 0.592 second using v1.01-cache-2.11-cpan-39bf76dae61 )