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.
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:
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.
If we want to change the number of steps used, but not the tolerance, we would need to put in
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.
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.
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:
2 numbers
function the_max(x::Number,y::Number)
end
3 numbers
function the_max(x::Number,y::Number,z::Number)
end
variable number of numbers
function the_max(x::Number...)
end
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
the_max(1:10)
the_max(10:-1:1)
the_max(3:0.5:9)
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.
As we are thinking about writing other the_max
functions with a different argument signature, a natural one would be an array of Real
s (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
).