The `integral`
function
at the end of the preceding section shows
how we can use streams to model signal-processing systems that contain
feedback loops. The feedback loop for the adder shown in
figure 3.32 is modeled by the fact that `integral`'s
internal stream `int` is defined in terms of itself:

const integ = pair(initial_value, () => add_streams(scale_stream(integrand, dt), integ); );

Unfortunately, stream models of systems with loops may require uses of a delay beyond the stream programming pattern seen so far. For instance, figure 3.34 shows a signal-processing system for solving the differential equation $dy/dt=f(y)$ where $f$ is a given function. The figure shows a mapping component, which applies $f$ to its input signal, linked in a feedback loop to an integrator in a manner very similar to that of the analog computer circuits that are actually used to solve such equations.

Assuming we are given an initial value $y_0$ for $y$, we could try to model this system using the function

function solve(f, y0, dt) { const y = integral(dy, y0, dt); const dy = stream_map(f, y); return y; }

On the other hand, the intent of our definition does make sense,
because we can, in principle, begin to generate the `y` stream
without knowing `dy`.
Indeed, `integral` and many other
stream operations can generate part of the answer given only
partial information about the arguments.
For `integral`, the
first element of the output stream is the specified `initial_value`. Thus, we can generate the first element of the output
stream without evaluating the integrand `dy`. Once we know the
first element of `y`, the
`stream_map`
in the second line of
`solve` can begin working to generate the first element of `dy`, which will produce the next element of `y`, and so on.

To take advantage of this idea, we will redefine `integral` to
expect the integrand stream to be a
*delayed argument*.
The function `integral` will force
the integrand to be evaluated only when it
is required to generate more than the first element of the output stream:

function integral(delayed_integrand, intial_value, dt) { const integrand = delayed_integrand(); const integ = pair(intial_value, add_streams(scale_stream(integrand, dt), int)); }

Now we can implement our `solve`
function
by delaying the
evaluation of `dy` in the definition of `y`:

function solve(f, y0, dt) { const y = integral( () => dy, y0, dt); const dy = stream_map(f, y); return y; }

In general, every caller of `integral` must now
delay
the integrand argument. We can demonstrate that the `solve`
function
works by approximating
$e\approx 2.718$ by computing the value at
$y=1$ of the solution to the differential equation $dy/dt=y$ with
initial condition $y(0)=1$:

stream_ref(solve(y => y, 1, 0.001), 1000);

implicitdefinition of the infinite stream of integers in section 3.5.2. Alternatively, we can give a definition of

function integral(integrand, intial_value, dt) { return pair(intial_value, is_null(integrand) ? null : integral(stream_tail(integrand), dt * head(integrand) + initial_value, dt)); }

There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in Github.

The examples in this section illustrate how
delayed evaluation
provides great programming flexibility, but the
same examples also show how this can make our programs more complex.
Our new `integral`
function,
for instance, gives us the power to
model systems with loops, but we must now remember that
`integral`
should be called with a delayed integrand, and every
function
that uses `integral` must be aware of this. In effect,
we have created two classes of
functions:
ordinary
functions
and
functions
that
take delayed arguments. In general, creating separate classes of
functions
forces us to create separate classes of higher-order
functions
as well.[1]

One way to avoid the need for two different classes of functions is to make all functions take delayed arguments. We could adopt a model of evaluation in which all arguments to functions are automatically delayed and arguments are forced only when they are actually needed (for example, when they are required by a primitive operation). This would transform our language to use normal-order evaluation, which we first described when we introduced the substitution model for evaluation in section 1.1.5. Converting to normal-order evaluation provides a uniform and elegant way to simplify the use of delayed evaluation, and this would be a natural strategy to adopt if we were concerned only with stream processing. In section 4.2, after we have studied the evaluator, we will see how to transform our language in just this way. Unfortunately, including delays in function calls wreaks havoc with our ability to design programs that depend on the order of events, such as programs that use assignment, mutate data, or perform input or output. Even a single delay in the tail of a pair can cause great confusion, as illustrated by exercise 3.51 and 3.52. As far as anyone knows, mutability and delayed evaluation do not mix well in programming languages, and devising ways to deal with both of these at once is an active area of research.

[1] This is a small reflection, in
JavaScript,
of the difficulties that conventional strongly typed languages such as
Pascal have in coping with higher-order
functions.
In such languages, the programmer must specify the data types of the arguments and the
result of each
function:
number, logical value, sequence, and so on.
Consequently, we could not express an abstraction such as `stream_map`.
Rather, we would need a different mapping
function
for each different combination of argument and result data types that might be specified
for a
`fun`.
Maintaining a practical notion of
Gordon, Milner, and Wadsworth 1979 ),
whose *type-inferencing* mechanism that uses information in the environment
to deduce the data types for newly defined
functions.

map a given functionby a single higher-order function such asfunover all the elements in a sequence

data typein the presence of higher-order functions raises many difficult issues. One way of dealing with this problem is illustrated by the language ML (

polymorphic data typesinclude templates for higher-order transformations between data types. Moreover, data types for most functions in ML are never explicitly declared by the programmer. Instead, ML includes a

3.5.4
Streams and Delayed Evaluation