Home CPSC 401

Functions

Overview

Functions are the fundamental way programming languages split code into multiple units. Some languages use the term "function" and others use other terms:

Some of these terms have slightly specialized meanings, but we will use "function" to mean any of these.

Functions:

Function terminology:


Parameters

Parameters allow callers to pass information to a function. There are two terms relating to parameters:


int add(int a, int b) {
  return a + b;
}

int main() {
  int x = add(3, 7);
}

In the code above, a and b are formal parameters and 3 and 7 are actual parameters. There are two ways to match actual parameters to formal ones:

Python allows both methods:


def function(name, age, height):
  print("Name is", name)
  print("Age is", age)
  print("Height is", height)

# by position
function("Bob", 42, 6.0)

# by keyword
function(height = 5.5, age = 31, name = "John")

Most languages allow for default values for parameters that are not specified.

Parameters can be passed in different ways:


Macros

C and C++ allow for macros. Unfortunately, macros in these languages are difficult to write correctly.

Below is a simple macro for multiplying two numbers:


#define TIMES(a, b) a * b

A problem with this macro is that it does not enforce precedence, so the following call will not produce what we want:


  printf("5 * 7 = %d\n", TIMES(3 + 2, 3 + 4));

We can fix it by enforcing precedence with parenthesis:


#define TIMES(a, b) (a) * (b)

Even with this trick, macros can lead into problems. What is happening with this code:


#define MAX(a, b) ((a) > (b)) ? (a) : (b)

int main() {
  /* what will go wrong now? */
  int x = 5;

  int largest = MAX(x++, 2);

  printf("x = %d\n", x);

  return 0;
}

Macros can be (ab)used for some very interesting code, as in this example:


#include <iostream>
using namespace std;

#define MAKEVAR(type, name) \
    private: \
        type name; \
    public: \
        type get_##name() {return name;} \
        void set_##name(type o) {name = o;}

class Circle {
    // make some variables automatically!
    MAKEVAR(int, x)
    MAKEVAR(int, y)
    MAKEVAR(double, radius)
};

int main() {
    Circle c;
    c.set_x(10);
    c.set_y(5);
    c.set_radius(7.5);
    
    cout << c.get_x() << ", " << c.get_y() << " " << c.get_radius() << endl;

    return 0;
}

Overloading

Function overloading is when multiple functions have the same name. This is allowable in C++, Java and C#.

Function overloading requires that the parameters be different in number and/or type.

Operator overloading allows programmers to specify how operators work with different types. Many languages allow operator overloading. Most only allow operators to be overloaded for new types.

Ruby is one of very few languages that allow built-in operators to be changed.


class Fixnum
  def +(y)
    # change how addition works!
    return (self * y) - 1
  end
end

# this will print 11 instead of 7
puts (3 + 4)

Function Values

It is sometimes handy to treat functions as values inside of a program. This means that functions can be stored in variables, put in data structures or passed to or from functions.

One example is writing a sort function that takes a comparison as a parameter. This can be done easily in Python which has functions as values:


# bubble sort
def sort(data, compare):
  sorted = False
  while not sorted:
    sorted = True
    for index in range((len(data) - 1)):
      if compare(data[index], data[index + 1]):
        sorted = False
        temp = data[index]
        data[index] = data[index + 1]
        data[index + 1] = temp

# one comparison function
def less(a, b):
  return a < b

# call sort and pass the less function
sort(nums, less)

Another common use of passing functions is callbacks which are used in GUI development.

Functions can also be put in variables, lists and so on. These techniques are used in functional programming extensively.


Function Pointers

C and C++ do not allow functions to be used as values directly, but they do allow function pointers. A function pointer is defined as a normal function exept for a * before the name:


return-type (*function-name)(parameter1type, parameter2type...)

To get a function pointer, we can use the & operator. The sort example in C++ would look like this:


void sort(int array[], int size, bool (*compare)(int, int)) {
  // for each element
  for(int i = 0; i < size; i++) {
    // for each element after this
    for(int j = i; j < size; j++) {
      // if compare says out of order,
      if(compare(array[i], array[j])) {
        // swap them
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
      }
    }
  }
}

bool less(int a, int b) {
  return a < b;
}

sort(nums, size, &less);

Anonymous Functions

When using functions as values, it can become tedious to write and name functions for each one that we want. Many languages allow anonymous functions, also called lambda functions.

In Python this is done with the lambda keyword:


sort(nums, lambda a, b: a > b)

The new C++ 11 standard adds lambda functions to C++:


sort(nums, size, [](int a, int b) {return a > b;});

Generic Functions

Generic functions allow for writing functions that work for multiple types of data in statically typed languages. C++ provides templates for this purpose:


template <typename T>
T max(T a, T b) {
  return a > b a : b;
}

Templates are filled in with the appropriate types by the compiler.

Java provides generics which provide the same service:


public static <T> max(T a, T b) {
  return a > b;
}

While they look similar, templates and generics work differently. Generics only produce one function and the actual types are filled in at runtime. Generics also only allow class types.


Nested Functions

In many languages, functions can be nested inside of other functions. This is possible in Pascal, Ada, Javascript, Python, Ruby and most functional languages.

The following Python code calculates all prime numbers in a range. To do this it uses a nested function


# return all prime numbers from 1 to n
def primes(n):
  # test if a number is prime in a nested function
  def isPrime(x):
    for i in range(2, x // 2 + 1):
      if x % i == 0:
        return False
    return True

  # go through numbers
  nums = []
  for i in range(1, n + 1):
    if isPrime(i):
      nums.append(i)
  return nums

Nested functions can access the local variables of their enclosing function.


Closures

One use of nested functions is to create closures. A closure is a function that can reference its enclosing environment from another point. A simple example:


# return a counter function
def counter(start, step):
  i = start - step
  def c():
    nonlocal i
    i += step
    return i
  return c


# create counter functions
counter1 = counter(1, 1)
counter2 = counter(100, -1)

The functions returned from counter are closures because they contain elements of the call that produced them.

Below is the same closure implemented in Racket:


(define (counter start step)
  (define i (- start step))
  (lambda ()
    (begin
      (set! i (+ i step))
      i)))

(define counter1 (counter 1 1))
(define counter2 (counter 100 -1))

Copyright © 2018 Ian Finlayson | Licensed under a Creative Commons Attribution 4.0 International License.