There are many parameter passing mechanisms that are used to pass data into, out of or both into and out of procedures or functions being accessed. What is said here applies to procedures and functions in the Pascal sense and to functions in the C sense; so I’ll omit ‘or function’ for the rest of the article.
Mechanisms for Passing Data into Procedures
There are basically three types of input passing mechanism:
- call by value;
- call by constant-value;
- call by reference-constant.
Note that this list doesn’t include call by reference – see later.
Most languages support only call by value, although ADA, for example, supports call by constant-value and can support (implementation dependent) call by reference-constant.
In call by value the actual parameter’s value is passed as an initial value to the formal parameter at the time the procedure call is made and the formal parameter (total in the following example) then acts as a local variable within the called procedure (i.e., it can be modified within the procedure). So,
procedure DoSomethingWith(total : integer); {in Pascal}
var symbol : char;
begin
while total > 0 do
begin
read(symbol);write(symbol);
total := total - 1
{ total is a local variable in Pascal }
{ so this is OK }
end
end { DoSomethingWith };
In some versions of Pascal a formal parameter isn’t allowed to be the controlling variable of a ‘for’ loop, so this wouldn’t compile using the ProPascal compiler:
program fortest;[Note that, for example, Turbo Pascal (PC) does allow this sort of thing to compile if the formal parameter j is a value parameter (as shown) whereas it fails to compile if j is declared as a var parameter (which is very sensible!)]
var z : integer;
procedure test(j : integer);
begin
for j := j downto 0 do
writeln( j ) { this line may fail to compile }
{ depending on the compiler }
end { test };
begin { fortest }
z := 10;
test( z )
end { fortest }.
In call by constant-value, the formal parameter simply receives the value of the actual parameter and then acts as a constant within the procedure. In ADA this is implemented using the ‘in’ mode keyword (which is implied if no mode is given explicitly), e.g,
procedure do_something_with(total: in integer) is
{ in ADA }
symbol: character;
count : integer := total;
{ required in ADA as total is constant }
begin
while count > 0 loop
get(symbol); put(symbol);
count := count - 1;
{ can't use total := total - 1 }
end loop;
end do_something_with;
in this case ‘total’ is treated purely as a constant within the procedure and the variable count is needed to control the loop iterations.
The main problem with call by value and call by constant-value is that when aggregates such as Pascal (or Modula-2) records are passed to procedures, then exact copies of the actual parameters need to be made and placed in the formal parameters, which is very expensive in both space and execution time. A common work-around in Pascal and Modula-2 is to pass the parameters using call by reference, where the address of the aggregate rather than its contents is passed into the procedure, but this allows the aggregate to be modified by the procedure (see later).
In some implementations of ADA when aggregates are passed using ‘in’ mode they are passed by reference-constant, in which the reference is treated locally as constant within the procedure and so its elements cannot be updated, only referred to and copied.
For all three of call by value, call by constant-value and call by reference-constant the value of the actual parameter is unaffected by the processes carried out within the procedure body.
In QL SuperBasic the simple rule is that any expression [literal numeral, or variable *+-/ literal numeral or variable or similar combination] is passed to the procedure or function by value. The interpreter really can’t do anything else. It has to evaluate the expression and then assign its result to a temporary storage location which is the formal parameter in the proc/func’s declaration, i.e., x in Tim’s example procedure inc(x) in QHJ #24. This x may be modified within the procedure (call by value).
Mechanisms for Passing Data out of Procedures
Most languages do not have a specific mechanism for passing information out of procedure (unless the parameter is both in and out), but ADA supports call by result which is implemented as an ‘out’ mode parameter. Such ‘out’ mode formal parameters act as if they are uninitialized local variables on entry to the procedure and the last value that they receive during the procedure is returned in the actual parameter. So in ADA you can get this sort of thing:
procedure read_neg_num( negative : out integer) is
number : integer;
begin
get(number);
while number > 0 loop
put_line("Number not negative, try again");
get(number);
end loop;
negative := number;
end read_neg_num;
In Algol-W call by result is also used, but by a slightly different mechanism when compared with ADA. If the formal parameter is a structure, then the same local copy problems occur as with input parameters; so ADA allows aggregates to be passed ‘out’ by reference. Pascal and Modula, etc., don’t support the call by result mechanism at all and use call by reference to return values to calling procedures.
Mechanisms for Updating Data Passed to Procedures
The first mechanism that achieves updating is call by value-result which combines call by value and call by result. In this the formal parameter acts as a local variable, which is initialized to the value of the actual parameter. During the procedure’s execution changes to the formal parameter only affect the local copy, but the actual paramater is updates to the latest value of the local copy on exit from the procedure. ADA achieves call by value-result for its ‘in out’ mode parameters, but uses call by reference (qv) for aggregates.
The second mechanism is call by reference (and is by far the most common) and Pascal, Modula, C, FORTRAN, PL/I, SuperBasic, etc., are all capable of updating parameters using this approach. In some cases call by reference is the default (FORTRAN, PL/I) in others call by value (Pascal, Modula, ADA) and in others it depends on the parameter type – in C arrays are passed by reference as a default, everything else is by value.
In SuperBasic when the actual parameter is the name of a variable [not contained in an expression], the interpreter has a choice – as Tim says in QHJ #24 – it can pass the address of the variable (its reference) or its value. All implementers of compilers and interpreters need to decide what to do in this situation as default. The default for SB is by reference, but it is simple to pass by value when required – you simply create an expression using parentheses around the variable name and the result is passed by value.
There is another type of updating call mechanism “call by name” that was used in Algol 60. In this the name of the actual parameter effectively textually replaces the occurrence of the formal parameter in the procedure body, with the address of the actual parameter being recalculated each time it is needed. This is much more expensive to implement and in execution than call by reference, so it has not been used in more modern languages.
Other Issues
Some languages (C++, ADA and others) support overloading of procedure and function names wherein different instances of the same procedure name are distinguished by their parameter types. For example in ADA,
procedure swap(a,b : in out real) is
begin
...
end swap;
procedure swap(a,b : in out integer) is
begin
...
end swap;
The compiler sorts out which procedures is being called by inspecting the types of the parameters and then uses the appropriate version.
Normally actual parameters and formal parameters are associated by the order in which they occur in the procedure call and procedure declaration, i.e., 1st formal parameter = 1st actual parameter, and so on. In PL/I and ADA it is possibly to make associations explicitly by name (Named Association), for example in ADA,
procedure inc(val : in out integer;
by : in integer) is
begin
val := val + by;
end inc;
and the call could be written in any of the following ways,
inc( number, 2 );
inc( number, by => 2 );
inc( val => number, by => 2 );
or even,
inc( by => 2, val => number );
all with exactly the same effect!
Some languages support the idea of default values (C++, ADA) for formal parameters so that if no parameter is supplied explicitly the default value is used. For example in ADA if ‘by’ is defaulted to 1, then,
procedure inc(val : in out integer;
by : in integer := 1 ) is
begin
val := val + by;
end inc;
inc(number);
is identical in effect to
inc(number,1);
You can also pass procedure or function names as parameters in some languages – food for another article sometime…
One thing that I try to do when writing procedures that I believe is a good idea is to only allow the code in the procedure to modify or use either local variables or parameters. In other words I make every effort NOT to use, and especially NOT to modify, global variables or non-local variables that are effectively global to the procedure under the scope rules. This makes it much easier to identify from the procedure code what is its precise effect. When I am constrained to modify globals or other non-local variables in scope then I tend to use comments to this effect in the code, e.g.,
procedure SomethingOrOther( var x : integer;
var y : real );
(* GLOBALS: var a : SomeRecordType;
{this is read-write accessible}
b : SomeOtherRecordType;
{this is read} *)
(* NON-LOCALS: var q : boolean;
p : integer; *)
label ...;
const ...;
type ...;
var ...;
procedure ...;
function ...;
...
begin {SomethingOrOther}
...
end {SomethingOrOther};
This makes it quite clear to someone reading the code that the identified variables are potentially modified by the procedure. It suits me – others will, no doubt, hate it, but I defend it on maintenance grounds. I would still prefer to pass the items as parameters even if the parameter list gets very long as a result.
Some Thoughts on Functions
Functions and procedures only really differ in that the former return values directly to the calling procedure. In FORTRAN, ALGOL 60 and Pascal this is achieved by assigning a value to the function name within the function body. In later languages such as C and Modula a ‘return’ statement is used to pass a value to the caller. [NB In ADA, C and FORTRAN the return statement can also be used without an argument to exit a procedure]
Functions can usually take both value and reference parameters too; so there is the possibility of functions returning lots of values!! This has to be a really bad idea, but the languages let you do it!!!
In fact, ADA prevents ‘out’ and ‘in out’ mode for function parameters, but still allows the programmer to ‘mess about’ with globals and variables in scope – gruesome! . I believe that EUCLID and quite a few functional languages do prevent such social solecisms, but I only wish C, C++ (yes!), Pascal and others did too. We’d all be much better programmers (or at least we’d produce better source code) if language designers reduced the possibility of side-effects that can be created (all too often unwittingly!)