CS 334 Lecture 22

CS 334 Lecture 22


        1. Uses of cut.
        2. Examples
    1. Prolog based on resolution theorem proving!
        1. Prolog restricts to horn clause logic.
        2. Prolog does not implement resolution correctly.
        3. Negation based on closed-world assumption
    2. Evaluation of Logic Programming and PROLOG
    1. Classification of machine architectures:
      1. 1.Tightly coupled (shared memory)
      2. 2.Distributed computing system (typically no global memory)
    2. Processes
    3. Language mechanisms supporting concurrency
      1. Semaphores
      2. Monitors


Uses of cut.

  1. When get to this position, no other rule should be applicable, thus never try another.

  2. When get to this point, no hope of ever succeeding.

  3. Only 1 sol'n of interest - don't even try generating others.


1. sum_to(N,X) should give 1 + 2 + ... + N.

        sum_to(N,Sum) :- Pred is N-1, sum_to(Pred,Partial_sum),
                    Sum is Partial_sum + N.

If ever try to resatisfy sum_to(1,X) will go into infinite loop. (Why?)

Most likely happens as part of resatisfying something complex. e.g. sum_to(1,X),foo(apples).

Fix by putting in sum_to(1,1) :- !.

Ex. try to resatify various sorts, get wrong answers. Really only one correct answer!

Try insert sort, selection sort.

If can exclude some cases early on. Can use cut to terminate search.

    Possible_Pres(rush) :- !,fail.
    Possible_Pres(X) :- NativeBorn(X).

fail is predicate that is never satisfied.

Sometimes get strange behavior if not careful.

Suppose have following program:

    likes(joe,pizza) :- !.

What happens if put in query: ?- like(Person,pizza).

Get answer of joe, but not jane. Yet likes(jane,pizza) is true!

Unfortunately, putting in cut may screw up reversibility.


        append([],X,X) :- !.
        append([A|B],C,[A|D]) :- append(B,C,D).
Now only get one answer (when running either direction).

Prolog based on resolution theorem proving!

Understand program as either proof or set of procedure calls.

Fact: A(a,b,c)

is just treated as an atomic statement which is asserted to be true.

Rule: A(X,Y) :- B(X,Y),C(X,Y).

is understood as for all X,Y (B(X,Y) ^ C(X,Y) -> A(X,Y))

Formulas of the form for all X1,...,Xm (B1(X) ^...^ Bn(X) -> A(X)) are said to be Horn clause formulas (notice fact is degenerate case where n = 0).

Program is understood as a collection of Horn clause formulas.

Query: ?- D(X,Y)

is understood as there exists X,Y. D(X,Y).

Is there a proof of there exists X,Y. D(X,Y) from statements in program.

Resolution theorem proving works by attempting to show that hypotheses and negation of conclusion (i.e. for all X,Y. ~ D(X,Y)) generate a contradiction. Contradiction is essentially found by finding values for X, Y such that D(X,Y).

Prolog restricts to horn clause logic.

Clause contains at most one positive atomic formula.

Prolog does not implement resolution correctly.

Omits the "occurs check" during unification.


is_own_successor :- X = successor(X).

return true, even in absence of other info about successor.

If write

        is_own_successor(X) :- X = successor(X).
then prints infinite term:
    X = successor(successor(successor(successor(successor(...
(Can also occur if try to unify clauses containing A(Y,f(Y)) and ~A(Z,Z) )

But no finite term which satisfies - "occurs check" would rule this out.

Can lead to incorrect conclusions.

Just as depth-first search can lead to missing correct solutions.

Occurs check used to be believed to be exponential time. Now have faster (but more complex) algorithms.

Changed status from being a "bug" to being a "feature".

Negation based on closed-world assumption

If system cannot prove statement is true, then it must be false.

In Prolog negation actually based on finite failure. That is report something false if no proof that it is true and the attempt to prove it terminates.

Recall 3 possible outcomes of proof. Succeeds, fails, doesn't terminate.

Returns false only if it fails (finite failure).

If attempt never terminates then don't report it false!.

Note that this is a non-monotonic rule of reasoning.

E.g., if have program above (w/father, mother, etc), then since can't find or prove

father(shezad,kim), system deduces that it is false.

Built-in predicate, "not", defined so that:

not(X) succeeds if an attempt to satisfy X fails.

not(X) fails if an attempt to satisfy X succeeds.

not(X) :- X,!,fail.


?-  not(father(shezad,kim)).
reports true

However if add fact, father(shezad,kim), then will reverse answers.

What happens if write:

?- not(Z) where Z has not been unified

If define

    childless(X) :- not(father(Y,X)),not(mother(Z,X)).
then childless(fred) will return yes if fred not mentioned in program!

whereas childless(Z) returns no.

If define

    old(X) :- not(young(X)).
and don't have facts or rules for young, then everything is old!

If write the other direction,

    young(X) :- not(old(X)).
get the opposite response, everything is young!

Seems to fly in the face of closed-world assumption!

Not does not always behave properly - in particular, not(not(term)) does usually not return same answers as term.

Safest if only use not on predicates which have no free variables (i.e., make sure variables instantiated before calling it!). See homework for problems otherwise.

Control structure implicit, but very important!

Evaluation of Logic Programming and PROLOG

Keep in mind difference between PROLOG and pure (Horn clause) logic programming

Limits effectiveness to 2 classes of programs:

  1. Where efficiency not a consideration.

  2. Where too complex for conventional language.

Retains useful role as well for prototyping or as specification language.

One of reasons Japanese chose PROLOG is belief that it can be speeded up by use of highly parallel machines. OR-parallelism seems useful (work on different branches of search tree in parallel), but AND-parallelism (trying to satisfy various terms of right side of rule in parallel) seems more problematic.


Classification of machine architectures:

S = single, M = multiple, I = instruction, D = data

SISD - traditional von Neumann - one processor, one piece of data at a time,

SIMD - currently used supercomputers (typically synchronous)

pipeline (interleave operations) for speed-up or

array-processor - do same thing to all elts of array,

MIMD - most interesting to computer science (asynchronous).

1. Tightly coupled (shared memory)

Difficulties - synchronization - wait for one to finish for another to take output and start.

Generalization of sequential algorithm not necessarily fastest on parallel machine

Contention over resources - reader - writer problem.

2. Distributed computing system (typically no global memory)

Each processor has its own memory but share a common communications channel.


Process: instance of program or program part that has been scheduled for independent execution.

In one of three states: executing, blocked, or waiting.


  1. Synchronizing w/ other processes.
  2. Communicate data.

With shared memory, communication typically via shared memory.

Problem: Mutual exclusion (reader-writer contention)

Distributed memory, communication via sending messages:
Problem: How to asynchronously send and receive messages?

Need mechanisms in OS to

  1. Create and destroy processes.

  2. Manage processes by scheduling on one or more processors.

  3. Implement mutual exclusion (for shared memory).

  4. Create and maintain communication channels between processors (for distributed memory).

Language mechanisms supporting concurrency

Can't deliver detailed description of how to support concurrency here. Instead focus on language mechanisms to support concurrency.

Three major mechanisms:

  1. Semaphores (for mutual exclusion)

  2. Monitors (for mutual exclusion)

  3. Message passing (using "tasks")

Focus here on producer/consumer or bounded buffer problem.
Two processes cooperating, one by adding items to a buffer, the other removing items. Ensure not remove when nothing there and not overflow buffer as well.

Text also focuses on parallel matrix multiplication (read on own).

Text also discusses some ways of handling with simple extensions of existing languages:

Coroutines also worth noting (part of Modula-2). Iterators were a special case.

Idea is co-equal routines which pass control back and forth.
E.g., our Modula-2 has library supporting routines:

NewCoroutine(P: PROC; workspaceSize:CARDINAL; VAR q: COROUTINE);

Starts up new coroutine, q, by executing procedure P.

Transfer(VAR from, to: COROUTINE)

Transfers control from one coroutine to another.

Can have multiple coroutines executing same procedure or can all be distinct.

Usually run on single processor.

Can think of as supporting multi-tasking. Good for writing operating systems.

See Modula-2 code in text for bounded buffer problem with coroutines.


Support mutual exclusion and synchronization in shared-memory model.

Three operations:

    InitSem(S: Semaphore; value: integer);
    Wait(S: Semaphore);
    Signal(S: Semaphore);

InitSem starts up semaphore with an initial (non-negative) value.

Wait(S): If S > 0 then S := S - 1 else suspend self

Signal(S): if processes are waiting, then wake up a process, else S := S + 1;

Think of Wait(S) as claiming a resource so that no one else can get it, while Signal(S) releases the resource.

In order to solve mutual exclusion problem, must ensure that Wait and Signal execute atomically (i.e., cannot be interrupted and no one else can execute at same time).

If start w/S = 1 then protect a critical region by:

    Wait(S);    -- grab token
    {Critical region}
    Signal(S);  -- release token
Can also start with other values of S, e.g., if start w/S = 0 and call Wait(S) then suspend execution until another process executes Signal(S).

Solution to bounded buffer:

Suppose also have procedures:

    CreateProcess(p:PROC; workspacesize: CARDINAL);
        Creates nameless process
    StartProcesses;  -- starts all processes which have been created.
    Terminate;  -- stop execution of process
When all processes are terminated control returns to unit calling StartProcesses.

Main program:
CreateProcess(Producer,WorkSize);   -   - create at least one producer
CreateProcess(Consumer,WorkSize);   -- create at least one consumer
BufferStart := 1;  BufferEnd := 0
InitSem(NonEmpty, 0)    -- semaphore w/initial value of  0 to indicate empty
InitSem(NonFull, MaxBuffSize)   -- semaphore w/initial value of  size of buffer
InitSem(MutEx,1)        -- semaphore used for mutual exclusion

Procedure Producer;
        BufferEnd := BufferEnd MOD MaxBuffSize + 1;
        Buffer[BufferEnd] := ch;
    end loop;

Procedure Consumer;
        ch := Buffer[BufferStart];
        BufferStart := BufferStart MOD MaxBuffSize + 1;
    end loop

Why is there a MutEx semaphore?

Technically it is not necessary here since Producer only changes BufferEnd, while Consumer only changes BufferStart, but if they both changed a count of the number of items in the buffer would be important to keep them from executing at the same time!

What would go wrong if you changed the order of the two Wait's at the beginning of either Producer or Consumer?

Biggest problem with semaphores is that they are too low level and unstructured. Accidentally reversing order or Wait'ing twice could be disastrous.


Monitors are a much higher-level construct to support mutual exclusion and synchronization.

ADT mechanism encapsulating shared data and operations. At most one process can be in monitor at a time.

Provides condition variables, each with a waiting queue of processes and associated suspend and continue operations.

Suspend puts process on queue, continue results in removing next process from queue for execution.