TEACH SETS Revised Aaron Sloman Nov 1989
Slightly revised Oct 2001
An INTRODUCTION TO SET MANIPULATION USING LISTS
===============================================
CONTENTS
-- Introduction
-- Differences between lists and sets
-- Prerequisites for this teach file
-- A first example
-- Checking whether a person has a property
-- Defining iselement with a "for" loop
-- Re-doing iselement as a recursive program
-- Using the matcher to define iselement recursively
-- How to do it
-- Words need to be quoted to refer to themselves
-- Defining the procedure ismale
-- A procedure to test for two properties: happyman
-- Finding an item with a property
-- Using valof
-- A schema for FINDONE
-- How to specify a procedure
-- Specifying internal and external semantics
-- Using ENTER procheader
-- Specifying FINDONE
-- FINDALL
-- Digression: findintegers
-- Now let's return to findall
-- A schema for FINDALL
-- If you need help
-- It still doesn't work?
-- A set of properties: findset
-- Help with recursive issubset
-- Some advice on testing
-- Returning to FINDSET
-- If you needed help
-- A different findset
-- Further reading
-- Introduction -------------------------------------------------------
One common use of a list is to represent a set of objects. It is then
possible to write programs that perform operations on the list, to
represent operations on the set represented. The empty list [] would
then represent the empty set. Sets of sets are then represented as lists
of lists, and operations on sets of sets can also then be defined as
operations on lists.
For example, the following are common operations on objects (represented
here as O1, O2, O3, ....) and sets represented here as S1, S2, S3, ...):
(1) Test whether O1 is a member of the set S1
(2) Test whether the set S1 is a subset of the set S2
(3) Given an object O1 and a set S1, containing O1, create set S2
containing all the elements of S1 except O1
(4) Given two sets S1 and S2, create the "union" set S3, containing
all the elements that are EITHER in S1 or S2 (or both).
(5) Given two sets S1 and S2 create the "intersection" set S3,
containing all the elements that are in BOTH S1 and S2.
(6) Generalize (4) and (5) as follows: given S, a set containing sets
S1, S2, S3 ... Sn as its elements, create the union of all of S1 to Sn
or the intersection of all of S1 to Sn.
(7) Given two sets S1 and S2, create the "difference" set S3, such that
S3 contains all the objects in S1 that are not in S2
(8) Given an object O1 and a set S containing sets S1, S2,... Sn, create
a new set of sets containing all and only the sets that contain O1.
(9) Define a predicate (a procedure returning a boolean --- true or false
--- result) which tests whether O is a member of S.
(10) Define a predicate which tests whether S1 is a subset of S2.
(11) Define a predicate which tests whether S1 intersects S2, i.e.
whether they have any elements in common (so that their intersection is
not the empty set).
(12) Define a predicate which tests whether S1 and S2 are the same set,
i.e. contain exactly the same elements (not necessarily represented in
the same order in the lists representing the sets).
(13) Given a set S1 and a predicate P create a set S2 containing all the
elements of S1 that satisfy the predicate P (i.e. the set of all the
objects such that P returns true when applied to them).
(14) Given a set S and a predicate P, define a new predicate that
returns true if and only if every object in S satisfies P.
(15) Given a set S and a predicate P define a new predicate that returns
true if at least ONE object in S satisfies P, and false if none do.
(16) Given a set S and a predicate P define a counting procedure that
returns an integer N such that N is the number of objects in S that
satisfy the predicate P.
(17) Given a set S some of whose elements are sets, and some of them
have elements as sets, and some of those sets have elements of sets,
i.e. so that sets can occur to arbitrary depth, create a new set
containing all the "atomic objects" (i.e. the non-sets) that are either
in S or in one of the elements of S or one of the elements of the
elements of S or .... (In short if we think of S as a tree of sets,
create the set of the "leaves" or "tips" of the tree, sometimes called
its "fringe".
These are just illustrations of operations on sets that are often found
to be useful. There are many more. You could try to extend this list.
AI programming languages often provide built-in or library procedures
that perform these tasks. However, it often turns out that a particular
operation that you require is not available, so it is useful to develop
the techniques required for building procedures to perform such
operations on sets. This will also help to develop general programming
techniques. The exercises in this teach file, in TEACH SETS2 and in
TEACH LISTQUESTIONS are designed to help you develop these techniques.
-- Differences between lists and sets ---------------------------------
Notice that lists are not exactly like sets: for example the items in a
list have an order, whereas sets are just collections of things without
any particular order. Further, lists may have repeated elements, as in
[Fred John Mary Fred Sue]
whereas sets contain each item only once. For these reasons the use of
lists to represent sets requires some care. For example you can't simply
form the union of two sets S1 and S2 (i.e. the set containing everything
that is in at least one of S1 and S2) simply by concatenating the
corresponding two lists, for that could produce repeated elements, as in
[Tom Dick Sue] <> [Dick Mary Joe] =>
** [Tom Dick Sue Dick Mary Joe]
Similarly, because the order of elements is immaterial in sets, the
two lists
[Tom Dick Sue] [Sue Tom Dick]
would represent the same set. So you cannot use the equality tester
"=" or the match operator "matches" to test for whether two sets are
the same.
[Tom Dick Sue] = [Sue Tom Dick] =>
** <false>
-- Prerequisites for this teach file ----------------------------------
Before trying these exercises, you should be fluent in the use of the
editor and know how to mark ranges in files in order to copy them, load
them etc (TEACH * MARK, TEACH * LMR). It will also be useful if you
worked through the following:
TEACH * RESPOND teaches the use of conditionals.
TEACH * LISTS, * DEFINE, * VARS, * MATCHES, * ARROW,
The section on lists in TEACH * PERCENT.
TEACH * ARITH introduces some "loop" instructions
TEACH * RECURSE1 introduces recursion
You may also find that TEACH * STACK is useful.
-- A first example ----------------------------------------------------
Suppose we want to store information about several people, declaring a
variable for each person, and then assigning a list of descriptions to
that variable.
E.g. if John is male, happy, single, a clerk and tall, then we could do
the following.
vars john;
[male happy single clerk tall] -> john;
and similar things for other people. We could then have a list called
"people" with the names of all the people, e.g.
vars people;
[john ethel mary fred] -> people;
You could declare variables and construct suitable lists of properties
for the other people. (For now, let's not worry about whether this is a
GOOD way to represent information about people. Choosing a good
representation for a particular task is often very difficult, and there
are very many different ways the same information can be represented in
a computer.)
We now have the following situation: the variable "people" contains a
list of names of people, and for each person the name is also a variable
containing a list of properties of that person. For example, The
variable "john" represents information about about John. We should now
be able to write programs to examine those lists, in order to answer
questions, for example:
Does John have the property "tall" ?
Does Ethel have the properties "rich" and "clever"?
Does Fred have either the property "rich" or the property "clever"?
Additional tasks that you might require a program to perform would be
such things as "find all the people who are tall" (i.e. make a list of
their names), or "find all the people who are tall and either rich or
happy".
Try thinking about how these tasks break down into sub-tasks. This will
help you anticipate some of the problems discussed in the rest of this
file.
-- Checking whether a person has a property ---------------------------
How can a program check whether a person has a property (using the above
representation of properties)?
We see immediately from the list assigned to "john" that John is tall,
but that it is not known whether he is fat. How? When asked, a person
might say something like the following: "Well I look at the list,
starting at the left hand end and going along the list to the right,
until I come across the appropriate word. If I get to the end of the
list before finding the word, I know the property isn't in the list."
Pop-11 includes a built in procedure member, that can be used to perform
this test (try it):
member("tall", john) =>
** <true>
member("fat", john) =>
** <false>
For now, suppose the procedure member did not exist, and you therefore
had to define a procedure called "iselement" that behaved like member.
How would you do it?
-- Defining iselement with a "for" loop -------------------------------
One easy way is to use the built-in "for" loop mechanism that Pop-11
provides, that enables you to use the format
for <variable> in <list> do <actions> endfor;
So you could define a procedure called iselement thus
define iselement(item, list) -> result;
;;; take in one item, and a list, and return a truth value,
;;; true if the item is in the list, otherwise false.
lvars element;
for element in list do
if element = item then
;;; found it
true -> result;
return(); ;;; leave the procedure now
endif;
endfor;
;;; failed to find the item in the list so
false -> result;
enddefine;
(Not all the semi-colons are essential - those immediately before
closing brackets like "endif", "endfor" and "enddefine" can be omitted.)
Type that in and test it with various combinations of arguments:
iselement(3, [1 2 3 4]) =>
iselement(3, [1 2 3]) =>
iselement(1, [1 2 3 4]) =>
iselement(3, []) => ;;; always check the "empty list case
vars john;
[male happy single clerk tall] -> john;
iselement("tall", john) =>
-- Re-doing iselement as a recursive program --------------------------
Now consider how you might have written the procedure iselement if
Pop-11 had not contained any looping constructs, like "for ... endfor"
or "while .... endwhile", "until ... enduntil" etc.
We are going to assume that "if" is available (as introduced in TEACH
RESPOND), and that procedures can be defined and "called", not only by
other procedures, but also by themselves. We'll see that, as if by
magic, the procedure iselement can call itself, in order to get the same
effect as a looping instruction. This is called "recursion". Later you
will learn that recursion is useful in some cases where loops would be
very clumsy.
Before proceeding you must know about output local variables (as
explained in TEACH DEFINE). You will also need to know how to use the
procedure HD to get the first element of a list, and TL to get the
"tail" i.e. a list composed of all but the first element, as explained
in TEACH LISTS. Thus, if "list" is a variable whose value is a list, and
"item" is another variable, whose value is anything (e.g. a word) the
expression
item = hd(list)
is a "boolean" (i.e. true or false) expression that will be true if the
first element of list is the same thing as item, otherwise false.
(Experienced programmers may like to look at HELP * EQUAL to see how
POP-11 deals with different kinds of equality.)
Using these ideas, let's work up to a new definition of the procedure
iselement.
Imagine the procedure iselement is running and that it has input local
variables item and list, and an output local variable called result, and
the input variables have been given values thus:
vars item, list;
3 -> item;
[1 2 3 4] -> list;
How can we check if item is in the list? We can first see if the
following is true
item = hd(list)
is it?
If it is true, then all the procedure iselement has to do is to assign
true to its output local variable, and then stop.
Otherwise, if it isn't true, then it can do the same test on the tail of
the list, i.e.
item = hd(tl(list))
If that is true it can assign true to result and stop. If not then it
has to check the next thing in the list
item = hd(tl(tl(list)))
But how many times will it have to do this? Without knowing the lengths
of the lists we can't tell. And if we want the procedure to work on
lists of ANY length, then we need some way of going on indefinitely.
Now, if the procedure we are defining already has an instruction to
compare item with hd(list) (as above), then instead of explicitly
looking at the head of the tail of the list and then the head of the
tail of the tail, etc, it can just call itself "recursively". There's
the magic (until you get used to it).
Thus we can come up with the following schema (which has a bug, but will
work in some cases):
define iselement(item, list) -> result;
if item = hd(list) then
true -> result;
elseif iselement(item, tl(list)) then
;;; the item was found (somewhere) in the recursive call, so
true -> result;
endif;
enddefine;
Try defining that. Notice that the embedded (recursive) call of
iselement has to take TWO arguments, because iselement is being defined
as a procedure that takes two arguments. Notice also that the recursive
call can succeed (produce TRUE) not just because the very next element
in the list matches item, but because a later one did.
It should already be clear that something is wrong, because there is no
case in which FALSE is assigned to the result.
Nevertheless, you can compile that procedure then test it:
iselement(3, [1 2 3 4]) =>
iselement(3, [1 3]) =>
So far so good. You can even see how it works if you trace the procedure
and redo those commands.
trace iselement;
iselement(3, [1 2 3 4]) =>
etc.
But suppose you search for something that isn't in the list - what
happens?
iselement(99, [1 2 3]) =>
> iselement 99 [1 2 3]
!> iselement 99 [2 3]
!!> iselement 99 [3]
!!!> iselement 99 []
;;; MISHAP - NON-EMPTY LIST NEEDED
;;; INVOLVING: []
;;; DOING : hd iselement iselement iselement iselement ...
When iselement failed to find 99 at the head of the original list, it
went into recursion and tried on the tail. So it then failed to find it
at the head of that, and recursed again. Eventually it tried to compare
99 with the head of the empty list.
But hd([]) produced an error (see TEACH LISTS). And the procedure
aborted.
Clearly the procedure iselement has to be able to cope with the
"terminating" case when it gets the empty list as its second argument.
You might think that all you need is to add an extra "else" clause to
deal with the case where list is the empty list, thus:
define iselement(item, list) -> result;
if item = hd(list) then
true -> result;
elseif iselement(item, tl(list)) then
true -> result;
else
false -> result;
endif;
enddefine;
You can try that and test it on the same examples as before, and you
will find that it still eventually produces an error when the item is
not in the list, because it eventually gets down to the empty list and
tries to take the hd of the list.
I.e. the "else" clause comes TOO LATE in the procedure - at least too
late to handle the empty list, because the error produced by testing
hd([]) has already occurred.
It is possible to get round this using the matcher, which doesn't
produce an error, but can do the test.
-- Using the matcher to define iselement recursively ------------------
Instead of
if item = hd(list) then
we can do
if list matches [^item ==] then
This condition is true if the list starts with item, no matter what
comes after it, even if nothing comes after it.
This is a bit wasteful, as we have to build a pattern containing item
every time, in order to do the comparison, but never mind that for now.
Try the following and test it:
define iselement(item, list) -> result;
lvars item, list, result;
if list matches [^item ==] then
true -> result;
elseif iselement(item, tl(list)) then
true -> result;
else
false -> result;
endif;
enddefine;
If you try that you will now find that instead of getting an error
message from hd saying NON-EMPTY LIST NEEDED you will get it from tl,
because of the line containing tl(list).
You could overcome that problem by again using the pattern matcher
and using an extra variable to dig out the tail of the list, thus
define iselement(item, list) -> result;
lvars rest; ;;; new variable for tail of list
if list matches [^item ==] then
true -> result;
elseif list matches ![= ??rest] and iselement(item, rest) then
true -> result;
else
false -> result;
endif;
enddefine;
Notice the use of "and" to join two conditions to form a more complex
condition.
Now test it as before
iselement(99, [1 2 3]) =>
iselement(3, [1 2 3 4]) =>
iselement(3, []) =>
iselement("tall", john) =>
iselement("fat", john) =>
It works, but it's pretty clumsy. It needs the extra local variable
(rest) for the second call of the matcher and we have to make a special
list containing the item for the first test.
We can avoid all that if instead of starting by testing the head or the
tail of the list, we check whether it has a head or tail. I.e. FIRST see
if the list is empty. If it is empty, what should the result of
iselement be?
-- How to do it -------------------------------------------------------
Try completing the following schema.
define iselement(item, list) -> result;
if <the list is empty> then
false -> result;
elseif <the item is the first element of the list> then
true -> result;
else
<use ISELEMENT to search for the item in the TL of the
list and assign the outcome to RESULT>
endif
enddefine;
So, at each stage of the process there are three options, either the
list is empty, or the item is the first element of the list, or it is
not and we have to try the tail! The above schema represents these
options, using the IF statement.
By putting the test for the empty list near the beginning we make sure
that if it IS empty, the procedure stops and does not try testing the hd
or the tl of the list.
That is one of many possible schemas for the required procedure.
If you have managed to define a procedure, test it:
iselement(3, [ 1 2 3 4])=>
iselement("tall", john) =>
iselement("fat", john) =>
These tests should each produce **<true> or **<false>
If you did not manage to complete that schema for iselement
successfully, look at TEACH SETS1.
If you find the example hard to follow you could try TEACH RECURSION for
some more examples and explanation.
-- Words need to be quoted to refer to themselves ---------------------
The list john included the word "single". So try this
john=>
iselement(single, john) =>
You will find that Pop-11 declares "single" as a variable, gives it an
"undef object" as a value, and then iselement fails to find the undef
object in the list john. So its result is false.
In order to get the desired effect, i.e. searching for the word "single"
itself in the list, rather than its value as a variable, you have to
enclose it in double quots (i.e. WORD quotes, not string quotes):
iselement("single", john) =>
It should now work.
Can you explain why
iselement("single","john") =>
does not work? If not, ask for help. The difference between a word and a
list is crucial.
Compare
iselement("single", valof("john")) =>
That works, because, if "john" is the name of a variable, then
valof("john")
and
john
are equivalent Pop-11 expressions denoting the value of the variable.
(Actually, if you use sections later on you will find that they are not
equivalent in all contexts.)
Try experimenting with ISELEMENT, creating several more variables like
"john" and checking that the ISELEMENT process does in fact work. E.g.
vars mary;
[female happy married teacher thin tall] -> mary;
iselement("male", mary) =>
-- Defining the procedure ismale --------------------------------------
If your program frequently needs to check whether something has the
property male, then you can define a procedure called "ismale" to
do this:
define ismale(person) -> result;
;;; Person is a list of words. Return true if the
;;; list contains "male" otherwise return false.
iselement("male", person) -> result;
enddefine;
Test it:
ismale(john) =>
ismale(mary) =>
Define a similar procedure called "ishappy" which takes a list of words
as input and returns true if the list contains "happy" otherwise false.
-- A procedure to test for two properties: happyman -------------------
Try writing a procedure called HAPPYMAN which takes a single argument, a
list, and returns TRUE if that list represents a happy man. I.e. it
should test for whether the list contains both "happy" and "male". The
easiest method is to use the two procedures ismale and ishappy, defined
in terms of iselement.
You could also try defining it directly in terms of iselement. Either
way the following schema should work:
define happyman(list) -> result;
if <list represents a male> then
if <list represents a happy thing> then
true -> result
else
false -> result
endif
else
false -> result
endif
enddefine
Test your procedure:
happyman(john)=>
happyman([ male old hungry])=>
happyman(mary) =>
Some questions:
(a) Why did the above schema need TWO "else"s.
(b) The nested IF ... ENDIF expression does a test, and if it is
true assigns true to result and if it is false, assigns false
to result. Can it be written so that it achieves the same thing
without using IF at all?
(c) Can you write the whole procedure without using IF at all. Just
use "and", and assign the result of "and" to result?
Use the fact that if <E1> is an expression and <E2> is an
expression, then
<E1> and <E2>
is also an expression with a result that is FALSE if both <E1> and
<E2> have the value false, otherwise it has the same value as <E2>.
So you can use imperatives of the form
<E1> and <E2> -> result;
(If <E1> is false, the whole thing is false without <E2> being
evaluated. This is often useful for preventing an error that
will occur if the preconditions of <E2> are not satisfied.)
("OR" is another useful infix "boolean" operator.
See HELP * AND, HELP * OR, HELP * BOOLEAN)
-- Finding an item with a property ------------------------------------
Suppose we had a list of people thus:
vars people;
[john ethel mary fred] -> people;
vars john, ethel, mary, fred;
[male happy single clerk tall] -> john;
[female unhappy single prime_minister ] -> ethel;
[female happy married teacher thin tall] -> mary;
[male small typist old] -> fred;
where each element of the list of people has been declared as an
ordinary variable and assigned a list representing the person. A
question one might want to ask is "do you know of a happy person?" or
"do you know of a clerk?". The answer should be either the name of the
person or FALSE.
More precisely, suppose we want to define a procedure called "findone"
that takes a word, representing a property, and a list of names of
people, and then returns true or false, thus:
findone("male", people) =>
** john
findone("old", people) =>
** fred
findone("female", [john fred]) =>
** false
The procedure findone must look at each item in the list (its second
argument), and can use iselement to see if the property name is in the
corresponding list of properties. But how does it get the corresponding
list of properties?
The items in the list are WORDS, and there are lists of properties
corresponding to them as their VALUES. To deal with this you need to
know about the built in Pop-11 procedure "valof", which, when given a
word that has been declared as a variable, returns its value.
-- Using valof --------------------------------------------------------
Assuming that the above declarations and assignments have been compiled,
try the following:
"john" =>
john =>
valof("john") =>
valof(john) =>
It should be clear that the second and third of these have the same
effect. Now consider
vars item;
"john" -> item;
What will the following print? (Try to work it out before trying):
item =>
valof(item) =>
valof("item") =>
VALOF produces an error if you apply it to anything but a word. Now try
all these:
vars list;
[john ethel] -> list;
"list" =>
list =>
valof("list") =>
hd(list) =>
valof(hd(list)) =>
valof(list) =>
If you are not thoroughly confused, you should now have the following
model (remembering that inside list brackets words are quoted, not
replaced by their values -- unless you use "^" or "^^" or percent
signs):
"list"
|
+--valof--->[john ethel]
| |
| +-valof-->[female unhappy single prime_minister]
|
+-valof-->[male happy single clerk tall]
The list called -people- can be thought of in the same way, except that
it would require a bigger diagram.
Thus if our procedure findone is to search down a list of words naming
people, then test for the corresponding property, it has to use valof on
each name to get at the list of properties.
-- A schema for FINDONE -----------------------------------------------
Although findone could be defined using "for ... endfor" like the first
version of "iselement" above, it is a useful exercise to try using
recursion.
Below is a schema for findone.
You should be able to use the procedure iselement defined above, in the
ELSEIF line. You will also have to use valof(hd(list)). Don't be worried
by the fact both iselement and findone both use a local variable "list".
Because it is local in both cases, the two uses of "list" will not
interact. Each procedure effectively has its own "private" version of
the variable.
Try completing this schema:
define findone(prop, list) -> result;
if <no more in the list> then
false -> result
elseif <valof first element has property> then
<first element> -> result
else
findone(prop, tl(list) -> result
endif
enddefine;
-- How to specify a procedure ------------------------------------------
In thinking about designing a procedure like FINDONE you have to have a
very clear idea of what you are trying to do. Produce a specification
for the procedure before you try writing any Pop-11 code.
The specification for a procedure should contain the following items,
each set out with its own sub-heading, for clarity.
(a) A specification of the type(s) of arguments (if any), e.g.
"it takes a list of words and an integer"
"it takes a list (of anything) and a procedure of one
argument producing an integer as a result"
etc.
(b) A specification of the type(s) of outputs (if any) e.g.
"it produces a list of integers"
"it produces either false or an integer"
"it produces two results, a list and a boolean (true or false)"
etc.
NB the specification of types of inputs and outpus should not be
concerned with what they look like when typed in or printed out. (E.g.
it is not relevant to talk about commas and spaces and list brackets:
these are not objects that will be in the machine when the procedure
runs). So the arguments should be specified in terms of the kinds of
objects (integers, words, lists, lists of lists, etc) that will be put
on the stack as input(s) for the procedure, and similarly the outputs.
In specifying the TYPES of inputs and outputs you don't say how the
output is derived from the input. That comes in the next section when
you say WHAT the procedure does.
(c) A description of WHAT the procedure does - without saying HOW it
does it. This should say how the results are related to the input,
without describing the method by which the results are produced.
This section should also include a specification of any side-effects
the procedure may produce (i.e. effects it has other than taking
arguments off the stack or putting results on the stack.
Side-effects could include reading something from the terminal,
printing something to the terminal, changing a file, changing the
value of a global variable, such as "database") - though in general
changing global variables is something to be avoided if possible,
as it can lead to obscure errors.
(d) Some typical test cases, with an indication of what the procedure
does with them (not how it does it). The test cases should include
"limiting" or "extreme" test cases, e.g. empty lists as input, lists
of only one argument, lists where the item searched for is at the
beginning, lists where it is at the end, lists that don't contain
what is searched for, etc. Selecting a good range of test cases is
not always easy, but is crucial to good design. The test cases can
be described using simulated program executions, e.g.
iselement(99, []) =>
** <false>
iselement(99, [99]) =>
** <true>
iselement(99, [a b 3 4 c d]) =>
** <false>
iselement(99, [a b 96 c 97 d 99]) =>
** <true>
iselement(99, [a [99] b]) =>
** <false>
and so on.
(e) When you have specified all that you can begin to write a
description of HOW the procedure should work in order to meet its
specification. E.g.
iselement takes a word W and a list L and it searches down the
list checking to see if any item in the list is the same as W.
If it finds such an item it terminates the search and returns
TRUE as its result. If it gets to the end of L without finding
W it returns FALSE as its result.
Notice that the specification does not say anything about the
programming language used, it does not include any bits of code, and it
is written using concepts that should be clear to someone who does not
know the programming language.
-- Specifying internal and external semantics -------------------------
In chapter 2 of the Pop-11 Primer (TEACH PRIMER, also accessible online
as
http://www.cs.bham.ac.uk/research/poplog/primer/ )
a distinction is made between the internal semantics of a program and
the external semantics.
The internal semantics of a program is concerned with which objects and
processes inside the computer are referred to by the program code.
Some programs which deal with a particular domain of application also
have an external semantics if they refer to entities, within that
domain.
In the specification of iselement above only the internal semantics were
referred to, e.g. :
"it takes a list of words and an integer"
"it produces a list of integers"
However in some cases the words may refer to people and the integer may
refer to an age or a salary. In the context of a project or application
program, the comments explaining a procedure may need to state both the
internal and the external semantics of the input and output variables
and the procedure (e.g. "it returns a list of vectors of words and
numbers, representing all the employees whose salary is less than that
given").
-- Using ENTER procheader ---------------------------------------------
To simplify the task of producing a header comment it is useful to use
the VED command ENTER procheader, described in HELP VED_PROCHEADER
which is available at Birmingham or at the FreePoplog site:
http://www.cs.bham.ac.uk/research/poplog/freepoplog.html
-- Specifying FINDONE -------------------------------------------------
(a) Types of arguments
FINDONE takes two arguments, a word and a list of words, naming
people. The words in the list should already have been declared as
Pop-11 variables and their values should be lists of words
indicating properties of people.
(b) Type of output
FINDONE produces either FALSE or a word as its output
(c) What the procedure does.
Given a word W and a list of people's names P it produces the
first name in P that denotes a list containing W. If there is
no such name it produces FALSE.
It has no side effects.
(d) Typical test cases
findone("happy", []) =>
** <false>
;;; assume lists -john- and -ethel- etc exist as defined above
findone("female", [john ethel]) =>
** ethel
findone("rich", [john ethel]) =>
** <false>
vars people;
[john ethel mary fred] -> people;
findone("typist", people) =>
** fred
findone("angry", people) =>
** <false>
Now try writing the code for FINDONE, so that it meets this
specification, using the SCHEMA presented above, or any other method you
like.
When finished write down a description of HOW it does it, along the
lines illustrated with the description of iselement under (e) in the
previous section.
-- FINDALL ------------------------------------------------------------
It's harder to answer questions of the form "Find all the people who are
clerks", for example:
findall("male", people) =>
** [john fred]
findall("rich", people) =>
** []
In order to solve this task you have to know how to traverse a list,
testing each element, and make a list of all those that pass the test.
-- Digression: findintegers -------------------------------------------
Here, for example, is a procedure that takes a list and returns a list
of all the integers in it. We use the built in procedure "isinteger"
which produces TRUE if applied to an integer, and FALSE if applied to
anything else. (An integer is a "whole" number, positive or negative,
e.g. 0 3 999 -5 -392, but not 3.5 or -77.111).
define findintegers(list) -> result;
lvars item;
[% for item in list do
if isinteger(item) then item endif
endfor
%] -> result
enddefine;
Notice that the line
if isinteger(item) then item endif
puts things on the stack if and only if they are integers, and the list
brackets with percents [% ... %] make a list of everything put on the
stack, between them. Because the percents are used, the list brackets do
not quote all the enclosed text: instead the text is treated as program
and the program is run. See the list examples in TEACH PERCENT.
Try that out
findintegers([1 2 3 cat mouse 99 6]) =>
** [1 2 3 99 6]
findintegers([one 1 two 2]) =>
** [1 2]
findintegers([one two three]) =>
** []
Try some other tests. E.g. what should it do with the empty list?
Doing it recursively is a bit more tricky if you have not met the idea
previously. Try completing the following schema, replacing the dots and
the bits of English in angle brackets, with bits of Pop-11:
define findintegers(list) -> result;
if list = [] then
.... -> result
elseif isinteger(hd(list)) then
<find the integers in the tail of the list, stick
the head of the list on the front> -> result
else
.... -> result
endif
enddefine;
Notice that there are two recursive calls here. Try completing that
schema, and then test it on the above examples, with tracing where
necessary to help you understand what is going on.
You can do it with only one recursive call if you use an extra nested
conditional and modify it thus:
define findintegers(list) -> result;
if list = [] then
.... -> result
else
findintegers(tl(list)) -> result;
if isinteger(hd(list)) then
<add the hd of the list to the front of result>
endif
endif
enddefine;
Once again, try completing that schema, and then test it on all the
previous examples of findinteger.
-- Now let's return to findall ----------------------------------------
Findall is supposed to take a word W, and a list L of people, i.e. a
list of names of lists,. It should return a list of names, i.e. the
names of people whose descriptions include the word W.
findall("male", people) =>
That should print out something like:
** [john fred]
You could try using the "for item in list" format with [% ... %] to make
a list of all the things found that satisfy the test of the form
if iselement(prop, valof(hd(list))) then
as in the iterative definition of findintegers.
Alternatively try doing it recursively, on the model of the recursive
definition of findintegers.
Remember that before you start trying to write the Pop-11 definition for
the procedure it is essential to be clear about what you want the
procedure to do. Follow the example given above, and write the
specification in steps (a) to (e) as was done above for "iselement" (and
"findone").
Examine all the test cases carefully and make sure that your plan for it
covers all the cases. Make sure that it always returns a list, even if
it's the empty list.
If you use recursion make sure that you give the recursive call the
correct types of inputs (see the specification for the inputs), and also
make sure that you use the result of the recursive call properly (i.e.
the list it produces).
-- A schema for FINDALL -----------------------------------------------
Here is a suitable recursive schema for findall. Remember that you'll
have to use iselement as well as the procedure valof on the hd of the
list (as in findone, above).
define findall(prop, list) -> result;
if <list is empty> then
[] -> result; ;;; i.e. empty set is result
else
<get all the items in the tail of the list that
have the property> -> result;
if <the first item in the list has the property> then
<add it to the front of the result>
endif;
endif;
enddefine;
Try testing it, with
findall("female", people) =>
** [ethel mary]
and other examples. To see what is going on trace findall and iselement.
-- If you need help ---------------------------------------------------------
If you didn't succeed, try completing this:
define findall(prop, list) -> result;
if <list is empty> then
[] -> result; ;;; i.e. empty set is result
else
findall(prop, tl(list)) -> result;
if <the first item in the list has the property> then
;;; add it to the result
[^(hd(list)) ^^result] -> result;
endif;
endif;
enddefine;
N.B. When you complete the second conditional, VALOF is needed as
before, so that, for example, ISELEMENT looks in the list associated
with "john", not in the word "john" itself.
Test your procedure. Try Tracing it and ISELEMENT, when you do.
-- It still doesn't work? ---------------------------------------------
If you didn't manage, try typing in this
define findall(prop, list) -> result;
if list = [] then
[] -> result
else
findall(prop, tl(list)) -> result;
;;; Got all the things from the tail. Check whether the hd
;;; of the list needs to be added to the front of result
if iselement(prop, valof(hd(list))) then
;;; create a new result list, using the hd of the list
;;; and the old result, got from the recursive call
[^(hd(list)) ^^result] -> result
endif;
endif;
enddefine;
Then try:
trace findall;
findall("happy", people) =>
findall("female", people) =>
Try other tests. If you haven't traced ISELEMENT trace it too - though
you may get a lot of printout.
-- A set of properties: findset ---------------------------------------
A yet harder task is to find ALL the people with a set of properties, for
example:
findset([male happy], people) =>
In order to do this you will need to define a procedure that takes a
list of properties and a person (in the form of a list) and then returns
true if the person has all the properties.
E.g. suppose we call it has_all, then
has_all([male tall], john) =>
** <true>
has_all([female tall], john) =>
** <false>
What should we do if the first list is empty?
has_all([], john) =>
It turns out to be convenient to assume that if increasing the list of
properties provides a tougher, more restrictive, test, then having an
empty list of properties means that everyone passes the test. I.e.
has_all([], john) =>
** <true>
And the same would apply to mary, ethel etc.
The only really tricky bit is
<the person has all the properties>.
You may find it useful to define a procedure called ISSUBSET which
checks if every element of one list is in another. It should take two
sets and see if all the elements of the first are ISELEMENTs of the
second. can then write:
elseif issubset(proplist, valof(hd(thinglist)))
Try defining ISSUBSET iteratively (using "for item in littleset do ...")
and recursively.
-- Help with recursive issubset ---------------------------------------
Here is a recursive schema for ISSUBSET:
define issubset(littleset, bigset) -> result;
if <littleset is empty> then
<result is TRUE>
elseif <head of littleset is in bigset> then
if <tail of littleset is a subset of bigset> then
<result is TRUE>
else
<result is FALSE>
endif
else
<result is FALSE>
endif
enddefine;
Try completing that. Test it, then use it to define FINDSET. Then test
FINDSET, tracing everything.
-- Some advice on testing ---------------------------------------------
An important point: whenever you define a new procedure TEST it. Try to
make sure it works on all the limiting cases, e.g. empty lists, lists
with "target" items at the beginning or at the end, lists with only one
item, etc. Try to make sure you have covered all the significantly
different cases in your tests. Plan those tests BEFORE you write the
procedure definition, so that you can use them in thinking about what
the procedure has to do, i.e. you can make sure it covers all the
different cases that can arise.
NEVER type in a long list of procedures and then try testing them all by
running one high level procedure that runs all the others. That will
ensure that you waste a lot of time searching for obscure bugs.
Keep a set of test cases in the file, e.g. in comment brackets
/*
test1
test2
.....
*/
so that you can conveniently re-run the tests if you change the program.
ALWAYS re-run them after every change. Make sure that the first tests
are SIMPLE ones, so that if procedure doesn't work on them it should be
easier to find out why not.
By including the test cases in a comment, you can safely load the whole
file without running the tests every time.
-- Returning to FINDSET -----------------------------------------------
What should we get with an empty list of properties:
findset([], people) =>
It all depends on how you interpret that command. Depending how you had
defined your procedures, POP-11 is likely to interpret it to mean find
the set of people without any constraint at all, i.e. it will produce a
list of ALL the people. This is quite natural if you think of the fact
that adding more properties will REDUCE the list of people. E.g. if you
give the property "happy" then that selects the subset that are happy.
If you give the properties "happy" and "male" then the second property
will (usually) select an even smaller subset from the first.
So if you have no properties it should not reduce the list at all. The
above definition of issubset is consistent with that line of thought.
The procedure FINDSET takes a list of properties and a list of things. It is
to 'return' a list of those things which have ALL the given properties. Here
is a recursive schema for FINDSET:
define findset(proplist, thinglist) -> result;
if <thinglist is empty> then
<the result is the empty set>
else
;;; recursively call findset on the tail
<get all the things in the tail of thinglist that have
all the properties> -> result;
;;; now use issubset to see if the head of thinglist should
;;; be included in the result
if <valof hd of thinglist has all the properties> then
<add it to result> -> result
endif
endif
enddefine;
It may look VERY odd that you have to work on the tail before the head.
At first it may appear that the procedure can't do anything because
nothing can get added to the result till everything else has been added.
But the crucial point is that ultimately the recursion gets down to the
empty list [], and this final recursive call returns a result of [],
after which it works back through thinglist from right to left as the
recursion unwinds, adding items to the result if they satisfy the test.
You should be able to see this by tracing the procedure.
(The "sum" example in TEACH RECURSION is similar.)
Try completing that, and test it if you succeed.
-- If you needed help -------------------------------------------------
If you didn't manage try completing this template:
define findset(proplist, thinglist) -> result;
if <thinglist is empty> then
[] -> result
else
;;; recursively call findset on the tail
<get all the things in the tail of thinglist that have
all the properties> -> result;
;;; Now use issubset to see if the head of thinglist should
;;; be included in the result
if issubset(proplist, valof(hd(thinglist)) then
[^(hd(thinglist) ^^result] -> result
endif
endif
enddefine;
-- A different findset ------------------------------------------------
Can you understand this alternative definition of FINDSET, which makes
use of the previously defined FINDALL:
define findset(proplist, thinglist) -> result;
if proplist == [] then
thinglist -> result
else
findset(tl(proplist), findall(hd(proplist), thinglist))
-> result
endif
enddefine;
(This assumes that you have already defined FINDALL)
Try defining and testing it, tracing everything to see how it works.
Incidentally, the Pop-11 system includes a procedure MEMBER, which does
what your procedure ISELEMENT does. So you don't really need to define
your own. You can change all occurrences of ISELEMENT above to MEMBER
(except in the definition of ISELEMENT).
NOTE: the above procedure uses "==" in the test. That is because the
empty list [] is a unique object and therefore the test can come out
true ONLY if the value of proplist is exactly that object. There is no
need to test whether it is a similar type of object with similar
contents, as "=" does. So using "==" saves time.
For more on that topic see
HELP EQUAL
and HELP EFFICIENCY
-- Further reading ----------------------------------------------------
Several teach files have been mentioned already, including
TEACH * LISTS, * RECURSION
Additional exercises on list processing can be found in
TEACH * RECURSE2
TEACH * RECURSE3
TEACH * LISTQUESTIONS
TEACH * SETS2 -- takes you through a collection "set" operations.
See the POP-11 Primer, chapter 6, also available online at
http://www.cs.bham.ac.uk/research/poplog/primer/
There are additional list processing exercises in some of the books
listed in the file HELP *POPREFS:
Laventhol
(e.g. chapter 5)
and
Barrett, Ramsay and Sloman
(e.g. chapter 7)
Both books may be out of print by now. Also they use an older syntax for
Pop-11, which still works, though often the new syntax is clearer.
--- C.all/teach/sets
--- Copyright University of Sussex 1989. All rights reserved. ----------
--- $poplocal/local/teach/sets
--- Copyright University of Birmingham 2001. All rights reserved. ------Author: Revised Aaron Sloman Nov 1989