Conditional Evaluation of Expressions
As an example for why short-circuit evaluation is useful, I gave this example in C:
if (ptr != NULL && *ptr > 10) {
...
} else {
...
}
Evaluating the expression *ptr > 10
is safe only if ptr != NULL
. Clearly,
there are scenarios where expressions of types other than Bool
are safe to
evaluate only under certain conditions. For example, we want to evaluate x / y
only if y
is not 0. Or we want to take the tail of a list only if the list is
non-empty. Let's take the division by 0 example:
safeQuotient :: Double -> Double -> Maybe Double
safeQuotient x y | y == 0 = Nothing
| otherwise = Just (x / y)
There's little to improve about this definition, but for the sake of discussion,
let's assume that we want to assign the result of x / y
to a local variable
before returning the result. Here's how we can do this:
safeQuotient :: Double -> Double -> Maybe Double
safeQuotient x y | y == 0 = Nothing
| otherwise = let q = x / y
in Just q
Clearly, we calculate q = x / y
only when it is safe to do so. However, most
Haskell programmers find where
blocks more readable and use let
expressions
only when absolutely necessary. So we would really like to write
safeQuotient :: Double -> Double -> Maybe Double
safeQuotient x y | y == 0 = Nothing
| otherwise = Just q
where
q = x / y
And it turns out we can, thanks to lazy evaluation. The variable q
is visible
in both branches of the definition because, remember, a where
block is
attached to the entire equation, not just to an individual branch. In the first
branch, when y == 0
, the result is Nothing
, so we will never ask for the
value of q
. The dangerous expression x / y
is never evaluated. In the second
branch, we return Just q
. Thus, the caller of safeQuotient
may evaluate q
at some point. But this is fine. We return Just q
only if y
is not 0, so the
division x / y
is safe.
In summary, lazy evaluation allows us to define expressions that we know aren't always safe to evaluate, as long as we only evaluate them when they are safe to evaluate. This can lead to more readable code—the above is a bad example—and may help to make our program more efficient.
How can this help efficiency? Assume we have a function someCostlyComputation
that fails when given an empty list as argument, and assume we need the result
of someCostlyComputation
twice in our code, something like this:
madeUpFunction :: [Int] -> Int
madeUpFunction xs | null xs = 0
| otherwise = y * y
where
y = someCostlyComputation xs
Without local variables, we would have to define madeUpFunction
as
madeUpFunction :: [Int] -> Int
madeUpFunction xs | null xs = 0
| otherwise = someCostlyComputation xs * someCostlyComputation xs
but this would evaluate the expression someCostlyComputation xs
twice and thus
would be more expensive. So we really need a local variable here. We could have
introduced it in the otherwise
branch using a let
expression, as we did with
safeQuotient
before, but we don't have to, thanks to lazy evaluation.
A note: This type of scenario does come up in real programs, but I found it hard
to come up with a small standalone example. Hence madeUpFunction
and
someCostlyComputation
. I hope the idea is clear and your imagination sees how
this might apply to real-world situations.