Day 17 - Beware of global variables
TODO: these examples are too simple / repetitive. Revise.
Outcomes:
Pitfalls of Global Variables
1. Introduction (10 minutes)
A. Global Variables and Mutable State
- Definition:
- Global variables are defined at the top level and can be accessed or modified by multiple functions.
- Problems for Testing:
- They create hidden dependencies between functions.
- Tests may pass or fail based on the order in which functions are called.
- They reduce "referential transparency" (functions, given same input, may no longer produce same output), making it hard to test functions in isolation, hard to understand function behavior.
B. Learning Goals
- Understand how global mutable state can interfere with testing.
- See concrete Pyret examples showing these issues.
- Discuss best practices for isolating function behavior.
2. Demonstration: Global Variables and Testing Difficulties (25 minutes)
A. Example Scenario: A Global Counter
-
Setup:
- We define a global variable
global-counter
that is updated by different functions.
- We define a global variable
-
Pyret Code Example:
# Global variable declaration
var global-counter = 0
# Function that increments the counter by a given value
fun increment-counter(x :: Number) -> Number block:
global-counter := global-counter + x
global-counter
end
# Function that resets the counter to 0
fun reset-counter() -> Nothing:
global-counter := 0
end
B. Testing Without Isolation
-
Test Case Example:
# Test Block 1: Reset state before testing
check:
reset-counter()
increment-counter(5) is 5
increment-counter(3) is 8
end
# Test Block 2: No reset between tests
check:
# No call to reset-counter here!
increment-counter(2) is ? # What should the result be?
end -
Discussion:
- Ask: “If Test Block 1 runs first, what will the result in Block 2 be? What if Block 2 runs in isolation?”
- Key Point:
- Since
global-counter
is shared, Block 2’s outcome depends on whether the counter was reset. Tests become order‑dependent and non‑repeatable.
- Since
C. Real-World Consequences
- Side Effects and Hidden Dependencies:
- Functions that rely on global variables cause side effects that “leak” between tests.
- Example: A logging function that appends to a global log makes it hard to test without first clearing the log.
- Harder to test is also harder to debug, harder to understand!
- Interactive Exercise:
- Ask students: “Predict the value of
global-counter
after the following sequence:What is the final value?”reset-counter()
increment-counter(4) # Expected: 4
increment-counter(6) # Expected: 10
# Then later call increment-counter(2) without a reset. - Discuss the potential for intermittent test failures when state is not controlled.
- Ask students: “Predict the value of
3. Discussion and Best Practices (15 minutes)
A. Pure Functions Are Easier to Test
-
Referential Transparency:
- A pure function’s output depends only on its inputs, making it predictable and easy to test.
-
Example Contrast:
# Impure function with global state
fun increment-counter(x :: Number) -> Number block:
global-counter := global-counter + x
global-counter
end
# Pure function version
fun pure-increment(total :: Number, x :: Number) -> Number:
total + x
end -
Testing
pure-increment
is straightforward because it has no side effects.
B. Strategies to Avoid Global State
- Pass Variables as Parameters:
- Instead of using a global variable, pass the current state as a parameter.
- Localize Mutable State:
- Use local accumulators within functions (e.g., within loops) so that state does not leak.
- Isolate Side Effects:
- If side effects are necessary, confine them to a small portion of your code and reset state between tests.
C. Group Discussion
- Interactive Question:
- “How would you rewrite our counter example so that each test can run in isolation without depending on a global variable?”
- Encourage students to share ideas (e.g., using pure functions or passing the counter as a parameter).
4. Wrap-Up (10 minutes)
- Global mutable state (global variables) can interfere with testing by introducing hidden dependencies.
- Tests may behave unpredictably if global state isn’t reset between function calls.
- Pure functions—those that depend only on their inputs—are much easier to test and reason about.
- Emphasize that designing code to minimize or isolate side effects leads to more maintainable and testable software.