Building Programs

“Programs must be written for people to read, and only incidentally for machines to execute.” 1

So far, we explored some of the “building blocks” (the fundamental parts) of programming, this chapter introduces the “combining parts” of programming. In order to build up the solution to a problem the building blocks are combined together into larger and larger forms until, finally, a useful whole is produced.

The “combining parts” of programs are called “functions”. In many ways, these are equivalent to mathematical functions but we will not use any more mathematical analogies here. Functions can be defined, applied (or used, called, run), or composed.

Function Application

You can think of a function as a mystery box. At the top is an opening for you to put things into it. The mystery box transforms anything you put into it and out the bottom it produces the transformed object.

Function

The inputs to a function can be a variety of things (some of which we’ve seen already): a string, a number, even another function, a mouse press, a video stream, signals from a sensor, a file containing sequence data, …

Two built-in Python functions are abs() and len(). In Python, you apply a function by typing its name followed by an open parenthesis (, a comma separated list of arguments, and finally a closing parenthesis ).

abs(-56)
len("hello")

These functions take a single argument. Here is one that takes a list of several arguments.

print(True, "A string", 55)

Function Definition

We can also build (or define) our own functions. When you’re writing your own function, start with the keyword def like so:

def name_of_my_function(some, function, arguments):
    # Some operations
    
    return result

The final part of a function is return which specifies what the function evaluates to when you apply it. This function has 3 arguments called some, function, and arguments.

Let’s write our own function to compute the square of a number. This function takes a single integer argument and returns that number squared. Notice that I have included a type annotation on the argument.

def my_square(number: int):
    return number ** 2

Notice that, when you run this code, nothing seems to happen. This only defines the function. It has not yet been run or applied. Think of this as telling Python about your function like you would do with a variable. It might be helpful to imagine Python has a notebook where it writes down the names of all the definitions it knows about. When you def a new function, Python writes that name down in its notebook.

This new function can now be applied (using matching opening- and closing parenthesis) as you have already seen:

my_square()

😱 TypeError: my_square() missing 1 required positional argument: 'number'

What does that mean? It means my_square() expects 1 argument, but we gave it 0 arguments. Let’s fix that:

my_square(-5)

Because this is a TypeError we know that functions also have a type! You can even annotate the type of the return value like so:

def my_square(number: int) -> int:
    return number ** 2

The type of my_square is a function that accepts a single integer parameter and returns an integer.

Function Arguments

When you apply a function to some arguments you can also say that the arguments are “passed” to the function. You can think of this as dropping the number 7 into the mystery box labelled my_square. So what happens to arguments when they’re “passed” to a function? Where do they come from? And where do they go? Let us try to build an intuition.

# We apply the my_square() function to some argument
my_square(7) # Python then goes off to find the my_square() function
# Found it! It looks like this: my_square(number: int) -> int:
# Python then places the value 7 that we passed into a variable or box called number
# that only the body of my_square can see

You can think of function arguments as variables that are assigned on function application. In order to understand what happens, we can now substitute the name “my_square” with the function body in the definition of my_square:

number = 7 # Argument passed to the my_square function
number ** 2 # Body of the my_square function

Since we know that number is defined to be 7, we can substitute the name “number” with the definition: 7:

7 ** 2

Finally, we’re left with a mathematical expression that Python can trivially evaluate.

What we just did together is use a metal model of how Python evaluates programs which is known as the “substitution model of execution” 2, though it is an imprecise model for Python, it will get you a long way.

# This mental model also works for passing variables as arguments:
my_var = 7
my_square(my_var)
# Once again, Python places the value in the "my_var" box into the variable or box called number.

The next substitution step is to replace everywhere we see the name “my_var” with its definition: 7.

# 7 = 7  Initial variable assignment
my_square(7)

Next, replace the name “my_square” with its definition.

number = 7 # Argument passed to the my_square function
number ** 2 # Body of the my_square function

And proceed as above.

  1. Abelson, H., Sussman, G.J., Sussman, J. (1996) Structure and Interpretation of Computer Programs (2 ed.). The MIT Press. 

  2. Chiusano, P., Bjarnason, R. (2015) Referential transparency, purity, and the substitution model. In Functional Programming in Scala (pp.10-12). Manning.