---
jupyter:
  jupytext:
    text_representation:
      extension: .Rmd
      format_name: rmarkdown
      format_version: '1.1'
      jupytext_version: 0.8.7
  kernelspec:
    display_name: Python 3
    language: python
    name: python3
---

# Functions

We have already used functions.

For example, we have used the `round` function:

```{python}
a = 3.1415
```

```{python}
# Call the "round" function
b = round(a, 2)
```

```{python}
b
```

We often need to define our own functions. Before we do, we need to go into
more detail about what functions are for, and what they are.


## Functions are like named recipes

A function is a named recipe.  It is a name we give to a set of steps to
follow, a piece of code to run.

Thanks to [the Berkeley
team](https://www.inferentialthinking.com/chapters/08/Functions_and_Tables)
for this metaphor.

A recipe is the *procedure* to go from *ingredients* to a *meal*.

A function is the *procedure* to go from the *arguments* to the
*return value*.

For example, I might have a recipe with the procedure to go from the
ingredients: two eggs; butter; and cheese \- to the meal \- a cheese
omelette.

The function `round` has the procedure to go from the arguments - two
numbers, to the return value, which is the value of the first argument
rounded to the number of digits specified in the second.

I could call my recipe "two egg cheese omelette", or "recipe number
4".  Whatever I called it, it would be the same recipe.  I might
prefer a name that describes what the recipe makes, to help me
remember.

Likewise, the name `round` refers to a procedure above.  I could give it
another name, like `my_function`, but `round` is a good name, because it helps
me remember what the procedure does.

I say `round` has a procedure, but we can't see what that procedure is, it's
buried inside the internal workings of Python.

Now we are going to write our own function, where we can see the
procedure.


## Revision on variables

Here is an *assignment statement*:

```{python}
a = 2
```

As we know, we can read this as "The variable 'a' gets the value 2".

We also know that we have, on the left, a variable name, 'a', and on
the right, an expression, that gives a value.

In this case, the expression on the right is `2`. Python evaluates
this expression, to make its own internal computer representation of the
integer 2.  Call this: Computer Representation (CR) of int 2.

After Python executes this statement, the name "a" points to the CR of int 2.

To continue the revision:

```{pythno}
b = a * 4
```

The right side `a * 4` is an expression.  Python evaluates the
expression.  First it gets the value of `a`.  This is the CR of int 2.
Next it gets the value of `4`.  This is the CR of int 4. Then it
multiplies these results to get an CR of int 8.

"b" now points to the CR of int 8.

Finally:

```{pythn}
a = 3
```

"a" no longer points the CR of int 2, it points to the CR of int 3.

What value does "b" have now?

The same value as it had before.  It pointed to the CR of int
8 before.  Changing `a` has no effect on `b`.

## Defining a function

We define our function called `double`.  It accepts one argument
(ingredient), call that `x`.  It's procedure is to multiply the
argument by 2.  The return value is the argument multiplied by 2.

Here it is:

```{python}
def double(x):
    d = x * 2
    return d
```

Let's look at the first line:

```
def double(x):
```

The first word `def` tells Python we are defining a function.

The next word `double` is the name we will give to our function.

Between the parentheses, we have the function signature.  This specifies how many arguments the function has.  In our case, there is only one argument, named `x`.

Finally there is a colon `:` signifying the end of the signature.

As in [for loops](iteration), the colon signifies that the next
bit of code must be [indented](indentation).

Here is the indented part:

```
    d = x * 2
    return d
```

This is the *body* of the function.   It gives the function procedure; it defines what the function will do to its arguments, and what result it should return.

For example, here we *call* the function we just created:

```{python}
double(4)
```

Notice that `double(4)` is a [call expression](Calls).

So, what just happened?

1.  Python finds what `double` points to.  It points to internal
    representation of our function (procedure).
1.  Next it sees the parenthesis `(` and sees that we want to *call*
    our function.
1.  Now Python knows we want to call the function, it knows that
    there are one or more *expressions* inside the parentheses.  In
    our case there is one, `4`.  As usual, it evaluates this
    expression to the CR of int 4.
1.  Now Python does the *call*.  To do this it:

    1. Puts itself into *function world* (more on this later).
    2. Sets the new variable `x` to have the value CR of int 4, from
       above.
    3. Executes the code in the function body (procedure).
    1. The first line `d = x * 2` is an assignment statement.  `x`
       evaluates to CR of int 4, 2 evaluates to CR of int 2, so `d`
       has the value CR of int 8.  This is how the statement would
       work anywhere in Python, function body or not.
    1. The next line starts with `return`.  This is a *return
       statement*. When Python sees a `return` statement, it evaluates
       the expression to the right, to get the *return value*, then
    5. Pulls itself out of *function world*.
    6. Gives the *return value* as the final result of the call
       expression.  This is CR of int 8.

We can run the function with any values for the argument.

```{python}
double(2)
```

This time round, everything happened in the same way as before, except
Python found the *argument* inside the parentheses evaluated to CR of
int 2.  Thus, in *function world*, `x` gets the value CR of 2, and the *return value* becomes CR of int 4.

## Function world

I cryptically used the term *function world* for the state that Python goes into when it calls a function.

This state has two important features.

### Variables defined in functions have local scope

The first feature of function world is that all variables defined
inside function world, get thrown away when we leave function world.

We can see this if we run the following code in a notebook cell.  This
code runs in our usual top-level world, and so, not inside a function.

```{python tags=c("raises-exception")}
d
```

Notice that, in the function, we set `d` to point to the result of `x * 2`.  We called the function a couple of times, so we executed this statement a couple of times.  But the `d` in the function, gets thrown away, when we come back from function world.

In technical terms, this is called *scope*.   The *scope* of
a variable, is the pieces of code in which the variable is visible.
`d` can only be seen inside the function.  Its scope is the function.  We can also say that its scope is piece of code where it is defined, that is, it has *local* scope.

The same is true for `x`, the argument variable:

```{python tags=c("raises-exception")}
x
```

### The function has limited access to variables outside the function

We have not seen this yet, but function world has limited access to variables defined at the top level.

We won't go into much detail here, but the summary is that functions can see the values of variables defined at the top level, but they can't change what top level variables point to.  For example, say you have a variable `a` at the top level.  A function can see and use the value of `a`, but it cannot change top-level `a` to point to a different value.   We will come back to this later.

## Python checks the function signature

The signature for `double` is `(a)`.  That tells Python to expect one
and only one argument.  If we try to call it with no arguments
(nothing inside the parentheses), we get an error:

```{python tags=c("raises-exception")}
double()
```

If we try and call it with more than one argument, we get an error.  We separate arguments with commas.

```{python tags=c("raises-exception")}
double(2, 3)
```

```{python tags=c("raises-exception")}
double(2, 3, 4)
```

## Function arguments are expressions

Remember that Python knows that the arguments to a function are
expressions, and evaluates them, before running the function.

For example:

```{python}
double(2 + 3)
```

All the procedure is the same as above.  Python evaluates the
expression `2 + 3`, to get CR of int 5, then goes into function world,
sets `x` to have the value CR of int 5, and continues from there.

## Functions can have many arguments

Now we define a new function:


```{python}
def multiply(a, b):
    return a * b
```

The new thing here is that the function signature `(a, b)` has two arguments, separated by commas.  We need to give the function two values, when we call it:

```{python}
multiply(2, 3)
```

If we do not give it exactly two arguments, we get an error.

```{python tags=c("raises-exception")}
multiply(2)
```

```{python tags=c("raises-exception")}
multiply(2, 3, 4)
```

## Functions can have no arguments

Perhaps the recipe analogy breaks down here, but sometimes functions take no arguments.  For example:

```{python}
import numpy as np
# Make random number generator.
rng = np.random.default_rng()
```

```{python}
# Notice - nothing between the parentheses

def biased_coin():
    # A single random number
    r = rng.uniform()
    # A biased coin
    result = r < 0.45
    return result
```

When we call the function, we have no arguments, so no expressions between the parentheses.

```{python}
biased_coin()
```

As you would expect by now, if we try and send an argument, Python will complain:

```{python tags=c("raises-exception")}
biased_coin(0.45)
```

## Without a return statement, functions return None

Our functions so far all have a `return` statement.  This is not true of every function.

If your function does not have a return statement, the function returns the value [None](none.Rmd).

```{python}
def silent_addition(first, second):
    result = first + second
```

Notice that the body of this function has no `return` statement.  When we call it, it returns `None`:

```{python}
result = silent_addition(10, 12)
result
```

```{python}
result is None
```

## End of the introduction

That's it for the introduction.  For a less basic description, have a look at
the [Berkeley introduction to
functions](https://inferentialthinking.com/chapters/08/Functions_and_Tables.html).
