Unit 3: Functions
Learning Objectives
After completing this unit, students should be able to:
- explain functions as an abstraction mechanism that separates what a computation does from how it is implemented.
- define and use functions in Java with appropriate parameters and return types, including void, to structure computation.
- reason about programs using the abstraction barrier, distinguishing the responsibilities of a client from those of an implementer.
- use functions to reduce code complexity through locality, information hiding, and reuse.
- identify violations of the abstraction barrier and explain why they lead to brittle or error-prone code.
Overview
As programs grow larger, complexity becomes the primary challenge. One of the most fundamental tools for managing this complexity is the function.
At first glance, a function may seem like nothing more than a convenient way to group statements and give them a name. But in CS2030/S, we are interested in a deeper role that functions play: functions are an abstraction over computation. They allow us to separate what a piece of code is supposed to do from how it does it.
This separation is crucial for two reasons. First, it allows us to reason about programs at a higher level without being overwhelmed by low-level details. Second, it enables collaboration and change. In real software systems, different programmers often work on different parts of a program. Some implement functionality, while others use it. Functions provide a clear boundary that allows these roles to be separated cleanly.
In this unit, we will study functions as a design tool. You will learn how functions reduce complexity, enable code reuse, and support information hiding. You will see that a function is not just a way to reuse code, but it is a promise that enables independent reasoning, change, and collaboration. More importantly, you will begin to think explicitly in terms of two roles: the implementer, who defines how a function works, and the client, who relies only on what the function promises to do. This way of thinking will form the foundation for later abstractions in the course.
Function as an Abstraction over Computation
An important abstraction provided by a programming language is the function (also known as method or procedure). This abstraction allows programmers to group a set of instructions and give the group a name. The named set of instructions may take one or more variables as input parameters, and return zero or one value.
Like all other abstractions, defining functions allows us to think at a higher conceptual level. By composing functions at increasingly higher levels of abstraction, we can build programs with increasing levels of complexity.
Every function can be understood in terms of a specification: a description of what the function is supposed to do, expressed in terms of its parameters and return value. In Java, a function is a typed abstraction. This means that both the input parameters and the return value of a function have types associated with them. This typing defines how the rest of the program may interact with the function.
Abstraction also makes programs easier to test and reason about. Because a function interacts with the rest of the program only through its parameters and return value, it can be tested independently of the rest of the system.
Defining a Function in Java
Let's look at how we can define a function (or method in Java terminology) in Java. Note that the Java examples below are not complete programs and are merely snippets. As such, they cannot be compiled. But, we can type them into JShell to interpret and execute them.
The basic syntax of a function is as follows:
| Syntax of a Java Function | |
|---|---|
1 2 3 | |
Note that the return type and parameter types must be explicitly stated in Java, as they enforce the contract between the caller and the function. The caller knows what kind of inputs are allowed and what kind of results can be expected. The function (or programmer of the function) knows what kind of assumptions can be made about the inputs and what kind of outputs must be produced.
For example, the following function is named factorial. It takes in a parameter n of type int and returns a result of type int.
| Simple Java Function | |
|---|---|
1 2 3 4 5 6 | |
In the case where a function does not return anything, we still need to specify the return type. In this case, we use a type called void1.
Note that, unlike Python, Java does not allow returning more than one value.
Note that the type int alone does not capture all assumptions about valid inputs. For example, factorial is typically defined only for non-negative integers. Such assumptions form part of the function's specification and must be respected by the client.
Reducing Code Complexity With Function
Functions help us deal with complexity in a few ways.
-
Functions allow programmers to compartmentalize computation and its effects. We can isolate the complexity within its body: the intermediate variables exist only as local variables that have no effect outside of the function. A function only interacts with the rest of the code through its parameters and return value, and so, reduces the dependencies between variables to these well-defined interactions. Such compartmentalization reduces the complexity of code.
-
Functions allow programmers to hide how a task is performed. The caller of the function only needs to worry about what the function does. By hiding the details of how, we gain two powerful tools against code complexity. First, we reduce the amount of information that we need to communicate among programmers. A fellow programmer only needs to read the documentation to understand what the parameters are for, and what the return values are. There is no need for a fellow programmer to know about the intermediate variables or the internal computation used to implement the functions. Second, as the design and requirements evolve, the implementation of a function may change. But, as long as the parameters and the return value of a function remain the same, the caller of the function does not have to update the code accordingly. Reducing the need to change as the software evolves reduces the chances of introducing bugs accordingly.
-
Functions allow us to reduce repetition in our code through code reuse. If we have the same computation that we need to perform repeatedly on different values, we can construct these computations as functions by replacing the values with parameters and passing in the values as arguments to the function. This approach reduces the amount of boiler-plate code and has two major benefits in reducing code complexity and bugs. First, it makes the code more succinct, and therefore easier to read and understand. Second, it reduces the number of places in our code that we need to modify as the software evolves, and therefore, decreases the chance of introducing new bugs.
Consider the example function below, which approximates \(e^n\) using a Taylor series. You can ignore how the math works; focus on what the caller sees.”
| A Function to Estimate e^n | |
|---|---|
1 2 3 4 5 6 7 8 9 | |
The computation above involves three intermediate variables, i, x and res. These variables are local to the function and are not exposed to the caller of the function. Thus, the caller has three less variables to keep track of and worry about. The caller of the function does not know how the estimation was done -- Does it use a for loop or a while loop? Does it calculate it recursively? How many terms in the Taylor's series are used, etc. The caller only needs to know that the function exp takes an integer n as input and returns a double as output. Finally, every time we need to estimate \(e^n\), we can call the function exp with the value of n as an argument, without repeatedly writing the same loop. You can also see that exp reuses the function factorial that we defined earlier.
Abstraction Barrier
We can imagine an abstraction barrier between the code that calls a function and the code that defines the function body. Above the barrier, the concern is about what task a function performs, while below the barrier, the concern is about how the function performs the task.
While many of you are used to writing a program solo, in practice, you rarely write a program with contributions from only a single person. The abstraction barrier separates the role of the programmer into two: (i) an implementer, who provides the implementation of the function, and (ii) a client, who uses the function to perform the task. Part of the aim of CS2030/S is to switch your mindset into thinking in terms of these two roles. In fact, in CS2030/S, you will be both but may be restricted to just being either a client or an implementer on specific functionality.
The abstraction barrier thus enforces a separation of concerns between the two roles. When using a function, a programmer must act as a client. The client does not have to care how the implementer implements the functionality. This gives the implementer the freedom and flexibility to change how the function is implemented, without affecting the client, as long as the behavior of the function remains unchanged. When defining a function, a programmer acts as an implementer. The implementer is free to change the implementation at any time, as long as the specified behavior is preserved. The implementer does not have to care how the client is using the function.
In Java, the abstraction barrier of a function is enforced primarily through its parameter types and return type, which restrict how clients may use the function and how implementers may define it.
The same ideas: abstraction barriers, specifications, and separation of roles, will reappear throughout the rest of the course in increasingly powerful forms. We will see how it is used for a higher level of abstraction, classes, in the next unit.
-
voidin Java is like a true nothingness (unlike Python'sNoneor JavaScript'sundefined). If a function is declared as returning a typevoid, it cannot even be used in an assignment! ↩