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(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.