4.3. IMPERATIVE FEATURES 41
4.2.3 Forcing evaluation
We have emphasized that, since CAML does not evaluate underneath function
abstractions, one can use such constructs to delay evaluation. We will see some
interesting examples later. Conversely, however, it can happen that one wants to
force evaluation of expressions that are hidden underneath function abstractions.
For example, recall the tail recursive factorial above:
#let rec tfact x n =
if n = 0 then x
else tfact (x * n) (n - 1);;
#let fact n = tfact 1 n;;
Since we never really want to use tfact directly, it seems a pity to bind it to a
name. Instead, we can make it local to the factorial function:
#let fact1 n =
let rec tfact x n =
if n = 0 then x
else tfact (x * n) (n - 1) in
tfact 1 n;;
This, however, has the defect that the local recursive definition is only evaluated
after fact1 receives its argument, since before that it is hidden under a function
abstraction. Moreover, it is then reevaluated each time fact is called. We can
change this as follows
#let fact2 =
let rec tfact x n =
if n = 0 then x
else tfact (x * n) (n - 1) in
tfact 1;;
Now the local binding is only evaluated once, at the point of declaration of fact2.
According to our tests, the second version of fact is about 20% faster when called
on the argument 6. The additional evaluation doesn’t amount to much in this case,
more or less just unravelling a recursive definition, yet the speedup is significant.
In instances where there is a lot of computation involved in evaluating the local
binding, the difference can be spectacular. In fact, there is a sophisticated research
field of ‘partial evaluation’ devoted to performing optimizations like this, and much
more sophisticated ones besides, automatically. In a sense, it is a generalization of
standard compiler optimizations for ordinary languages such as ‘constant folding’.
In production ML systems, however, it is normally the responsibility of the user to
force it, as it is here in CAML Light.
We might note, in passing, that if functions are implemented by plugging to-
gether combinators, with fewer explicit function abstractions, there is more chance
that as much of the expression as possible will be evaluated at declaration time. To
take a trivial example, f ◦ g will perform any evaluation of f and g that may be
possible, whereas λx. f(g x) will perform none at all until it receives its argument.
On the other side of the coin, when we actually want to delay evaluation, we really
need lambdas, so a purely combinatory version is impossible.
4.3 Imperative features
CAML has a fairly full complement of imperative features. We will not spend
much time on the imperative style of programming, and we assume readers already