Defining New Words Lesson 4 Contents

We said earlier that you can add words to the Mops dictionary while building a program. In fact, that is what programming in Mops is all about. Class names, method names, and object names become part of the dictionary in your program. Defining new Mops words also lets you write your own routines and subroutines by defining one short and simple word to take the place of several or many other Forth statements. Thus you can factor your program, a very important programming concept and technique.

Special Note:
Unless you save the dictionary you've created for a program, the words and definitions will not be remembered when you Quit Mops. In the remainder of this tutorial, you will be defining new words that pertain only to this tutorial. If you wish to save the current state of the dictionary at the end of a lesson, then choose Save As... from the File menu and type in a name for the file, or else you may choose Save if you have already given it a name. You will be able to recall that particular dictionary at a later time by doubleclicking on its icon.

The first exercise will be to define a new word that mimics a simple addition operation. The new word is called "Add", although you could choose any name not already in the Mops dictionary.

The safest way to check that the name of a new word you want to define is not already in the dictionary, is to issue the "tick" command with the name you want to test for.
In Mops, a tick is an apostrophe. By typing apostrophe, space, and the name of the word you're testing for, Mops searches the dictionary for an occurrence of that word. If the word is in the dictionary, tick will leave a number on the stack (the location in memory of the word's definition). But if the word is not in the dictionary, the message "not found" appears on the screen, and you're in the clear to define a word with that name. (Remember to use «enter» at the end of every line you type):


 ' window .
4962146 

' twindow .
Error # -13 : undefined word
' twindow .
         ^
Current object: TW    Class:
Stack: Depth 1
 9568706 $9201C2
Return stack: Depth 43
 9513434 $9129DA ?NOTFOUND
 9513710 $912AEE '
 9502126 $90FDAE (no name)   

You define a new Mops word by typing a colon, a space, the name of the new word, one or more spaces or tabs, the sequence of values and/or commands to be performed when you use that new word, and then a final semicolon, indicating the end of your new definition.
This kind of Mops definition is called, aptly enough, a colon definition. Note that :class and :m are other, more special purpose defining words.

Here's an example that defines the new word, "add", which will perform the addition of two numbers on the stack, display the results, and move the Mops prompt to the left margin of the next line. (We include the stack comment here for completeness and as an example of good practice; you need not type it in.) Recall that each word definition must be terminated by the «enter» key in order to compile:

: add ( n1 n2 -- ) + . cr ;

If you see text like this, then that means you have not compiled the new word, so the word definition was not found:

Return stack: depth 29 
  $4AABEE    MLTEFWINDMOD    class:  MODULE
  $47A7F6    DIE
  $482152    ?NOTFOUND

The + operation expects to find two numbers on the stack, so to use your new word you would type two numbers (which go onto the stack) and then the new word:


2 6 add
8

Remember you can press option-«enter» to suppress creation of a lineending after your command line in the QuickEdit window. On my keyboard, «enter» is shift-«return», so that makes a total of three keys I am pressing just to compile one line.

A good exercise at this point would be to define new words to perform some of the other arithmetic operations, and just leave them all in the dictionary. It won't do any harm.

The Return Stack
A Mops program basically consists of a sequence of words and messages sent to objects (which cause a method to be executed). The definitions of these words and methods can contain many other words and messages. If you think about what must happen when Mops is executing one definition or method, you can see that when it has to go and execute other words or methods, it will then need to come back to where it was. It needs to mark its place in some way. The way this is handled is with a second stack, called the return stack. When Mops has to execute something somewhere else, it saves its current position as an address on the return stack. In that other place, if it has to go yet somewhere else, it pushes the new address on to the return stack as well. This is how words or methods can call other words or methods which can in turn do the same, down to a great depth. And by using a second stack, all these return addresses on the return stack don't interfere with items on the parameter stack.

Normally you won't need to worry about what's going on with the return stack. When there's an error however, it's usually very useful to know what the program was executing, and where it had come from before that. Our error dump includes a dump of the return stack. Mops puts some other items besides return addresses on the return stack, but for items there which look like return addresses from within a word, the dump will include the name of the word. If you look at the dump just above for example, you can see that the address of ' (tick) is underneath the address of ?NOTFOUND. This shows that ' called ?NOTFOUND and it was at that point that the error was detected. (?NOTFOUND in fact checks the result of a dictionary search to see if it was successful).

We can't display the names of methods in this way, however, since the names of methods aren't stored in a readable form. But just seeing the names of the words which were executing at the time of an error can give you very useful information.

Named Input Parameters
Mops can make things a little easier for you by reducing concern about the order in which data are stored on, and recalled from, the parameter stack. Whenever you define a new Mops word, Mops lets you assign names to the parameters that are passed to it. After that, you needn't worry about the stack or the order of the data. When you need a datum for an operation, simply refer to it by the name you have assigned to it.

As an example, we will use the multiply-then-divide problem described in lesson 3. If you recall, the operation was presented as:

5 * 12 * 50
    40

To calculate this without named input parameters as shown in lesson 3, you had to multiply the three numbers in the numerator, and then place the denominator on the stack before dividing.
See how this is simplified in a definition that performs the math with named input parameters:

: formula  { denom n1 n2 n3 -- solution }
            n1 n2 n3 * * denom /  ;

The magic of named input parameters takes place inside the curly brackets. The syntax is deliberately similar to a stack comment, because it is in fact a kind of stack description. So in this case, whenever the word "formula" is executed, like this:


40 5 12 50 formula .
75
the first thing that happens is that the values are taken from the stack and put in a special area of memory where they are associated with the names in the curly brackets in the same order as they were put on the stack. Once that happens, their order is no longer important. Their names are used to fill in the values in the calculation.

But note that the "solution" parameter is actually a comment—anything between the -- and the } is treated as a comment. You should use this comment area to indicate what your definition leaves on the stack, exactly as in a normal stack comment.

It is important to bear in mind that the names and values you assign to named input parameters are valid only within their own colon definition. You could use the same names with the same or different values in other colon definitions without any interference.

Named input parameters become very powerful in the way you can adjust their values in the course of a colon definition.
Consider for example, this formula:

a² + b²

Since the computer can compute only one square at a time, it needs to hold the result of one square while it calculates the second before it can add the two squares. A definition for the equivalent word would be:

: formula1  { a  b -- solution }
            a  a  *  -> a
            b  b  *
            a  +    .  cr  ;

The arrow (gazinta) operation (->) stores the value currently on the stack (the result of a-squared) into the named parameter, a. This overwrites the original value in a, which came from the stack in the opening instant of this definition's execution. Near the end of execution, a is recalled to be added to the results of b times b. To solve the same formula without named input parameters would require several stack manipulations that sometimes trip up even the experts.

Incidentally, there are other operations you can perform on a number stored as a named input parameter. You can add a number to what is there, or subtract a number from what is there, with the ++> and --> operations.
For example

10 ++> denom

inside a colon definition adds ten to the value stored in the named input parameter named denom.

Local Variables
While we're at it, we'll also introduce you to a similar concept, called local variables. They too, appear inside curly brackets within a colon definition, but instead let you assign names to intermediate results that can occur inside such a definition. Local variables are preceded by a backslash.
Take, for instance, the formula,

( a + b - 3c ) / ( b + 2c )

The word definition would be:

: formula2  { a  b  c  \  num den -- result }
            a  b  +  3  c  *  -  -> num
            2  c  *  b  +        -> den
            num  den  /  ;

In this example, a, b, and c in the curly brackets are named input parameters that take on the values on the stack. The backslash indicates that the names to the right are local variables that will be called into action within the definition. In the example, the numerator and denominator are calculated separately and stored (->) in their respective local variables. Then, the local variables are recalled in the proper order for the division operation to produce the result.

An important thing to remember is that local variables aren't initialized to any particular value at the beginning of the definition. Don't assume they're initially zero, let's say. They might have anything at all in them, and it might be different in different runs of your program. Thus your first use of a local variable should be to store something into it with ->.

Previous Chapter Contents Next Chapter
This page online:  http://PowerMops.com/MopsManual/Lessons/Chapter4.html