1.4. Repeated Evaluation: For-Loops#
A loop is a fundamental programming construct that allows you to execute a block of code repeatedly. This is essential for automating repetitive tasks.
For example, suppose we want to compute the finite sum \(s_n = \sum_{i=1}^n \frac{1}{i}\) for some integer \(n \ge 1\). A for
-loop is the perfect tool for this.
The basic syntax for a for
-loop is:
for i = 1:n
# This code will be repeated n times, for i = 1, 2, ..., n
end
Using this structure, we can write a function to compute \(s_n\):
function compute_s(n)
# Initialize a variable to accumulate the sum.
# We start with 0.0 to ensure it's a float, since the terms are fractions.
s = 0.0
# Loop from i = 1 up to n.
for i = 1:n
s += 1/i # Add the next term to the sum.
end
return s
end
compute_s (generic function with 1 method)
compute_s(100)
5.187377517639621
The for
-loop is quite flexible. You can specify a start
, step
, and stop
value for the loop variable:
for x = start:step:stop
# Code to be repeated
end
The loop will begin with x
at start
, increment x
by step
in each iteration, and stop when x
passes stop
.
# Loop over the odd numbers from 1 to 20.
for x = 1:2:20
print(x, " ")
end
1 3 5 7 9 11 13 15 17 19
# The step value can also be a floating-point number.
for x = 1:0.2:5
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
# Use a negative step to count down.
for x = 10:-1:-5
print(x, " ")
end
10 9 8 7 6 5 4 3 2 1 0 -1 -2 -3 -4 -5
# If start > stop and step is positive, the loop body never executes.
for x = 10:2:5
print(x, " ")
end
More generally, a for
-loop can iterate over any collection of items. Instead of a range, you can provide a tuple (or other collection types like arrays, which we will see later) of values to loop over.
# The loop variable `i` will take on each value from the tuple in order.
for i in (3, 5, 11, 20)
println("The current value of i is ", i)
end
The current value of i is 3
The current value of i is 5
The current value of i is 11
The current value of i is 20
1.4.1. Example: The Factorial Function#
The factorial function, \(n!\), is defined as the product of all positive integers up to \(n\). We can implement this using a for
-loop.
function my_factorial(n)
# Initialize the result to 1.
product = 1
# Multiply by each integer from 2 to n.
for i = 2:n
product *= i
end
return product
end
my_factorial (generic function with 1 method)
The factorial function grows very quickly. Its value can easily exceed the range of the default Int64
type, leading to an overflow error.
my_factorial(20) # This is still within the Int64 range.
2432902008176640000
my_factorial(30) # Overflow! The result wraps around and becomes negative.
-8764578968847253504
To handle larger numbers, we can explicitly use a larger integer type like Int128
. Julia’s type promotion system will then automatically perform all calculations using the larger type.
my_factorial(Int128(30))
265252859812191058636308480000000
While this works, resorting to higher-precision types is not always the best solution. Unless you truly need to know the exact value of a large factorial, it’s often better to rearrange your calculations to avoid producing such large intermediate numbers in the first place. This leads us to the next example.
For completeness, note that Julia has a built-in factorial()
function. Unlike our version which wraps around on overflow, the built-in function gives an OverflowError
, which is often more helpful for debugging.
# This will produce an error because 30! is too large for an Int64.
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]:2
1.4.2. Example: Taylor Polynomial for Cosine#
Consider the Taylor polynomial of degree \(2n\) for \(\cos x\): $\( \cos x \approx \sum_{k=0}^n \frac{(-1)^k}{(2k)!}x^{2k} \)$ A naive implementation might calculate the factorial and power separately in each step.
function taylor_cos_bad(x, n)
y = 0.0
for k = 0:n
# This is computationally expensive and overflows for large k.
y += (-1)^k / factorial(2k) * x^(2k)
end
return y
end
taylor_cos_bad (generic function with 1 method)
This function works well when the arguments x
and n
are small.
# For x = 0.25, a low-degree approximation is excellent.
println("Taylor (n=2): ", taylor_cos_bad(0.25, 2))
println("cos(0.25): ", cos(0.25))
Taylor (n=2): 0.9689127604166666
cos(0.25): 0.9689124217106447
However, for larger x
, a higher-degree polynomial (larger n
) is needed for a good approximation. This is where our taylor_cos_bad
function fails due to the factorial()
overflow.
# For x=10, the n=2 approximation is poor.
println("Taylor (n=2): ", taylor_cos_bad(10, 2))
println("cos(10): ", cos(10))
Taylor (n=2): 367.66666666666663
cos(10): -0.8390715290764524
# Trying to increase n to 15 causes an overflow error because 2k = 30.
taylor_cos_bad(10, 15)
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]:5
[4] top-level scope
@ In[16]:2
1.4.2.1. A Better Approach#
A much more robust method is to compute each term incrementally from the previous one. Notice the relationship between term \(k\) and term \(k-1\):
This avoids large intermediate calculations involving factorials and powers, preventing overflow and improving efficiency.
function taylor_cos(x, n)
term = 1.0 # First term (k=0) is (-1)^0 * x^0 / 0! = 1
y = 1.0 # Initialize sum with the first term
for k = 1:n
# Calculate the next term from the previous one.
term *= -x^2 / ((2k-1) * 2k)
y += term
end
return y
end
taylor_cos (generic function with 1 method)
This improved version can now handle a higher degree approximation for x=10
without any issues.
# Let's try the degree 30 (n=15) approximation again.
println("Taylor (n=15):", taylor_cos(10, 15))
println("cos(10): ", cos(10))
Taylor (n=15):-0.839420205180993
cos(10): -0.8390715290764524
# We can go even higher for better accuracy.
println("Taylor (n=50): ", taylor_cos(10, 50))
println("cos(10): ", cos(10))
Taylor (n=50): -0.8390715290766048
cos(10): -0.8390715290764524
1.4.3. Scope of Variables#
The scope of a variable is the region of code where it is visible and can be accessed. A new local scope is introduced by most code blocks, including for
-loops and functions.
By default, a local scope inherits variables from its parent scope. However, you can use the local
keyword to declare a new variable that is visible only inside that local scope, even if a variable with the same name exists in the parent scope.
x = 10
y = 10
for i = 1:5
z = i # `z` is local to this for-loop.
x = z # This modifies the `x` from the parent scope.
local y = z # This creates a new `y` that shadows the parent `y`.
end
println("x is now ", x) # Prints 5, as the loop modified the parent `x`.
println("y is still ", y) # Prints 10, as the loop used its own local `y`.
# The following line will cause an error because `z` only existed inside the loop.
# println(z)
x is now 5
y is still 10