Evaluation Strategies
Understanding Evaluation Strategies
Before diving into CBV and CBN, let’s step back and understand why evaluation strategies matter.
When a function is called in a programming language, how and when its arguments are evaluated can affect:
- Performance (do we compute things too early or too often?).
- Correctness (do we compute things in the right order?).
- Termination (will the program ever stop running?).
Expressions specified by methods are calculated in the same way as operators:
- Evaluate all function arguments in a specific order. i.e., from left to right.
- Replace the function name with its definition.
- Substitute the argument values for the function’s parameters.
- Compute the result step by step.
This is known as function application, and it follows a structured process. To make this more concrete, let’s break down an example.
Example
Consider the function:
def sumOfSquares(x: Double, y: Double): Double = square(x) + square(y)
And let’s evaluate the expression:
sumOfSquares(3, 2 + 2)
At first glance, this might seem trivial, but the order in which we evaluate things makes a huge difference. Let’s go step by step.
Step 1: Expanding the Function Definition
We replace the function call with its definition:
sumOfSquares(3, 2 + 2)
→ square(3) + square(2 + 2)
At this stage, we have a choice:
- Do we evaluate
2 + 2first? - Or do we keep
2 + 2as is and evaluatesquarefirst?
This choice defines how the evaluation strategy works.
Step 2: Expanding the First Function Call
Let’s focus on square(3):
def square(x: Double): Double = x * x
So,
square(3) + square(2 + 2)
→ (3 * 3) + square(2 + 2)
→ 9 + square(2 + 2)
At this stage, we still haven’t evaluated 2 + 2 yet. Again, the question arises:
- Should we compute
2 + 2 = 4now? - Or should we delay it until we actually need the value?
Step 3: Expanding the Second Function Call
Now, let’s expand square(2 + 2):
9 + square(2 + 2)
→ 9 + ((2 + 2) * (2 + 2))
If we now compute 2 + 2:
9 + (4 * 4)
And finally:
9 + 16
→ 25
At this point, we’ve completed our evaluation — but notice something interesting:
We delayed evaluating 2 + 2 until the very last moment when it was absolutely necessary.
Changing the evaluation strategy
So we still have the same function:
def sumOfSquares(x: Double, y: Double): Double = square(x) + square(y)
// calculate the sum of squares
sumOfSquares(3, 2 + 2)
But now, let’s evaluate it with a different strategy:
Reducing arguments first
We had a choice in the previous example:
- Do we evaluate
2 + 2first? - Or do we keep
2 + 2as is and evaluatesquarefirst?
This time, let’s evaluate 2 + 2 first!
sumOfSquares(3, 2 + 2)
→ sumOfSquares(3, 4)
Expanding the function
Now, we expand the function:
sumOfSquares(3, 4)
→ square(3) + square(4)
Performing the calculations
Finally, we calculate the squares:
square(3) + square(4)
→ 9 + 16
→ 25
By evaluating 2 + 2 first, we were able to reduce the expression to a simpler form. So why we can’t always do this?
Why Does This Matter?
This example may look straightforward, but what if:
- The argument involves complex calculations instead of
2 + 2? - The argument is an infinite loop? (e.g.,
while(true) {}). - The argument is never actually used in the function?
Depending on how we choose to evaluate arguments, these scenarios could either be efficient and correct or cause serious performance issues — or even non-termination.
Evaluation strategies determine when computations happen and whether they happen at all. This is the foundation of function application, and it’s the key to understanding different ways languages handle execution.
From here, we can now explore the two main strategies:
Call by Name (delay computation until needed) and Call by Value (compute first, use later). And this stuff will be crucial as we dive deeper into functional programming concepts. It can explain what happens when an argument is an infinite computation?