1.4. Repeated evaluation: for-loops#
Suppose we want to compute the finite sum \(s_n = \sum_{i=1}^n \frac{1}{i}\) for some integer \(n\ge 1\). To do this in a computer code, we need a loop, which runs a block of code repeatedly for a given number of times.
The simplest form of a for-loop has the syntax
for i = 1:n
# This code will be repeated n times, for i = 1,2,...,n
end
Using for-loops, we can write a function to compute \(s_n\):
function compute_s(n)
sn = 0
for i = 1:n
sn += 1/i
end
sn
end
compute_s (generic function with 1 method)
and use it to, e.g., compute \(s_{100}\):
compute_s(100)
5.187377517639621
More generally, a for-loop can have the syntax
for x = start:step:stop
# Code to be repeated
end
This will repeat the code inside the for-loop, with x
beginning at the value start
, increasing by step
for each repetition, and end at or before x
reaches the value stop
.
for x = 1:2:20 # Steps of 2 - only odd values x
print(x, " ")
end
1 3 5 7 9 11 13 15 17 19
for x = 1:0.2:5 # Non-integers are ok
print(x, " ")
end
1.0 1.2 1.4 1.6 1.8 2.0 2.2 2.4 2.6 2.8 3.0 3.2 3.4 3.6 3.8 4.0 4.2 4.4 4.6 4.8 5.0
for x = 10:-1:-5 # Use a negative step to count down
print(x, " ")
end
10 9 8 7 6 5 4 3 2 1 0 -1 -2 -3 -4 -5
for x = 10:2:5 # No repetitions since start > stop and step is positive
print(x, " ")
end
1.4.1. Example: The factorial function#
function my_factorial(n)
y = 1
for i = 2:n
y *= i
end
y
end
my_factorial (generic function with 1 method)
my_factorial(5)
120
Note that the factorial function grows very fast with increasing inputs, and its value will quickly exceed the maximum value of the default Int64
type:
my_factorial(20) # This is OK
2432902008176640000
my_factorial(30) # Too large - overflow
-8764578968847253504
This particular value can still be computed by passing an Int128
to the function (which will automatically force all computations inside the function to use Int128
):
my_factorial(Int128(30))
265252859812191058636308480000000
However, this technique will eventually fail as well, and in general this is a good illustration that it is important to know what types are used internally (even if Julia is weakly typed, that is, it chooses the types for you) and what their limitations are.
Note that Julia has a built-in function for factorials, which gives an error if the return value is too large:
factorial(30)
OverflowError: 30 is too large to look up in the table; consider using `factorial(big(30))` instead
Stacktrace:
[1] factorial_lookup
@ ./combinatorics.jl:19 [inlined]
[2] factorial(n::Int64)
@ Base ./combinatorics.jl:27
[3] top-level scope
@ In[12]:1
1.4.2. Example: Taylor polynomial#
Consider the Taylor polynomial of degree \(2n\) for \(\cos x\):
A first version of a function to evaluate this could look like this:
function taylor_cos_bad(x,n)
y = 0
for k = 0:n
y += (-1)^k / factorial(2k) * x^2k
end
y
end
taylor_cos_bad (generic function with 1 method)
This function works as expected:
# x = 0.25, excellent approximation of degree 4
println(taylor_cos_bad(0.25, 2)) # Taylor approximation
println(cos(0.25)) # true value
0.9689127604166666
0.9689124217106447
# x = 10, very poor approximation of degree 4
println(taylor_cos_bad(10, 2)) # Taylor approximation
println(cos(10)) # true value
367.66666666666663
-0.8390715290764524
But note that if you try to increase \(n\), eventually you will run into the same problem as before with the factorial
function:
# x = 10, try approximation of degree 30
println(taylor_cos_bad(10, 15)) # Taylor approximation
println(cos(10)) # true value
OverflowError: 22 is too large to look up in the table; consider using `factorial(big(22))` instead
Stacktrace:
[1] factorial_lookup
@ ./combinatorics.jl:19 [inlined]
[2] factorial
@ ./combinatorics.jl:27 [inlined]
[3] taylor_cos_bad(x::Int64, n::Int64)
@ Main ./In[13]:4
[4] top-level scope
@ In[16]:2
This problem is associated with something more fundamental, namely that the Taylor polynomial for large \(x\) and \(n\) divides very large numbers to produce small numbers. A better way to compute this is to note that both the factorial and the power of \(x\) can be implemented incrementally, that is, each term can easily be obtained from the previous one. This is true for the \((-1)^k\) factor as well, it simply says that the terms have alternating signs. These observations can be implemented in this better code:
function taylor_cos(x,n)
term = 1
y = 1
for k = 1:n
term *= -x^2 / ((2k-1) * 2k)
y += term
end
y
end
taylor_cos (generic function with 1 method)
This version easily computes with degree 30:
# x = 10, try approximation of degree 30
println(taylor_cos(10, 15)) # Taylor approximation
println(cos(10)) # true value
-0.839420205180993
-0.8390715290764524
and even higher:
# x = 10, try approximation of degree 100
println(taylor_cos(10, 50)) # Taylor approximation
println(cos(10)) # true value
-0.8390715290766048
-0.8390715290764524
1.4.3. Scope of Variables#
The scope of a variable is the region of code within which a variable is visible. A new local scope is introduced by most code blocks. A local scope inherits all the variables from a parent local scope, both for reading and writing, unless the variable is specifically marked with the keyword local
. This is illustrated in the example below.
x = 10
y = 10
for i = 1:5
z = i # Local scope, only visible inside for-loop
x = z # Using x from parent scope
local y = z # Local scope, only visible inside for-loop (not using y from parent scope)
end
println(x) # = 5, since for loop modifies parent variable x
println(y) # = 10, since for loop uses local variable y
println(z) # Error: z only defined in the local scope of the for-loop
5
10
UndefVarError: `z` not defined
Stacktrace:
[1] top-level scope
@ In[20]:12
This can be convenient in for example nested functions, where the variables defined in the top function can be used in the inner function without having to pass it as a parameter.