Subroutines, parameters and passing by reference

Out-of-line subroutines and their interfaces, returning several values, and passing by value and by reference.

A1.4Programming techniques and object-oriented programmingA level20 min

Do this lesson in the simulator

At GCSE you wrote functions and procedures with parameters and return values (F4.1 and F4.2). A level asks you to explain precisely what a subroutine is and why it helps, to describe its interface, to return more than one value, and to know what happens to an argument when it is passed by value or by reference.

What a subroutine is

A subroutine is a named, out-of-line block of code that is run by writing its name. Out of line means it is written once, apart from the code that uses it; each call jumps to it, runs it, and comes back to the line after the call. A function returns a value; a procedure does not. Python writes both with def.

The advantages are the ones examiners want by name:

  • Reuse: written once, called as often as needed, so a fix is made in one place.
  • Decomposition: a large problem becomes a set of small subroutines, each easy to understand.
  • Testing: each subroutine can be tested on its own before it is used.
  • Teamwork: different people can write different subroutines, agreeing only on how they are called.
  • Libraries: tested subroutines can be shared between programs, like the robot's forward and distance.

The interface

The interface of a subroutine is what a caller needs to know to use it: its name, its parameters with their types and meaning, and what it returns. The body can change without breaking any caller, as long as the interface stays the same.

# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()

def gap_after(move_cm: float, margin_cm: float = 10) -> float:
    """How far the wall will be after driving move_cm, less a safety margin."""
    return distance() - move_cm - margin_cm

print(gap_after(20))
print(gap_after(20, margin_cm=5))

Run this in the simulator

The type hints (float) and the docstring document the interface. margin_cm has a default value, so a caller can leave it out. A parameter is the name in the definition; an argument is the value given in the call.

Returning more than one value

A Python function returns one value, but that value can be a tuple, which the caller unpacks into several variables:

def closest(readings):
    best = 0
    for i in range(1, len(readings)):
        if readings[i] < readings[best]:
            best = i
    return best, readings[best]

index, cm = closest([31.0, 41.0, 51.0, 19.0, 15.0, 20.0])
print("index", index, "distance", cm)

Run this in the simulator

In AQA's pseudo-code a subroutine is written with SUBROUTINE and returns a value with RETURN:

SUBROUTINE closest(readings)
  best ← 0
  FOR i ← 1 TO LEN(readings) - 1
    IF readings[i] < readings[best] THEN
      best ← i
    ENDIF
  ENDFOR
  RETURN best
ENDSUBROUTINE

By value and by reference

When an argument is passed by value, the subroutine gets a copy of the value. Changing the parameter inside the subroutine does not change the caller's variable. When an argument is passed by reference, the subroutine gets the address of the caller's variable. Changing the parameter changes the caller's variable, because both names refer to the same place in memory.

OCR's exam reference language marks each parameter:

procedure nudge(cm:byVal, total:byRef)
    cm = cm + 5
    total = total + cm
endprocedure

step = 10
driven = 0
nudge(step, driven)
print(step)      // 10: cm was a copy
print(driven)    // 15: total was the caller's variable
By value By reference
The subroutine receives A copy of the value The location of the caller's variable
Changes inside affect the caller No Yes
Memory and time The copy costs space and time for large data No copy, so large data is passed cheaply
Safety The caller's data cannot be damaged Side effects are possible, intended or not

What Python does

Python does not let you choose. Every argument is passed as a reference to an object, and what you see depends on what the subroutine does with it:

def nudge(cm):
    cm = cm + 5            # makes the local name refer to a new integer
    print("inside:", cm)

def log_reading(readings, cm):
    readings.append(cm)    # changes the list the caller also refers to

step = 10
nudge(step)
print("after nudge:", step)

survey = []
log_reading(survey, 31.0)
log_reading(survey, 51.0)
print("after logging:", survey)

Run this in the simulator

nudge behaves like passing by value: integers cannot be changed, so cm + 5 makes a new integer and points only the local name at it. log_reading behaves like passing by reference: append changes the one list that both readings and survey refer to, just as the two names did in lesson A1.1. Assigning a new list, readings = [99], would not affect survey, because that only moves the local name.

So in Python, to change a caller's number, return the new value and let the caller assign it. To fill in a list or change an object, pass it in and change it; there is no need to return it. If a subroutine must not change a list it is given, pass it a copy, list(survey).

Task: survey by reference

Write two subroutines.

  • The procedure survey(readings) takes an empty list readings. It takes eight distance readings, turning right 45 degrees after each one, so the robot ends facing where it started, and appends each reading to readings. It returns nothing, and the main program calls it on a line of its own, survey(readings).
  • The function nearest(readings) takes a list of real numbers and returns two values: the index of the smallest reading (the first one if two are equal) and that reading. Find it with a loop; do not use min, index or global.

Then print nearest: <cm> cm at <degrees> degrees, where the degrees are the index times 45, turn right by that many degrees to face the nearest wall, and play one note.

# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()

def survey(readings):
    pass

def nearest(readings):
    return 0, 0.0

readings = []
survey(readings)
index, cm = nearest(readings)
print(f"nearest: {cm} cm at {index * 45} degrees")

Challenges

  1. Write survey_copy() that makes its own list and returns it. Which version is easier to test, and why?
  2. Inside survey, add readings = [] as the first line. What does the main program see now, and why?
  3. Write nearest_two(readings) that returns the indexes of the two smallest readings.