[net.lang] nested procedures.

steven@mcvax.uucp (Steven Pemberton) (07/28/86)

(A long time ago,) in article <149@opus.UUCP> rcd@opus.UUCP (Dick Dunn) wrote:
> The worst of it is that <<nested procedures are seldom necessary>> IF a
> language provides other hiding mechanisms.  Simple as it is, C's rule for
> limiting the scope of static objects to a compilation unit is entirely
> adequate for this sort of hiding.

Let me try and give a simplified description of a case where nested
procedures are useful, i.e. functional, not just cosmetic.

You are implementing an interpreter for a programming language. Among the
simple types in this language are unbounded length integers. Among the
compound types are sequences of any other type: so you can have sequences of
integers, but also sequences of sequences of integers, and so on. Sequences
are written e.g. {1; 2; 100000}, and there is a shorthand for ranges:
{1..100000}. The implementation should be able to handle ranges where the
lowerbound is very far from the upperbound (eg {1..10**1000}).

Among the simple commands of the language is a write command that can write
values of any type. Among the compound commands is a for-loop
	FOR name IN sequence : commands
which executes 'commands' for each element of the sequence, e.g.
	FOR i IN {1..1000000}: WRITE i**2

Now you want to implement this language in a good modular fashion: you want
to write a separate module to implement the values of the language, so that
the interpreter proper can be written without reference to how the values
are implemented. This will then allow you to try different representations
for values, without constantly having to rewrite the interpreter.

Now, how do you arrange it so that the interpreter can step through each
element of a sequence without knowing how they are represented? In
particular, suppose you decide to hold ranges internally as only the
lower-bound and upper-bound?

One possibility is to supply a routine in the values module that steps
through each element of a supplied sequence, calling a supplied routine.
In Pascal:

	procedure forEach(seq: value; procedure action(elem: value));
	begin
	     { For each element e of sequence seq, call action(e) }
	end;

Now you can code the procedure to effect the write command as follows:

	procedure doWrite(v: value);
	var first: boolean;

	     procedure writeElement(element: value);
	     begin
	          if not first then write('; ');
	          first:=false;
	          doWrite(element)
	     end;

	begin
	     if simple(v) then writeSimple(v)
	     else
	          begin
	               first:=true;
	               write('{');
	               forEach(v, writeElement);
	               write('}')
	          end
	end;

Similarly, you can implement the for command as follows:

	procedure executeFor(name, expression, commands: parseTree);
	var loc: location;
	    val: value;

	    procedure loop(element: value);
	    begin
	         assign(element, {to:} loc);
	         execute(commands)
	    end;

	begin
	     loc:=locate(name);
	     val:=evaluate(expression);
	     forEach(val, loop)
	end;

Nested procedures give you the option to choose this method of implementation.

Steven Pemberton, CWI, Amsterdam; steven@mcvax.uucp
(Sorry I took so long to follow up. I was thinking about it.)

ludemann@ubc-cs.UUCP (Peter Ludemann) (07/30/86)

In article <7026@boring.mcvax.UUCP> steven@mcvax.uucp (Steven Pemberton) writes:
>
>Let me try and give a simplified description of a case where nested
>procedures are useful, i.e. functional, not just cosmetic.
>
> ...
>
>One possibility is to supply a routine in the values module that steps
>through each element of a supplied sequence, calling a supplied routine.
>In Pascal:
>
>	procedure forEach(seq: value; procedure action(elem: value));
>	begin
>	     { For each element e of sequence seq, call action(e) }
>	end;
>
>Now you can code the procedure to effect the write command as follows:
>
>	procedure doWrite(v: value);
>	var first: boolean;
>
>	     procedure writeElement(element: value);
>	     begin
>	          if not first then write('; ');
>	          first:=false;
>	          doWrite(element)
>	     end;
>
>	begin
>	     if simple(v) then writeSimple(v)
>	     else
>	          begin
>	               first:=true;
>	               write('{');
>	               forEach(v, writeElement);
>	               write('}')
>	          end
>	end;


This is a good example of why nested procedures should NOT exist.  
The procedure writeElement has a side-effect: modifying the variable 
"first".  Because this example is small, it is easily comprehended, 
but a larger example could be quite incomprehensible.

I've written lots of these kinds of systems using a language with no 
nested procedures.  There is a straightforward way of handling such 
cases: pass the control information explicitely to the "action" 
procedure.  This requires an extra initialisation procedure: 

	procedure forEach(seq: value; 
			  procedure actionInit(var control: bool);
			  procedure action(var control: bool; elem: value));
	begin
	    var control: bool;
	    actionInit(control);
	    { For each element e of sequence seq, call action(control, e) }
	end;

	procedure writeInit(var first: bool);
	begin
	    first := true;
	end;

	procedure writeElement(var first: bool; element: value);
	begin
	    if not first then write('; ');
	    first := false;
	    doWrite(element)
	end
	
	procedure doWrite(v: value);
	begin
	     if simple(v) then writeSimple(v)
	     else
	          begin
	               write('{');
	               forEach(v, writeInit, writeElement);
	               write('}')
	          end
	end;

Of course, this requires knowing the type of the "control" variable 
(it could be a fairly large record, containing perhaps a character 
count, an indentation level, or whatever).  I have ideas on hiding
this information, if anyone is interested.

steven@mcvax.uucp (Steven Pemberton) (08/04/86)

In article <337@ubc-cs.UUCP> ludemann@ubc-cs.UUCP (Peter Ludemann) writes:
[ Example program from original posting ]
> This is a good example of why nested procedures should NOT exist.  
> The procedure writeElement has a side-effect: modifying the variable 
> "first".  Because this example is small, it is easily comprehended, 
> but a larger example could be quite incomprehensible.

I'm unclear about this point. Procedures by definition have side-effects.
Maybe you mean that it modifies a non-local variable, but that is another
question altogether. Anyway, if the example is comprehensible, it stands.
Any use of procedures could become incomprehensible, but that's no argument
against procedures.

> I've written lots of these kinds of systems using a language with no 
> nested procedures.  There is a straightforward way of handling such 
> cases: pass the control information explicitely to the "action" 
> procedure.  This requires an extra initialisation procedure: 
> 
> 	procedure forEach(seq: value; 
> 			  procedure actionInit(var control: bool);
> 			  procedure action(var control: bool; elem: value));
> 	begin
> 	    var control: bool;
> 	    actionInit(control);
> 	    { For each element e of sequence seq, call action(control, e) }
> 	end;

It also requires a new version of forEach for every different type of action
procedure you need to call. Even the example program requires two: one for
writeElement which has a boolean parameter, and another for loop which has a
'location' parameter.

Steven Pemberton, CWI, Amsterdam; steven@mcvax.uucp
Lrf, vg'f ebg guvegrrarq!

cjl@iuvax.UUCP (08/13/86)

> It also requires a new version of forEach for every different type of action
> procedure you need to call. Even the example program requires two: one for
> writeElement which has a boolean parameter, and another for loop which has a
> 'location' parameter.

    A procedure without procedure parameters can be asserted with 
pre- and post-conditions. The inclusion of procedure parameters makes
it almost impossible to assert the pre- and post-conditions. In other words
procedures with procedure parameters are just control macros, not solid
abstraction. This may be the reason why Ada eliminates procedure parameters.
In Ada, you can only have generic procedures with procedure parameters.
You must instantiate generic procedures for each use of different procedure
parameters. That enforces the abstraction and descent use of every procedure.
If this represents a good software engineering principle, then it may
be better to write two different versions of forEach for this example
than to share codes with one forEach and non-local variable references.


C.J.Lo
Dept. of CIS, IUPUI
UUCP : ...!iuvax!cjl
ARPA : cjl@Indiana@CSNet-Relay

aglew@ccvaxa.UUCP (08/17/86)

>    A procedure without procedure parameters can be asserted with 
>pre- and post-conditions. The inclusion of procedure parameters makes
>it almost impossible to assert the pre- and post-conditions. In other words
>procedures with procedure parameters are just control macros, not solid
>abstraction. 

Bullshit. You can provide pre and post conditions for a procedure only
because you are able to do so for each statement in the procedure. An
invocation of a procedure parameter can have valid procedure parameters
just as easily as any other parameter. These must be satisfied by
all procedures that can be passed to that parameter.

Unfortunately, satisfaction of these pre and post conditions is rarely
checked automatically for procedure parameters, but then, neither is it
done for complex data structures.