联系方式

  • QQ:99515681
  • 邮箱:99515681@qq.com
  • 工作时间:8:00-23:00
  • 微信:codinghelp

您当前位置:首页 >> Java编程Java编程

日期:2024-04-20 08:46


Prolog Notes

I dug out a few text files of explanations that would be useful for our project, from how to work with Linux for those who are not familiar with it to how to implement backtracking recursively and how to debug SML code. I compiled them into this document for material related to the Prolog Interpreter and a second document with ML Notes.

Unix/Linux

There always are at least a couple people in class who don’t know Linux. There are a ton of Unix/Linux primers on the web, as well as courses on Linux. Here’s a link that looks useful at first glance:

http://www.ks.uiuc.edu/Training/Tutorials/Reference/unixprimer.html

If you are reasonably familiar with Unix and want to use some of those tools on a PC, I suggest installing Cygwin from

http://www.cygwin.com/

It implements a POSIX library on top of Win32 and then contains ports for just about any Unix/Linux tool out there to that library. With Cygwin, Windows becomes much more usable as a development platform.

For connecting to classes.csc.lsu.edu from your PC, if you don’t install Cygwin, I suggest PuTTY:

http://www.chiark.greenend.org.uk/~sgtatham/putty/

You can simply download the executable. There is also PSCP, a remote file copy program. If you use Cygwin, you can use ssh and scp, respectively.

SML/NJ

For running sml and pml on a PC, you need to install SML/NJ from http://www.smlnj.org/

first as we discussed in class. The version installed on classes.csc is 110.79. The version I used on my laptop for creating the Windows heap image is 110.71, but the latest version should work, too. The batch file for running SML will be installed in

    C:\Program Files\SMLNJ\bin\sml.bat

Then you need to download and unpack the skeleton code from Moodle. After unpacking it, you’ll find a file

1

    Prolog\.heap\pml.x86-win32

Copy the file sml.bat from the SML installation, into pml.bat and change the path of the heap image so it loads pml.x86-win32. When you then run pml.bat, you’ll be starting the SML version with the Prolog interpreter preloaded.

Code Structure for Project

For getting started with the Prolog code, I suggest reading the files Syntax.sig and Syntax.sml and then reading the code from Stansifer’s ML primer.

Here’s a short description of the files I made available:

Main.sml

Loads all the pieces of the Prolog interpreter into an empty sml system. After you start sml, you can load everything by typing ‘use "Main.sml";’ at the prompt. If the boolean EXPORT_ML is true, it generates a heap image.

pml

Shell script to start sml with the Prolog parser and the reference implementa-

tion pre-loaded. The heap image starts the Prolog top-level loop.

Prolog.lex

Lexical analyzer written in ML-Lex.

    Prolog.lex.sml

Output generated by ML-Lex.

Prolog.grm

Parser written in ML-Yacc.

    Prolog.grm.sig

Parser interface generated by ML-Yacc.

    Prolog.grm.sml

Parser implementation generated by ML-Yacc.

    Prolog.grm.desc

Human-readable form of parser table generated by ML-Yacc.

Parser.sml

Glue code to combine the lexical analyzer and the parser.

Makefile

Makefile for building scanner, parser, and heap image.

Syntax.sig

Interface for parse tree data structure.

2

Syntax.sml

Implementation of parse tree data structure and some simple helper functions.

    TopLevel.sml

Top-level loop, initializes Prolog database, reads and interprets a Prolog clause in a loop until EOF.

testsuite/flintstones.*, testsuite/monty-python.*

Sample Prolog programs. The *.pro files are how you could type them in at the Prolog prompt. The *.sml files contain calls to the underlying interpreter without using the top-level, so you can run them from the shell prompt or read them in with ‘use "testsuite/monty-python.sml";’.

You don’t need to know the details of the scanner and parser at all. The only code you need to understand are the interface to the parse trees (Syntax.sig) and how your interpreter is called from the top level (TopLevel.sml). The tree data structure consists of the two datatypes Term and HornClause. The variable db is the Prolog database, and the functions declared in Syntax.sig are useful for initializing the database, asserting an assertion, and for printing. The TopLevel is the function prolog (), which calls your interpreter by passing a clause to the function Prolog (). It’s this function Prolog (), that you need to implement. A skeleton of this function is in Prolog.sml. The rest of your code will go in this file above the function Prolog ().

Your function Prolog must be of type HornClause -> unit. The structure of this function is as follows (without error checks for malformed Horn clauses):

    fun Prolog (Headed (Fun ("init", nil), nil)) =

          (* Initialize database with built-in predicate init. *)

          (db := []; OutLine "Database erased.")

      | Prolog (x as (Headed _)) =

          (* Add the fact or rule x to the database. *)

          (db := !db @ [x]; OutLine("assert:  " ^ PrintClause x))

      | Prolog (Headless y) =

          (* Query the database. *)

          (OutLine ("query:  " ^ PrintClause (Headless y));

           OutQuery (y, !db)

          )

where OutQuery would take the list of terms from the query and the contents of the database, runs the search, and prints the results. This code contains all the direct accesses to the database you need. In the rest of your code, you shouldn’t use any assignments.

The Prolog interpreter consists of three larger pieces of code: unification, resolution (i.e., database search), and printing the results. The first things to implement are the substitution data structure and unification. For this part you have most of the code structure given from Stansifer’s ML Primer.

The best way to start would be to type in that code, experiment with it, change the data structure to our term representation, and then find out what bug there is in the code. Stansifer uses ML modules (structures, functors, and signatures) to make his unification implementation generic, so it can be used with any term data structure. That’s overkill for our purposes, so it’s best to rip that

3

out and only implement the functions. Wherever Stansifer uses Terms.TV that corresponds to Var in our Term datatype, and where he uses Terms.TO that corresponds to Fun.

Substitution Implementation

Here is the code for substitutions similar to Stansifer’s code with some explanation and examples.

To deal with renaming of variables, your substitutions should take a pair of variable name and instance number as argument (i.e., what’s inside a Var in our Term datatype) and return a Term, i.e.,

    type Substitution = string * int -> Term

I.e., instead of passing, say Var("x",0") as argument to a substitution, you only pass ("x",0). This is just for type-checking purposes. If you’d let a substitution take a Var as argument, the type inferencer will complain that the match is not exhaustive, that you’s also need to define a case for Fun.

You don’t need this type declaration, though. You can let ML figure out the type for you. The empty substitution simply substitutes each variable for itself:

fun empty x = Var x

or, if you want to declare that it’s of type Substitution, you could write

val empty : Substitution = fn x => Var x

These definitions of empty are equivalent. If you use the first definition, SML will tell you that the

type is string * int -> Term.

Other substitutions are constructed using the functions upd and comp below. The function value applies a substitution to a term:

    fun value S (Var v)         = S v

      | value S (Fun (f, args)) = Fun (f, map (value S) args)

Suppose, s is the substitution [X -> t'] then (value s t) substitutes every occurrence of the variable X in t for t'.

Now we can define the composition of two substitutions for convenience:

    fun comp (S, R) v = value S (R v)

or

So comp takes a pair of substitutions S and R as arguments and returns a new substitution (fn v => value S (R v)). E.g., if R is the substitution [X -> t1, Y -> Z] and S is the substitution [X -> t2, Z -> t3], then comp (S,R) is the substitution [X -> t1, Y -> t3, Z -> t3].

BTW, for testing you can easily define substitutions by hand. E.g., assuming that the variables t1, t2, and t3 would be some terms, the above substitutions S and R are:

    fun S v = if v = ("X",0) then t2 else if v = ("Z",0) then t3 else Var v

    fun R v =

fun comp (S, R) = fn v => value S (R v)

4

      if v = ("X",0) then t1 else if v = ("Y",0) then Var("Z",0) else Var v

Now, for convenience, we can define an update function that adds a new variable binding to an existing substitution:

fun upd (v, t) S = comp (fn w => if w = v then t else Var w, S)

E.g., if we take the substitution R from above, (upd (("Z",0), t3) R) should give you the same

result as (comp (S,R)) above.

In case it helps you understand what these functions are doing, here are their types in terms of the

type Substitution:

    val empty : Substitution

    val value : Substitution -> Term -> Term

    val comp  : Substitution * Substitution -> Substitution

    val upd   : (string * int) * Term -> Substitution -> Substitution

After copying this substitution implementation, you can copy the rest of Stansifer’s code from the exception declaration and the function pairup on. You don’t need the function top from his substitution implementation. I’ll let you figure out what to use instead. For the call to the function exists in the modern SML versions, you need to use List.exists and instead of the function fold you need to use foldr, but the parameters for foldr are in a different order. To see the type of foldr, simply type foldr; at the SML prompt.

Backtracking

Backtracking in the Prolog search is best implemented using exceptions. If we found that a goal matches the head of a rule in the database, we’d perform the necessary substitutions on the body of the rule and try to solve the subgoals. If solving these subgoals fails, we could raise an exception. When the exception is caught, we’d simply try the next rule in the exception handler.

If we only pass down the substitutions as arguments (instead of modifying a global data structure), raising an exception will automatically undo all the substitutions. If we find a solution, we need to pass the resulting substitution back up.

Here is an example of using exception handling for backtracking.

In the call ‘change coins amount’, coins is a list of coin values and amount is the total amount for which the change is computed. The function change tries to find a list of coins such that they add up to the total amount. The function change is of type int list -> int -> int list.

    exception Change;

    fun change _ 0 = nil

      | change nil _ = raise Change

      | change (coin::coins) amt =

        if coin > amt then

           change coins amt

        else

           (coin :: change (coin::coins) (amt-coin))

           handle Change => change coins amt;

5

E.g., the call ‘change [5,2] 16’ will return [5,5,2,2,2].

The first argument of change, i.e., the list [5,2], is matched against the pattern (coin::cons). So coin = 5 and coins = [2]. For this algorithm to find the smallest number of coins, the list of coins should be sorted in decreasing order. If the largest coin, 5 in this case, is greater than the remaining amount, then change is called recursively with the tail of that list. The call in the then-branch would be ‘change [2] amt’. If it would work to use the large coin, it’s tried out (in the else-branch). There first the recursive call ‘change [5,2] (amt-5)’ is tried. I.e., the large coin is used and we recursively try to give change for the remainder. If it doesn’t work to give exact change for amt-5, eventually the exception would be raised. Then we try to use the rest of the coins instead using the call ‘change [2] amt’.

In Prolog terminology, we are setting up a choice point for the call ‘change (coin::coins) (amt-coins)’ by associating it with an exception handler. If the branch ‘(coin::coins) (amt-coin)’ doesn’t work, then we’ll try the branch ‘coins amt’ next.

The other two recursive calls, the one in the then-branch and the one in the handle-clause, don’t have an exception handler attached. So if the exception is thrown in the recursive call, it’s passed up to the caller.

6


版权所有:留学生编程辅导网 2020 All Rights Reserved 联系方式:QQ:99515681 微信:codinghelp 电子信箱:99515681@qq.com
免责声明:本站部分内容从网络整理而来,只供参考!如有版权问题可联系本站删除。 站长地图

python代写
微信客服:codinghelp