Chapter 9: Advanced Features of Functions

Return to all notes

This chapter covers a bit more on Functions…

We know how to define an argument with a type, but what if more information is needed for an argument. Recall our factorial function

function fact(n::Integer)
  n==0 ? 1 : n*fact(n-1)
end

but what happens if you put in a negative number? Try it.

We can prevent it with the following:

function fact(n::Integer)
  n>=0 || throw(ArgumentError("The argument must be a nonnegative integer."))
  n==0 ? 1 : n*fact(n-1)
end

The first line of this evaluates n>=0 if that is false, the second after the || is evaluated and an error is thrown.

Optional arguments

Let’s return to Newton’s method, which we wrote before as

function newton(f::Function, x0::Number)
  local x1 = x0
  local xstep = f(x1)/ForwardDiff.derivative(f,x1)
  local steps = 0
  while abs(xstep)>1e-6 && steps<10
    x1 = x1- xstep
    xstep = f(x1)/ForwardDiff.derivative(f,x1)
    steps += 1
  end
  x1
end

Notice that we hard-coded the stopping criteria and the max number of steps. Let’s adapt this function to include the following:

  1. Make the stopping condition and max steps as optional parameters.
  2. Check to make sure that they are valid.
  3. If the number of steps exceeds the maximum number of steps, then throw an ErrorException and a reasonable message.

First, we can define an argument to be option by assigning a default value. Let’s define the tolerance (tol) and the max number of step (max_steps) in the following way:

function newton(f::Function, x0::Number,tol=1e-6,max_steps=10)
  local x1 = x0
  local xstep = f(x1)/ForwardDiff.derivative(f,x1)
  local steps = 0
  while abs(xstep)>tol && steps<max_steps
    x1 = x1- xstep
    xstep = f(x1)/ForwardDiff.derivative(f,x1)
    steps += 1
  end
  x1
end

This seems more robust in that we can now call Newton’s method with different values of tolerance and steps. So:

newton(x->x^2-5,2)

returns 2.236067977499809. But if we use a lower tolerance:

newton(x->x^2-5,2,1e-3)

returns 2.2360568742361395.

There may be a problem though. If

\[f(x)=- 0.00002776075556\,{x}^{4}- 0.0005550391111\,{x}^{3}+ 0.03053947111\, {x}^{2}+ 0.3328122667\,x- 10.0\]

and we run:

newton(f,25)

we get the response 20.807345491764316, however note that

f(ans)=-0.05670058064912986

which doesn’t appear to be a root. What happened. If we temporarily print out the values of x1 within the loop, we’ll see that the x values bounce all around and then just stops. The max number of steps is reached.

In this case, we should alert the user that we’ve reached the max number of steps. Before the last line of the function, let’s include

steps < max_steps || throw(ErrorException("The maximum number of steps: $max_steps was reached without convergence"))

and then rerunnning newton(f,25) gives a error.

Keyword Arguments

If we want to change the number of steps used, but not the tolerance, we would need to put in

Variable Number of arguments

Recall that the geometric mean of a set of numbers \(x_1,x_2, \ldots, x_n\) is given by

$$ \sqrt[n]{x_1x_2 \cdots x_n}$$

It would be great to write a geom_mean function which computed the geometric mean for any number of elements.
It would be unfortunate if we have to write different functions for different number of arguments. We can write a variable number of arguments with a … trailing the last argument. The following is a generalized version of the geometric mean:

function geom_mean(x::Number...)
    prod(x)^(length(n))
end

and this allows the following:

geom_mean(1,2)

as well as

geom_mean(1,2,3,4)

See The Julia documentation on Variable Arguments for more information on variable arguments.

Multiple Dispatch

Generally each function that you write (or is built-in to Julia) has a signature which is the number and types of arguments. For example, the function newton above has three arguments. The first two are functions, and the second is a number.

Julia allows functions of the same name with different signatures. The execution of this is called multiple dispatch. The classic example is the + function. If we only consider those with 2 arguments, then + can take 2 integers an integer and a float, two complex, a float and a complex, etc. Each one of those functions have the same name with different signatures.

To view all of the methods, use the methods command. For example, typing methods(+) lists 163 methods and the signature of each.

Example of Multiple dispatch

Let’s consider a function called the_max which return the maximum number of a whole bunch of possible arguments. Let’s start going through them:

It is also nice to return the maximum value of a range (technically an AbstractRange type) which includes a UnitRange, StepRange and LinRange. We can put all of these together with the function

function the_max(r::AbstractRange)

end

and note that r is an abstract range, then first(r) returns the first element of the range, last(r) returns the last element and step(r) returns the step size of the range. Write this function and test with

  1. the_max(1:10)
  2. the_max(10:-1:1)
  3. the_max(3:0.5:9)
  4. the_max(LinRange(1,5,9)) which makes a range from 1 to 5 using 9 steps.

once you have created these, and type methods(the_max) julia should return:

4 methods for generic function the_max:

and list the 4 methods with their types.

Parametric Methods

As we are thinking about writing other the_max functions with a different argument signature, a natural one would be an array of Reals (mainly because we will need to do something different for Complex arrays).

If we write:

function the_max(arr::Array{Real,1})
  local max = -Inf
  for num in arr
    if num > max
      max = num
    end
  end
  max
end

which is similar to that above for any number of numbers and then let’s test it with the array x=collect(1:10), we’ll get the error:

MethodError: no method matching the_max(::Array{Int64,1})
Closest candidates are:
  the_max(!Matched::Array{Real,1}) at In[XXX]:2

and this is because even though x is of type Array{Int64,1} that doesn’t fit the signature Array{AbstractFloat,1}.

One way to get around this is to write a function for each number type we may want to find the max of (Int128, Int64, …, BigInt,Float64,Float32,Float16), but if you notice every function will be identical and this would be painful and a lot of code copying.

Instead, we will use something in julia called parametric functions which allows us to write a large number of nearly identical functions with the same code. To do this with the the_max function, we will write:

function the_max(arr::Array{T,1}) where T <: Real
  local max = -Inf
  for num in arr
    if num > max
      max = num
    end
  end
  max
end

and the expression where T <: Real is used as any type T that is a subset of the Real numbers (including integers, floats and rational)

and then the_max(x) returns 10. Also if we have

the_max(collect(1.0:10.0))

returns 10.0 and

the_max(collect(big(1):big(10)))

returns 10 (as a BigInt).