TEACH POPCOURSE6 - Complex procedures

Contents


Procedures with input and output variables

POP-11 procedures are like `packages' of commands. Each procedure has a name. We can run all the commands in the package just by typing the name followed by a pair of round brackets (aka `doit brackets'). Procedures are created using the `define' command. A simple example is

define say_hello;
   [hello]  ==>
enddefine;
This just constructs a list containing the word `hello' and then prints it out.

Normally, we want a procedure to operate on a particular item of data and POP-11 allows us to set things up so as to make passing data into a procedure very easy. Lets say we would like the `say_hello' procedure to print `hello' followed by a specified name. To get this effect we need to redefine the procedure so that it takes an input. This involves inserting a variable name inside the round brackets in the header line:

define say_hello(person);
   [hello ^person] ==>
enddefine;
The variable name in the round brackets (`person' in this case) is called the `input variable'. The fact that the procedure has an input variable means that, when we call it, we must put a value between the round brackets of the call. POP-11 responds by passing this value (also called the argument) into the procedure, i.e., by automatically assigning it to the procedure's `person' before the procedure is run. We can see the effect of this in the following examples.

say_hello("John");
** [hello John]

say_hello("Fred");
** [hello Fred]
Experiment: work out what happens if you call `say_hello' giving it a list as input, and why.


Output variables

Although the new `say_hello' takes an input value, it does not do anything with that value except print it out. But, often, we want procedures that will take in an item of data, do something to it and then give back some kind of answer. And we would like this to happen in a way that makes it possible for other bits of POP-11 code to make use of the procedure as a subroutine.

To get this effect we have to define the procedure so that in addition to the input variable it also has an output variable. This is like a box into which the procedure puts its answer after it has finished running. To set up an output variable (or `answer box') we have to include an arrow at the end of the header line of the procedure, followed by (yet another!) variable name.

Let's say we want to define a procedure that works out the full price of something whose price is quoted excluding VAT (value-added tax). We would like to be able to pass-in a price and have the procedure work out the full price including VAT. But we do not want the procedure just to print out the full price. We want it to return the full price via an output variable so that we can then write other POP-11 code which makes use of this procedure as a subroutine.

To add VAT to an initial price, we must add 17.5%. We can get this effect by multiplying the original price by 1.175. Thus a possible definition of the VAT procedure is

define with_vat(price) -> full_price;
   price * 1.175 -> full_price;
enddefine;
This definition starts off in the usual way. After the `define' we have the name of the procedure. Then we have a pair of round brackets containing the input variable. But after the round brackets, we have an arrow followed by a third variable name. This is the output variable, i.e., the box into which the procedure must put its answer or result. The output variable is an ordinary variable but it plays a special role. It is the place that POP-11 assumes the procedure will put its answer. When the procedure finishes executing, POP-11 automatically produces the value of the output variable as a result.

The net effect of defining the procedure using an output variable is that calls on the procedure now produce a value. In other words they act like expressions such as 2 + 2 and x matches y. This means that when we call the procedure we need to do something with the value it produces, e.g., assign it to a variable or possible print it out.

with_vat(18) -> cost;

with_vat(34) ==> ** 39.95

We can also use calls on the procedure within larger expressions. For example

with_vat(29) + 3 ==> ** 37.075

Setting up procedures with input and output variables makes life easier in the long run. It is as if we have provided each procedure with a special `in tray' and `out tray'. When we call the procedure, we put a value inside the round brackets; this is automatically put in the procedure's `in tray.' The procedure then does its stuff on whatever it finds in its in tray and, at the end, puts its result in its `out tray'. POP-11 then automatically produces the contents of the out tray as the result of the procedure call.

Beware: it is very easy to be confused about the difference between a procedure that prints out a value and a procedure that returns a result. Re-read the passage above until you are completely happy with the distinction.

Experiment: write a procedure that returns (not prints!) the output 999 if its input has the value 1, and returns 333 otherwise.

Experiment: suppose that you define a procedure with an output variable but do not assign a value to the output variable within the procedure. What value, if any, does the procedure return?


Built in procedures

Most of the procedures you need you will have to write yourself. However, some procedures are built-in to POP-11 and can therefore be used without writing any special code at all. Almost all of these apply some form of processing to one or more data-objects and then return a result, i.e., they are defined using an input variable and an output variable. To execute the processing, we place the data-object(s) in question inside the round brackets at the end of the procedure name, just like we did with the `with_vat' procedure.

Consider the POP-11 code below. This contains a call on the built-in `sqrt' procedure. The `sqrt' procedure produces the square root of the number that is placed in between the round brackets. Thus

sqrt(25) ==>
prints

** 5.0
This is the square root of 25. The data-object (i.e., number) that we put inside the round-brackets is said to be passed in to the procedure as an argument. The value that is produced is said to be returned as a result. As we noted, procedure calls like this---which return a value---can be treated like any other expression. We can include them in more complex mathematical expressions. Thus

2 * sqrt(23) + sqrt(4) ==>
is valid POP-11. It prints out

** 11.5917
Experiment: construct an expression that multiplies the square root of 23 by the square root of 13 and adds it to the square root of 3. (The value produced should be 19.0237.)

A particularly useful built-in procedure is the `length' procedure. This measures the length of any sequential data-object such as a list. Thus

vars foo;
[a b c d] -> foo;
length(foo) ==>
prints out

** 4
and

length("foobang") ==>
prints out

** 7
which is just the number of characters in the word foobang.

We will also have need of the `not' procedure. This tests whether a value is <false>. So

vars happy; false -> happy;

if not(happy) then [take a pill] ==> endif;

Executing this produces

** [take a pill]

because the value of `happy' is <false>, which means that the value of not(happy) is <true>!

The `not' procedure can be applied to any truth value, regardless of how it is generated. Thus we can do things like

not(3 = 2) ==>
** <true>
and

not(true) ==>
** <false>
Another useful procedure is `intof'. This will be needed for an exercise later on. The procedure can be used to produce the integer part of a real number. Thus

intof(3.4) ==>
** 3
and

intof(97.6542) ==>
** 97
(The `readline()' command introduced earlier is a built-in procedure which takes no input values.)


Procedures with multiple input variables

It is often useful to define a procedure with multiple input arguments. To do this we simply include multiple variable names (separated by commas) between the round brackets of the `define' command. For example, if we want a procedure that takes two data-objects and decides whether the first one has more components than the second, we could write it like this.

define islonger(object1, object2) -> truth_value;
   length(object1) > length(object2) -> truth_value;
enddefine;
If we call the procedure like this

islonger([1 2 3], [1 2]) ==>
the value printed out is

** <true>
which is just the POP-11 for `yes'.

Note that the choice of variable names for the input or output variable(s) is completely arbitrary. Any valid variable name will do. There is nothing significant about using the name `truth_value'. I could just as easily have used `foobang'.

define islonger(apple, plum) -> foobang;
   length(apple) > length(plum) -> foobang;
enddefine;
The procedure still works the same although it is harder to interpret. Using the variable `truth_value' reminds us that the result which is returned by the procedure is a truth value, i.e., either <true> or <false>.

Experiment: define a procedure called `with_reduction'. This should take a number argument representing a price, and a second number argument representing a percentage reduction (where 0.1 means 10% off). The procedure should then return (not print) the final price after the reduction has been applied. The procedure should behave like this.

with_reduction(39, 0.1) ==>
** 35.1
In this example the procedure produces 35.1 which is 39 less 10%, i.e., 39 - (39 * 0.1).


Built-in procedures that take multiple inputs

A useful, built-in procedure that takes two input arguments is the `member' procedure. This tests whether a particular value is contained within a given list. To use this procedure we have to pass in both the value itself and the list. For example

member(7, [5 6 7 8]) ==>
prints out

** <true>
If we pass in a different initial value we get a different response.

member("foo", [5 6 7 8]) ==>
prints out

** <false>
But

member("foo", [bang ding foo]) ==>
prints out

** <true>
Note how in this case we are testing whether a word object is contained within a list object. When we construct the word object outside the list we do so using double quotes. When we construct the word object inside the list, we do not need the quotes because POP-11 assumes that sequences of characters inside lists are words anyway.

In the examples above, we used literal values such as 7, foo and [bang ding foo]. However, there is nothing to stop us using variable values as procedure arguments. Thus

vars word list;
"doobry" -> word;
[oobry goobry doobry] -> list;

member(word, list) ==>
** <true>
Experiment: write POP-11 code that assigns the number 3 to the variable `num', the list [3 2 1] to the variable `nums', and then calls `member' passing in the value of `num' and the value of `nums'.

Another useful procedure that operates on a list is `delete'. This produces a copy of the original list with the specified item deleted. Thus

delete("dung", [foo bung dung dong]) ==>
** [foo bung dong]

Input and output variables are local

Using variable names like `x' and `foo' is not good style. The best approach is to try to make variable names describe the objects that the variables will contain. This makes it easier to read the definition and understand what it does, and how.

In POP-11, you have complete freedom when choosing names for input and output variables provided that you don't choose the name of a built-in procedure or variable (e.g. `member' or `intof'). The variables that you define within a procedure belong exclusively to the definitions in which they appear. As far as POP-11 is concerned an input (or output) variable appearing in the definition of some procedure is implicitly tagged with the name of the procedure and the time that you call it. This means that there is no danger of variables with the same name in two different procedures interfering with one another.

This can be demonstrated using a couple of procedures both of which use an input variable called `num'.

define twice(num) -> value;
   num * 2 -> value;
enddefine;

define thrice(num) -> value;
   num * 3 -> value;
enddefine;
As far as POP-11 is concerned the two `num' variables are different variables. This means that we can happily mix calls of the two procedures together without the contents of one `num' variable every getting mixed up with the contents of the other.

twice(3) * thrice(5) + twice(2) ==>
This prints out

** 94
Because the `num' in `twice' is implicitly tagged with a specific procedure call, it cannot interfere with the `num' in `thrice' or any other `num' for that matter. Thus

vars num, result;
"one" -> num;
twice(3) * thrice(5) + twice(2) -> result;
num ==>
prints out

** one
Note that the original `num' remains unaffected by the calls on `twice' and `thrice'.

Input and output variables in procedure definitions are said to be `local' to their procedures. Variables that are not set up inside a procedure are said to be `global'. The thinking behind this is that input and output variables only really exist within the `locality' of a particular call on the relevant procedure.


Setting up additional local variables

Being able to set up local input/output variables which are guaranteed not to interfere with one another is useful when writing large programs, since it eliminates the necessity to make-up lots of different names. It also means that you can use the name that you feel captures the role of the variable without fear of producing a name clash. Because local variables are so useful, POP-11 allows the programmer to set up additional local variables. These are neither input nor output variables. They are just variables that are established inside the procedure and thus belong exclusively to that procedure in the same way input/output variables do.

To set up local variables inside a procedure, you include a vars command immediately after the `define' line. As an example, here is a definition of a procedure that adds up all the numbers in a list.

define addup(list) -> total;
   vars position i;
   0 -> total;
   0 -> i;
   repeat length(list) times
      i + 1 -> i;
      list(i) + total -> total;
   endrepeat;
enddefine;
The `define' line specifies one input variable (`list') and one output variable (`total'). Below the `define' line we have a `vars' command that sets up a variable called `position'. Then we have an assignment that initializes the `total' variable to have the value 0. Then we have a `for' command that steps the `position' variable through all the numbers between 1 and the length of the list. In the body of the loop, there is a `+' command that adds the indicated element of the list to the total and assigns the value produced to be the new value of `total'. At the end of this process the `total' variable contains the sum total of all the numbers in the list. If we test the procedure as follows

addup([45 72 81 23]) ==>
it prints out

** 221
Note how the length of the list makes no difference.

addup([1 2]) ==>
** 3

addup([8 7 6 5 4 3 2 1]) ==>
** 36

addup([]) ==>
** 0
In this procedure, the fact that we have set up the variable `position' in a `vars' command that appears within the procedure definition means that we do not have to worry that our use of `position' will interfere with any other use of this variable.

Experiment: modify `addup' so that it returns the result of multiplying all of the numbers together. You could call the procedure `produp'. Test `produp' on the list [2 3]. If you do not get the answer you expect, check that you have initialized `total' to the correct value.


Procedures that call procedures

We can include any sort of POP-11 command within the body of a procedure, including a call on another procedure. Thus we can write two procedures such that one procedure makes use of the other by calling it explicitly. To illustrate this, we will write a program that substitutes numerals into lists. First we define a procedure that returns the numeral for a single-digit number. (Note that a numeral is just the way we say a a number. Thus the numeral for `126' is `one hundred and twenty six'.)

define numeral_of(num) -> numeral;
   vars numerals;
   [one two three four five six seven eight nine] -> numerals;
   if num > 0 and num < 10 then
      numerals(num) -> numeral;
   endif;
enddefine;

numeral_of(2) ==>
** two
With this procedure available, we can define a new procedure that goes through a list, element by element, converting any number it finds into its corresponding numeral. This procedure will use the built-in procedure `isnumber' to test whether a particular element is a number value or not.

define put_numerals(list) -> list;
   vars i;
   0 -> i;
   repeat length(list) times
      i + 1 -> i;
      if isnumber(list(i)) then
         numeral_of(list(i)) -> list(i);
      endif;
   endrepeat;
enddefine;
Calling this procedure as follows

put_numerals([I saw 2 men with 3 dogs]) ==>
prints out

** [I saw two men with three dogs]
Experiment : test what happens if `put_numerals' is given a list that contains a number greater than 9.


Restriction procedures

In some situations, the advantages provided by matcher query variables can be best achieved using `restriction procedures'. Restriction procedures are procedures which produce truth values and which are used in conjunction with a query variable in a `matches' pattern.

Consider the problem of deciding whether a particular list contains a reference to a particular individual. We can work this out by going through all the relevant names and checking to see whether any one of them is contained within the list. We can do it more simply, though, using a restriction procedure. First of all we set up a list of all the names.

vars names;
[Fred Jill Bob] -> names;
Then we define a procedure that takes a word and decides whether it is the name of a person by seeing if it is in the list of names.

define isname(word) -> result;
   member(word, names) -> result;
enddefine;
This procedure behaves in the obvious way.

isname("Bob") ==>

** <true>

isname("Fandango") ==>

** <false>
To discover whether a list contains a name (and what that name is) we can use a `matches' command like this.

vars name, x, y;
[how much has Bob spent so far] matches [??x ?name ??y] ==>
produces

** <true>
and the value of `name' is

name ==>

** how
This is just the first element in the list. POP-11 has matched `y' against `[much has Bob spent so far]' and `??x' against `[]'. This is not the effect we wanted! We wanted the `name' variable to be matched agains Bob.

To make sure that the query variable can only be matched against a word that is a name, we set the `isname' procedure to be a restriction on the query variable. We do this by putting its name at the end of the query variable, preceded by a colon. Thus

vars name, x, y;
[how much has Bob spent so far] matches [??x ?name:isname ??y] ==>

** <true>

name ==>
** Bob
Experiment: Given the following:

vars relatives;
[father, mother, brother, sister] -> relatives;
define relation(word) -> truth_value;
  member(word,relatives) -> truth_value;
enddefine;
write a procedure that will give the following two interactions:

** [Which member of your family do you get on with best ?]
? I like my sister most
** [Tell me more about your sister]

** [Which member of your family do you get on with best ?]
? father
** [Tell me more about your father]

Organising your programs into files

When you start organizing your POP-11 code into procedures you cross the line between `playing with code' and programming proper. At this point, you need to start getting in the right habits. As your POP-11 code gets more complex you should think about splitting your work up into two files. You should have one file that contains all your POP-11 procedures and initializations. (The name of this file should have `.p' at the end so as to tell Poplog that it contains only POP-11 code.) Then you should have a second file that you use just for the purposes of executing specific POP-11 commands (i.e. `calling your program'). (You will also normally have your output file, of course, where printing is done.)

If you have all your POP-11 code in one file, you can load it all in one go by giving the Ved command `l1' when you are in that file (`l1' stands for `load this one'). You can then call your program by moving to the other file, marking the relevant procedure call, and executing it in with `lmr'.

If you don't adopt this strategy then you will find that your file(s) get in a mess. Loading up all the bits of code that you need to make a particular example work may involve hopping around in the file trying to find and load many different ranges of POP-11.

And while we are on the topic of shortcuts, there are some others worth bearing in mind. When you want to load a procedure, instead of marking the whole range and then doing `lmr' you can just put the cursor somewhere inside the procedure and give the command `lcp', which stands for `Load Current Procedure.' In fact, the `lcp' command can often be executed just by hitting `ESC c', i.e., by pushing down the excape key and hitting `c'. Similarly, the `lmr' command can usually be executed by pushing down the CTRL key and hitting `d'. Also, remember that Ved has a full repertoire of search and replace commands. For example, to replace all occurences of `happy' in the current file with `unhappy' you can give the command `gs /happy/unhappy'.


Tip of the day

To copy a range from one file to another, mark the range and then give the command `ti' in the other file.


Exercises

To test your knowledge of the material presented in this file you should do the following exercises. First, copy them into a file of your own. Then, edit in answers underneath each question (copying in any relevant output from your output file) and print out the file. Answers which involve writing POP-11 code should include some explanatory text.

  1. The following procedure is meant to take a list containing a name and produce a list containing a greeting including the name. It has at least two things wrong with it. What are they? Rewrite the procedure so that it works properly.

    define greet([name]) -> x;
        [pleased to meet you name] -> x;
    enddefine;
    
  2. Write a procedure that returns the third-to-last element of a list. The procedure should use the `matches' command. If the list is less than three elements long, the procedure should return <false>.

  3. The built-in procedure `isword' takes a single data-object as argument and returns a truth value that indicates whether the object is a word object. For example

    isword(chris) ==> ** <true>

    isword(8) ==> ** <false>

    Write a definition of a procedure that takes a list as argument and returns a value that is the just the number of words appearing in the list. The procedure should be called `countwords' and when called like this

    countwords([one two 3 four five 6 seven]) ==>
    
    it should print out

    ** 5
    
  4. Write a procedure called `biggest_num' that takes a list of numbers and returns the biggest one. It should behave like this.

    biggest_num([56 32 122 5 76]) ==>
    ** 122
    
    biggest_num([13 24 3 0 -5 42]) ==>
    ** 42
    
  5. Write a procedure called `roots' that takes a list of numbers, replaces all the numbers in the list with their square roots and then returns the updated list as a result. The behaviour should be like this

    roots([2 5 8 9 23]) ==>
    ** [1.41421 2.23607 2.82843 3.0 4.79583]
    
  6. Write another version of the `roots' procedure that produces the same output without updating elements of the original list. This new version should build up the result list bit by bit (using square-brackets, hats and double-hats) adding in one square-root value at a time.

  7. Write a procedure called `isanimal' that takes a single argument and returns <true> if the argument is a member of the list [dog cat snake bear tiger gorilla mouse], and <false> otherwise.

  8. Write a procedure that responds positively only if the user types in a sentence that mentions an animal contained in the list [dog cat snake bear tiger gorilla mouse]. The procedure should match each user sentence against a pattern in which the `isanimal' procedure is used as a restriction procedure. The procedure should respond with [the dog is a nice animal] if the input sentence mentions the word `dog', [the bear is a nice animal] if the input sentence mentions the word `bear' and so on. If the input sentence does not mention one of the listed animals the procedure should print out [I love animals].

  9. Write a version of `numeral_of' that returns the numeral for any positive number up to one thousand, i.e., which behaves like this

    numeral_of(458) ==> ** [four hundred and fifty eight]

    (To answer this you will need to use the `rem' and `div' operators. `rem' calculates the remainder from a division. Thus `5 rem 2 ==>' prints the remainder of dividing 5 by 2 which is 1. `div' sees how many times the second argument exactly divides the first argument. Thus `5 div 2 ==>' prints 2. Hint: by repeatedly dividing by 10 and looking at the remainder, it is possible to dig out successive digits in an arbitrarily large number.)


Quick reference

These are the commands introduced in this file.

:          in a pattern, attaches restriction procedure to query variable
sqrt      built-in procedure that returns the square-root of a number
member    built-in procedure that tests if an item is in a list
length    built-in procedure that returns the length of a data-object
delete    built-in procedure that deletes an item from a list
not       built-in procedure that returns `<true>' if given `false'
isnumber  built-in procedure that tests if an item is a number
isword    built-in procedure that tests if an object is a word
rem       built-in operator that computes the remainder of a division
div       built-in division operator that rounds result

Moving on

If you have completed all the exercises successfully, you are ready to move on to TEACH POPCOURSE7.


Page created on: Fri Apr 26 09:34:56 BST 2002
Feedback to Chris Thornton
hits this year