Question

I'm reading the famous paper Why Functional Programming Matters, and found something I can't understand:

  1. the bottom of page 8:

    The best analogy with conventional programming is with extensible languages — in effect, the programming language can be extended with new control structures whenever desired.

    I'm curious what languages are "extensible language"?

  2. middle of page 9, about lazy evaluation and termination

    g (f input)

    ... As an added bonus, if g terminates without reading all of f ’s output, then f is aborted. Program f can even be a nonterminating program, producing an infinite amount of output, since it will be terminated forcibly as soon as g is finished. This allows termination conditions to be separated from loop bodies — a powerful modularization

    I can't understand why "f will be terminated forcibly as soon as g is finished".

    If function f is defined as:

    function f(n) = 1 + f(n)

    Then f(1) will never returns until it gets stack overflowed.

    In this case, how can g terminate it? If I totally misunderstand these sentences, sorry that I don't have many functional language experience.

Was it helpful?

Solution

  1. Extensible Languages

Probably the most common examples of an extensible language are the Lisp family (Common Lisp, Scheme, Clojure) which have a syntactic macro system. This macro system allows you think of Lisp programs as ones that run at two points in time—once "during compilation" and again "at runtime". The "during compilation" phase takes certain parts of a Lisp program and expands them syntactically into more complex Lisp programs. The "at runtime" part runs like a regular language.

This phase distinction allows us to extend the control structures of a language because we can abstract over common idioms without executing anything. For instance, given that we have an if construct

(if true  then else) ==> then
(if false then else) ==> else

we can build a cond structure where each body is evaluated only if its paired with a true value

(cond (test1 body1) (test2 body2) (test3 body3))

by transforming it into a set of nested if statements at runtime

(do (if test1 body1 null)
    (if test2 body2 null)
    (if test3 body3 null))

Note that this cannot be done (normally) at runtime because in normal Lisp evaluation we'd have to evaluate each of the bodies 1-3 before we could even determine whether they should be executed, making cond pretty useless.

In Haskell we get a lot of these advantages from laziness and purity. Since the compiler won't compute things until they're needed, we can be more judicious about when things are actually computed. Frequently this is talked about as "using data structures as control structures", but we'll see more of that in what follows.

  1. Laziness

The way to think of laziness is that whenever you have a computation, no matter how silly

f = 3 -- pending!

it "pends" until it is needed.

print f  -- now we compute `f` and print it

This becomes especially important when you have complex data structures where parts can remain as pending even while others are computed. The linked list forms some great examples

-- [1,2,3,...] infinite list
nums = let go n = n : go (n+1)
       in go 1

Again, remember that this computation, in a lazy setting, will not be run until it is actually needed. If we were to print it, we'd see the numbers stream out one by one. In a strict setting the interpreter would be forced to compute the entire list before printing a single one of them... and thus it would hang without computing a single number.

As another example, consider the function take which, given a number n, trims off the first n elements of a list. On finite lists its behavior is obvious.

> take 3 [1,2,3,4,5,6]
[1,2,3]

> take 5 [1,2,3]
[1,2,3]

But what's less obvious is that in a lazy setting it works perfectly on infinite structures as well

> take 5 nums
[1,2,3,4,5]

In this way, we've specified the nums computation more generally than people often must when using enumeration notation like [1..10]. In fact, we've simply asked for "all" of the numbers instead of conflating the definition of nums with some information about how to select which numbers we desire.

We leave that to the consumer. After all, the consumer is the one which knows how many numbers it needs. Again consider a statement like (take n) nums where take n, the consumer, knows it needs n numbers and thus nums, the producer, need not worry about it.

There's a great deal more to this concept, but to directly answer your final question about f

f n = 1 + f n

it's important to talk about what it means for a consumer to "demand" something in the same way that take "demands" a list of a certain size. Your function f indeed will never terminate and could never be "forcibly terminated" by a consumer except in one situation: the consumer could just completely ignore it.

consumer fun = 3

> consumer f 
3

We can talk about this in terms of what kinds of things f produces which can be "partially demanded". In particular, all we can expect from f is the final result and thus we must wait on it to complete in order to "demand" anything from it at all. We need to be creative and think of a way to produce partial answers in order to be meaningfully stopped by a consumer.

One way is to encode numbers using Peano numbers. They look like this (in Haskell)

data Peano = Zero | Successor Peano

and we would write

0 ==> Zero
1 ==> Successor Zero
3 ==> Successor (Successor (Successor Zero))

We can adjust f to operate on Peano by replacing the (1+) with Successor as they mean about the same thing.

f n = Successor (f n)

And now if we write a consumer which is, say, a predicate about whether a Peano is greater than or equal to n

gte :: Peano -> Peano -> Bool
gte Zero Zero                   = True
gte Zero (Successor n)          = True
gte (Successor n) (Successor m) = gte n m

we can talk about statements like

gte Zero n ==> True             -- even without evaluating `n` at all
gte (Successor Zero) n ==> True -- while evaluating only one "layer" of `n`

and evaluate things like

> gte (Successor (Successor Zero)) (f 10)
True

where gte "forcibly halts" f after computing two layers, a sufficient demand in order for gte to proceed.

OTHER TIPS

Let's do this in Haskell.

f :: Integer -> Integer
f n = 1 + f n

Now, in order for the given statement to apply, we need a function g that terminates without needing to fully evaluate its input. In the case of integer numbers, there are only two possibilities: either you evaluate it completely, or not at all. So the only such implementation is of the form

g = const v

for some global constant v. Now, if you then try

Prelude> let v = "I'm constant!"
Prelude> let g _ = v
Prelude> g (f 1)
"I'm constant!"
Prelude> g (f 2)
"I'm constant!"
Prelude> g (f 3)
"I'm constant!"
Prelude> g (f 37)
"I'm constant!"

GHC handles this simply by not even starting to evaluate the result of f: it sees immediately the g doesn't actually need it, and spits out the result right away. That's an implementation detail though. What is however fixed in the language specification is that evaluation is non-strict, which means correctly the thing you've quoted: the evaluation must not get hung up indefinitely trying to fully obtain f's result. So, in principle, the compiler could also make it so f is first computed a bit (i.e. looped), until some timeout where it's decided to check if enough of the result is done already to be used by g. The answer would be positive (because nothing at all is needed in g), which would then prevent the calculation of f to resume.

Of course, this isn't very interesting: constant functions are trivial, at best relevant for a short-circuiting && operator or something like that. But the issue gets far more exciting when you deal with data structures that can be partially evaluated. Simplest example: Haskell lists. Consider

f' :: Integer -> [Integer]
f' n = [n ..]

That gives the (infinite) list of all numbers ≥ than n. Like f, this obviously won't ever finish with it's entire result, but in this case now that's not necessary to use the result. For instance,

g' :: [Integer] -> Integer
g' (start:l) = sum $ takeWhile (< start^2) l

Prelude> g' (f' 1)
0
Prelude> g' (f' 2)
3
Prelude> g' (f' 3)
30
Prelude> g' (f' 37)
935693

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top