In programming, a function (also called subroutines) is a block of code that can be called multiple times, takes in a variety of arguments, and (often) returns some value. Functions that are within objects are called methods.

Core idea

In many languages, we define the data type of the return value in the function (in some languages, this is inferred by the compiler or interpreter). This is a pretty good way of remembering things.

  • int, bool, etc. functions return their given type, but…
  • void functions don’t return anything. If you don’t want it to return anything then leave it as void.

The code that invokes the function is the caller, and the function itself is the callee.

In many languages, the function must be declared or defined before it is used. Occasionally we use things called function prototypes (also called declarations; see below). A function definition is the code for the full function. If the function is only defined after it is used, then we get a compilation error.

int add (int a, int b); // the prototype
int add (int, int); // equally as valid
 
int main (void) {
	int i = foo(3, 6);
}
 
int add (int a, int b) { // the actual function
	return a + b;
}

Note that functions that accept no arguments should explicitly declare that they take in void. This results in a clear interface and ensures behaviour is as expected.1

Inputs (pass-by-x)

We usually send in inputs by value (pass by value), i.e., for any value we pass in, there’s a copy made on the stack for the variables within the function. What this means is that any manipulations we make won’t be reflected in the input location (a classic example is with a swapping function).

We can also pass by pointer. If we pass in struct, we typically pass a pointer, mainly because it allows us to actually make changes and also because sometimes they can get fairly large and it will be expensive to copy that onto the stack.

In C++, we can pass by reference using an ampersand in a declaration. See the below example:

void bumpMark (int& mark) {
	if (mark < 90)
		mark += 10;
}
 
int main () {
	int mark = 64;
	bumpMark(mark); // no need for extra ampersand, etc. here
}

Sub-pages

  • Some function inputs are mutable or immutable (unchangeable). This describes whether we can manipulate the content of the input after it has been passed to a function.
  • Variadic functions take in any arbitrary amount of arguments. It’s fairly simple: we just use an ellipsis as an argument and it can take as many as it needs.
  • Function overloading occurs when we have different functions with the same name, but with different signatures (i.e., what we’re inputting in).
  • Inline functions combine a the caller and the callee into a single translation unit (i.e., a single source file).

Language-specific implementations

JavaScript

JavaScript is weird. So many different ways to implement functions:

function fn (base, exp) {
	return base * exp;
} // best one to use
 
const fn = function(base, exp) {
	return () => base * exp;
};
 
const fn = (base, exp) => {
	// whatever
};
 
let h = a => a % 3; // jesus christ

All functionally2 do the same thing. The one in the middle is probably the clearest, because of how similar it looks to other languages.

Python

In Python3, functions can be defined as follows, with input and output types:

def findDuplicate(self, nums: List[int]) -> int:

We can also set default parameters (i.e., learning_rate=0.01). Any values that aren’t set by the caller will take these default values.

In assembly

In Assembly languages, we need to be able to do these key properties: calling from anywhere, passing values, returning a value (and the subroutine to where it came from), and local variables destroyed when finished.

What if we have multiple function calls? If we just used branches the function wouldn’t know where to go back to. The idea is that we need to save the return address in a register3 and use that to return to after the function call. In Nios II, r31/ra serves as this return register. If we have multiple ret calls, we’ll end up overwriting the return register each time. Since we can’t arbitrarily add more return registers, we use the stack in memory to store return locations.

Generally function calls consist of distinct blocks of code: the pre- and post-call (in the caller) and the prologue and epilogue (in the callee). In the pre-call, the caller must write appropriate values into the registers for the first four parameters. Any other parameters need to be pushed onto the stack (p5 and higher). In the post-call, we must de-allocated space on the stack for passing arguments.

In the prologue, the callee should be allocating space for local variables and preserve register values that shouldn’t change (return address, local variables that stay after another call). In the epilogue, the callee will be reversing anything done in the prologue.

See also

Footnotes

  1. From this page at Carnegie Mellon’s Software Engineering Institute.

  2. haha

  3. ”Leave some breadcrumbs.” - Prof Moshovos