package AI::Prolog; $REVISION = '$Id: Prolog.pm,v 1.2 2005/01/23 20:23:14 ovid Exp $'; $VERSION = '0.03'; use Exporter::Tidy shortcuts => [qw/Parser Term Engine/]; use aliased 'AI::Prolog::Parser'; use aliased 'AI::Prolog::Term'; use aliased 'AI::Prolog::Engine'; 1; __END__ =head1 NAME AI::Prolog - Perl extension for logic programming. =head1 SYNOPSIS use AI::Prolog ':all'; my $database = Parser->consult(<<'END_PROLOG'); append([], X, X). append([W|X],Y,[W|Z]) :- append(X,Y,Z). END_PROLOG my $query = Term->new("append(X,Y,[a,b,c,d])."); my $engine = Engine->new($query,$database); print "Which lists append to form a known list?\n"; print "'append(X,Y,[a,b,c,d]).'\n"; while (my $result = $engine->results) { print "$result\n"; } $query = Term->new("append(X, [c,d], [a,b,c,d])."); $engine->query($query); # don't reload the engine if you can help it while (my $result = $engine->results) { print "$result\n"; } =head1 ABSTRACT AI::Prolog is merely a convenient wrapper for a pure Perl Prolog compiler. Regrettably, at the current time, this requires you to know Prolog. That will change in the future. =head1 DESCRIPTION A pure Perl predicate logic engine. In predicate logic, instead of telling the computer how to do something, you tell the computer what something is and let it figure out how to do it. Conceptually this is similar to regexes: my @matches = $string =~ /XX(YY?)ZZ/g If the string contains data that will satisfy the pattern, C<@matches> will contain a bunch of "YY" and "Y"s. Note that you're not telling the program how to find those matches. Instead, you supply it with a pattern and it goes off and does its thing. To learn more about Prolog, see Roman BartE<225>k's "Guide to Prolog Programming." http://kti.ms.mff.cuni.cz/~bartak/prolog/index.html. Amongst other things, his course uses the Java applet that C was ported from, so his examples will generally work with this module. Fortunately, Prolog is fairly easy to learn. Mastering it, on the other hand, can be a challenge. =head1 USING AI::Prolog There are three basic steps to using C. =over 4 =item Create the Prolog program. =item Create a query. =item Run the query. =back For quick examples of how that works, see the C directory with this distribution. Feel free to contribute more. =head2 Creating a Prolog program This module is actually remarkable easy to use. To create a Prolog program, you simply Cconsult($prolog_code)>. Note that in Prolog, the result is what is known as a database. my $database = Parser->consult(<<'END_PROLOG'); steals(PERP, STUFF) :- thief(PERP), valuable(STUFF), owns(VICTIM,STUFF), not(knows(PERP,VICTIM)). thief(badguy). valuable(gold). valuable(rubies). owns(merlyn,gold). owns(ovid,rubies). knows(badguy,merlyn). END_PROLOG =head2 Creating a query To create a query for the database, use C. my $query = Term->new("steals(badguy,X)."); =head2 Running a query my $engine = Engine->new($query, $database); while (my $result = $engine->results) { print "$result\n"; } =head1 RATIONALE You can skip this if you already know logic programming. In Perl, generally you can append one list to another with this: my @Z = (@X, @Y); In Prolog, it looks like this: append([], X, X). append([W|X],Y,[W|Z]) :- append(X,Y,Z). (There's actually often something called a "cut" after the first definition, but we'll keep this simple.) What the above code says is "appending an empty list to a non-empty list yields the non-empty list." This is a boundary condition. Logic programs frequently require a careful analysis of boundary conditions to avoid infinite loops (similar to how recursive functions in Perl generally should have a terminating condition defined in them.) The second line is where the bulk of the work gets done. In Prolog, to identify the head (first element) of a list and its tail (all elements except the first), we use the syntax [head|tail]. Since ":-" is read as "if" in Prolog, what this says if we want to concatenate (a,b,c) and (d,e,f): =over 4 =item * Given a list with a head of W and a tail of X: @list1 = qw/a b c/; (qw/a/ is W, the head, and qw/b c/ is X, the tail) =item * If it's appended to list Y: @Y = qw/d e f/; =item * We get a list with a head of W and a tail of Z: @list2 = qw/a b c d e f/; =item * Only if X appended to Y forms Z: X is qw/b c/. Y is qw/d e f/. Z is qw/b c d e f/. =back But how do we know if X appended to Y forms Z? Well, it's recursive. You see, the head of X is 'b' and it's tail is 'c'. Let's follow the transformations: append([a,b,c],[d,e,f],[a,b,c,d,e,f]) if append([b,c],[d,e,f],[b,c,d,e,f]) if append([c],[d,e,f],[c,d,e,f]) if append([],[d,e,f],[d,e,f]) As you can see, the last line matches our boundary condition, so the program can determine what the concatenation is. Now that may seem confusing at first, but so was the Schwartzian transform when many of us encountered it. After a while, it becomes natural. Sit down and work it out and you'll see what's going one. So what does this give us? Well, we can now append lists X and Y to form Z: append([a], [b,c,d], Z). Given Y and Z, we can infer X. append(X, [b,c,d], [a,b,c,d]). And finally, given Z, we can infer all X and Y that combine to form Z (again, like the regular expression, only easier). append(X,Y,[a,b,c,d]). Note that you get all of that from one definition of how to append two lists. You also don't have to tell the program how to do it. It just figures it out for you. Translating all of this into C looks like this: use AI::Prolog qw/:all/; my $database = Parser->consult(<<'END_PROLOG'); append([], X, X). append([W|X],Y,[W|Z]) :- append(X,Y,Z). END_PROLOG my $query = Term->new("append(X,Y,[a,b,c,d])."); my $engine = Engine->new($query,$database); while (my $result = $engine->results) { print "$result\n"; } =head1 GRAMMAR The language is given by the following simple grammar (pulled directly from Dr. Winikoff's page): Program ::= Rule | Rule Program Query ::= Term Rule ::= Term . | Term :- Terms . Terms ::= Term | Term , Terms Term ::= Number | Variable | AtomName | AtomName(Terms) | [] | [Terms] | [Terms | Term] | print(Term) | nl | eq(Term , Term) | if(Term , Term , Term) | or(Term , Term ) | not(Term) | call(Term) | once(Term) Number ::= Digit | Digit Number Digit ::= 0 | ... | 9 AtomName ::= LowerCase NameChars Variable ::= UpperCase NameChars NameChars ::= NameChar | NameChar NameChars NameChar ::= a | ... | z | A | ... | Z | Digit Comments begin with a C<%> and terminate at the end of the line or begin with C and terminate with C<*/>. Also, this will not work: p(X) :- X. /* does not work */ Use this instead: p(X) :- call(X). =head2 Built-ins. =over 4 =item * C. Invokes C as a goal. =item * C Succeeds if C and C are equal. =item * C If C succeeds as a goal, try C as a goal. Otherwise, try C. thief(badguy). steals(PERP, X) :- if(thief(PERP), eq(X,rubies), eq(X,nothing)). =item * C Prints a newline. =item * C. Succeeds if C cannot be proven. This is not negation as we're used to seeing it in procedural languages. =item * C Stop solving for C if C succeeds. Defined as: once(X) :- X, !; =item * C Succeeds as a goal if either C or C succeeds. =item * C. Prints the current Term. =back =head1 LIMITATIONS These are known limitations that I am not terribly inclined to fix. See the TODO list for those I am inclined to fix. IF -> THEN; ELSE not allowed. Use C instead. = and \= not available. Use C and C instead. =head1 TODO There are many things on this list. The core functionality is there, but I do want you to be aware of what's coming. =over * =item * Anonymous variables steals(badguy, _). Currently, that doesn't work. Make up a variable name and only use it once. =item * Improve printing. print(X). Currently, you can print variables, but to print strings is cumbersome: print(some_data), print(X). This will print some_data and the current value of X (or an internal representation of its state if its not bound to a variable.) There is no way to print a space. =item * Math. Math is hard. So we don't have it, but we will and we'll probably have it in the form of system predicates that call out to Perl. We can define most of what we need logically, but it's very slow and complicated, so we won't do that. =item * Database access No, we're not talking DBI. We're talking about C, C and C. We don't have that yet. =item * Add cut. Currently, we can use C to simulate it. Think of this predicate as being equivalent to: once(X) :- X, !; Frankly, I'm not overly worried about this but feel free to convince me I'm wrong (preferably with a patch :). =back Additionally, one can "trace" the program execution by setting this property to a true value before fetching engine results: Engine->trace(1); while (my $result = $engine->results) { print "$result\n"; } This sends trace information to C and allows you to see how the engine is trying to satify your goals. Naturally, this slows things down quite a bit. =head1 BUGS I'm sure there are bugs. I don't know of any right now. Let me know if (when) you find them. See the TODO list before that, though. =head1 EXPORT None by default. However, for convenience, it's recommended that you choose ":all" functions to be exported. That will provide you with C, C, and C. If you choose not to export the functions, you may use the fully qualified package names instead: use AI::Prolog; my $database = AI::Prolog::Parser->consult(<<'END_PROLOG'); append([], X, X). append([W|X],Y,[W|Z]) :- append(X,Y,Z). END_PROLOG my $query = AI::Prolog::Term->new("append(X,Y,[a,b,c,d])."); my $engine = AI::Prolog::Engine->new($query,$database); while (my $result = $engine->results) { print "$result\n"; } I have no idea why you would want to do this :) =head1 SEE ALSO W-Prolog: L Michael BartE<225>k's online guide to programming Prolog: L =head1 AUTHOR Curtis "Ovid" Poe, Emoc tod oohay ta eop_divo_sitrucE Reverse the name to email me. This work is based on W-Prolog, http://goanna.cs.rmit.edu.au/~winikoff/wp/, by Dr. Michael Winikoff. Many thanks to Dr. Winikoff for granting me permission to port this. =head1 COPYRIGHT AND LICENSE Copyright 2005 by Curtis "Ovid" Poe This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut