# 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$$:

$\cos x \approx \sum_{k=0}^n \frac{(-1)^k}{(2k)!}x^{2k}$

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(cos(0.25))                # true value

0.9689127604166666
0.9689124217106447

# x = 10, very poor approximation of degree 4
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(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]
@ 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.