Decomposition

Real problems are too big to solve in one go. "Make the monthly sales report" is not something you can type in as a single instruction. Decomposition is the technique of splitting a problem into smaller subproblems, and those into smaller ones again, until each piece is small and clear enough to write directly. Working from the whole down to the parts like this is called top-down design.

Structure chart. "Make monthly report" splits into Collect data, Calculate figures and Present report. Collect data splits into Read sales file and Read refunds file; Calculate figures into Total per region and Compare months; Present report into Format table and Email managers. Make monthly report Collect data Calculate figures Present report Read sales file Read refunds file Total per region Compare months Format table Email managers
Top-down decomposition of a report task. Each box becomes a function; the boxes at the bottom are small enough to write directly.

Each box in the chart becomes a function, and the higher-level functions are written almost entirely as calls to the lower ones. The top level then reads like the plan itself:

FUNCTION make_monthly_report(month)
    sales ← collect_data(month)
    figures ← calculate_figures(sales)
    present_report(figures)
END FUNCTION

A small, runnable version of the same idea in Python:

def collect_data():
    # In a real program this would read files; here the data is fixed.
    return [("North", 1200), ("South", 800), ("North", 300)]


def total_per_region(sales):
    totals = {}
    for region, amount in sales:
        totals[region] = totals.get(region, 0) + amount
    return totals


def format_report(totals):
    lines = [f"{region:<6}{amount:>6}" for region, amount in sorted(totals.items())]
    return "\n".join(lines)


def make_report():
    sales = collect_data()
    totals = total_per_region(sales)
    print(format_report(totals))


make_report()
North   1500
South    800

Decomposition pays off in several ways. Each part can be written, tested and fixed on its own; several people can work on different parts at once; and when the requirements change — the report must go to a web page instead of email — only one small part changes. Deciding on the parts before writing code is also how algorithms are designed.

Abstraction

Abstraction means hiding how something works behind a simple description of what it does, so you can use it without knowing the details. Everyday life is full of abstractions. A car's pedals hide the engine, gearbox and brakes; a light switch hides the wiring and the power plant; an ATM hides the bank's databases and networks. You know what each control does, and that is enough.

Programming is built from layers of abstraction. When you call print("Hello"), you do not think about character encodings, operating-system calls or pixels on the screen. The people who wrote print thought about those, once, so that nobody else has to. Every function you write adds such a layer: once total_per_region works, the rest of the program can use it as a single step.

A signature as a contract

What a caller needs to know about a function is its signature — its name, its parameters and their types, and its return type — plus a short description of what it promises. Together they form a contract: "give me this, and I will give you that". In C, the signature is written out as a prototype, often in a separate header file:

/* Returns the arithmetic mean of the first count values.
   count must be greater than zero. */
double average(const double values[], size_t count);

In Python, type hints and a docstring play the same role:

def average(values: list[float]) -> float:
    """Return the arithmetic mean of values. values must not be empty."""
    return sum(values) / len(values)


print(average([18.5, 21.0, 19.5]))
19.666666666666668

As long as the contract stays the same, the implementation behind it can change freely — made faster, fixed, rewritten — without touching a single caller. That is the practical value of abstraction: it limits how far a change spreads.

Functions, procedures, subroutines and methods

A subroutine is a named block of code that performs a task and can be called from other places in a program; when it finishes, execution returns to the point right after the call. It is the oldest general term for the idea — it dates from the first stored-program computers of the late 1940s — and different languages use different names for variations of it:

Term Usual meaning Where you meet it
Subroutine general name for a callable block of code assembly, Fortran (SUBROUTINE), BASIC (GOSUB)
Function returns a value to the caller C, Python, JavaScript — used for every subroutine
Procedure performs an action, returns no value Pascal (procedure), SQL stored procedures; a void function in C
Method a function that belongs to an object or class Java, C#, Python (prices.append(3))

In everyday speech the terms overlap, and "function" is the most common. In Python, every subroutine is a function and returns a value; one without a return statement returns None. Methods and objects are covered in Object-Oriented Concepts.

Parameters and return values

A function communicates with its caller through two channels. Parameters are the input: named placeholders in the function's definition that receive values when it is called. The actual values passed in a call are the arguments. The return value is the output: the result the function hands back, which the caller can store or use in an expression.

FUNCTION area(width, height)
    RETURN width * height
END FUNCTION

room ← area(4, 5)

Here width and height are the parameters, 4 and 5 are the arguments, and the return value 20 is stored in room.

A well-designed function gets everything it needs through its parameters and delivers everything it produces through its return value. The page Functions in the Python topic covers default values, keyword arguments, scope and more.

How arguments are passed

When a function changes one of its parameters, does the caller see the change? The answer depends on how the language passes arguments, and it is one of the most common sources of confusion when moving between languages.

Pass by value

With pass by value, the function receives a copy of each argument. Whatever it does to its copy, the caller's variable is untouched. C always passes arguments this way:

#include <stdio.h>

static void add_bonus(int score) {
    score = score + 10;              /* changes the copy only */
    printf("inside:  %d\n", score);
}

int main(void) {
    int score = 50;

    add_bonus(score);
    printf("outside: %d\n", score);
    return 0;
}
inside:  60
outside: 50

Pass by reference, and pointers

With pass by reference, the parameter becomes another name for the caller's variable, so assigning to it changes the original. C++ (int &score), C# (ref) and PHP (&$score) support this. C does not, but it gets the same effect by passing a pointer: a copy of the variable's memory address. Through that address, the function can reach the original.

#include <stdio.h>

static void add_bonus(int *score) {
    *score = *score + 10;            /* changes the value at that address */
}

int main(void) {
    int score = 50;

    add_bonus(&score);               /* pass the address of score */
    printf("after: %d\n", score);
    return 0;
}
after: 60

The & in the call makes the intent visible to anyone reading it: this function may change score. Pointers and addresses are explained in Memory Management.

Pass by object reference (Python)

Python fits neither label exactly. A Python variable is a name that refers to an object, and calling a function binds the parameter name to the same object the argument refers to. This is often called pass by object reference or call by sharing. JavaScript and Java (for objects) behave the same way. Two things follow:

  • If the function mutates the object — appends to a list, changes a dictionary — the caller sees the change, because there is only one object.
  • If the function rebinds the parameter to a new object with =, only the local name moves; the caller's variable still refers to the original.
def add_item(basket):
    basket.append("pear")         # mutates the shared list


def replace_basket(basket):
    basket = ["melon"]            # rebinds the local name only


fruit = ["apple"]

add_item(fruit)
print(fruit)

replace_basket(fruit)
print(fruit)
['apple', 'pear']
['apple', 'pear']

Numbers and strings are immutable in Python: they cannot be changed in place, only replaced. So a function that receives an int can never affect the caller's variable — which is why, for numbers, Python looks like pass by value.

Side effects and pure functions

A side effect is any change a function makes that outlives the call, other than returning a value: changing a global variable, mutating an argument, printing, writing a file, updating a database, sending a network message. Reading from outside — the clock, a global setting — does not change anything, but it has a similar cost: the result no longer depends only on the arguments. (A random number generator does both: it reads and advances hidden state.)

A pure function avoids both. Its result depends only on its arguments, and calling it changes nothing else. Call it twice with the same arguments and you get the same result twice, guaranteed — just like a function in mathematics.

tax_rate = 0.21
price_log = []


def price_with_tax_impure(net):
    price_log.append(net)                    # side effect: changes a global list
    return round(net * (1 + tax_rate), 2)    # depends on a global variable


def price_with_tax(net, rate):
    return round(net * (1 + rate), 2)        # pure: arguments in, result out


print(price_with_tax_impure(100))
tax_rate = 0.09                              # someone changes the global...
print(price_with_tax_impure(100))            # ...and the same call gives another result
print(price_log)

print(price_with_tax(100, 0.21))
print(price_with_tax(100, 0.21))
121.0
109.0
[100, 100]
121.0
121.0

Why pure functions are easier to test

To test a pure function, you call it with some arguments and compare the result with what you expect. There is nothing to set up beforehand and nothing to clean up afterward, and the test gives the same answer every time it runs. Testing the impure version means first putting the global variables in a known state and afterward checking what else changed.

def apply_discount(price, percent):
    return round(price * (1 - percent / 100), 2)


assert apply_discount(80, 25) == 60.0
assert apply_discount(19.99, 0) == 19.99
assert apply_discount(50, 100) == 0.0
print("all tests passed")
all tests passed

A program without side effects could not do anything useful — it could not even show its result. The practical rule is to keep the calculations pure and push the side effects (input, output, saving) to a thin outer layer, as make_report did above: total_per_region and format_report are pure, and only make_report prints. Testing is covered further in Debugging and Testing, and a whole style of programming built on pure functions in Programming Paradigms.

Modularity and reuse

Functions are the smallest unit of reuse. Group related functions into a file, and you have a module; group modules, and you have a library or package that many programs can use. Designing a program as separate parts with clear boundaries between them is called modularity. Most of the code any program runs was written by someone else: the language's standard library, the operating system, and third-party packages.

The boundary of a library is its API (application programming interface): the documented set of functions, their signatures and their promised behavior. An API is abstraction on a larger scale. You call math.sqrt(2) without knowing the algorithm behind it, and a web service's API lets your program use a payment system or a weather forecast without seeing a line of its code. As long as the API stays stable, both sides can change their internals independently. Modules with high cohesion and low coupling are the subject of Design Principles.

In Python, modules and packages are covered in Modules, Packages, pip and Virtual Environments, and the built-in library in The Standard Library.

Naming functions well

A function's name is the most visible part of its abstraction: callers read the name far more often than the body. A good name says what the function does or returns, so that the reader never has to look inside.

  • Name actions with a verb: send_invoice, calculate_tax, load_settings.
  • Name functions that answer yes or no as a question: is_valid_email, has_stock, can_edit.
  • Be consistent: if one function is called get_customer, do not call its sibling fetch_order.
  • Follow the language's convention: snake_case in Python and C, camelCase in JavaScript and Java.