The integral procedure 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.58 is modeled by the fact that integral's internal stream int integ is defined in terms of itself:
Original | JavaScript |
(define int (cons-stream initial-value (add-streams (scale-stream integrand dt) int))) | const integ = pair(initial_value, () => add_streams(scale_stream(integrand, dt), integ)); |
Original | JavaScript | |
The interpreter's ability to deal with such an implicit definition depends on the delay that is incorporated into cons-stream. Without this delay, the interpreter could not construct int before evaluating both arguments to cons-stream, which would require that int already be defined. In general, delay is crucial for using streams to model signal-processing systems that contain loops. Without delay, our models would have to be formulated so that the inputs to any signal-processing component would be fully evaluated before the output could be produced. This would outlaw loops. | The interpreter's ability to deal with such an implicit definition depends on the delay resulting from wrapping the call to add_streams in a lambda expression. Without this delay, the interpreter could not construct integ before evaluating the call to add_streams, which would require that integ already be defined. In general, such a delay is crucial for using streams to model signal-processing systems that contain loops. Without a delay, our models would have to be formulated so that the inputs to any signal-processing component would be fully evaluated before the output could be produced. This would outlaw loops. |
Original | JavaScript | |
Unfortunately, stream models of systems with loops may require uses of
delay beyond the |
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.61 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 procedure function
Original | JavaScript |
(define (solve f y0 dt) (define y (integral dy y0 dt)) (define dy (stream-map f y)) y) | 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 have properties similar to those of cons-stream, in that we can generate part of the answer given only partial information about the arguments. 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 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. Integral will force 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:
Original | JavaScript |
(define (integral delayed-integrand initial-value dt) (define int (cons-stream initial-value (let ((integrand (force delayed-integrand))) (add-streams (scale-stream integrand dt) int)))) int) | function integral(delayed_integrand, initial_value, dt) { const integ = pair(initial_value, () => { const integrand = delayed_integrand(); return add_streams(scale_stream(integrand, dt), integ); }); return integ; } |
Original | JavaScript |
(define (solve f y0 dt) (define y (integral (delay dy) y0 dt)) (define dy (stream-map f y)) y) | function solve(f, y0, dt) { const y = integral(() => dy, y0, dt); const dy = stream_map(f, y); return y; } |
Original | JavaScript |
(stream-ref (solve (lambda (y) y) 1 0.001) 1000) 2.716924 | stream_ref(solve(y => y, 1, 0.001), 1000); 2.716923932235896 |
implicitdefinition of the infinite stream of integers in section 3.5.2. Alternatively, we can give a definition of integral that is more like integers-starting-from (also in section 3.5.2):
Original | JavaScript |
(define (integral integrand initial-value dt) (cons-stream initial-value (if (stream-null? integrand) the-empty-stream (integral (stream-cdr integrand) (+ (* dt (stream-car integrand)) initial-value) dt)))) | function integral(integrand, initial_value, dt) { return pair(initial_value, is_null(integrand) ? null : integral(stream_tail(integrand), dt * head(integrand) + initial_value, dt)); } |
Write a procedure function RLC that takes as arguments the parameters $R$, $L$, and $C$ of the circuit and the time increment $dt$. In a manner similar to that of the RC procedure function of exercise 3.76, RLC should produce a procedure function that takes the initial values of the state variables, $v_{C_{0}}$ and $i_{L_{0}}$, and produces a pair (using cons) (using pair) of the streams of states $v_{C}$ and $i_{L}$. Using RLC, generate the pair of streams that models the behavior of a series RLC circuit with $R = 1$ ohm, $C= 0.2$ farad, $L = 1$ henry, $dt = 0.1$ second, and initial values $i_{L_{0}} = 0$ amps and $v_{C_{0}} = 10$ volts.
The examples in this section illustrate how the explicit use of delay and force delayed evaluation provides great programming flexibility, but the same examples also show how this can make our programs more complex. Our new integral procedure, 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 procedure function that uses integral must be aware of this. In effect, we have created two classes of procedures: functions: ordinary procedures functions and procedures functions that take delayed arguments. In general, creating separate classes of procedures functions forces us to create separate classes of higher-order procedures functions as well.[3]
One way to avoid the need for two different classes of procedures functions is to make all procedures functions take delayed arguments. We could adopt a model of evaluation in which all arguments to procedures 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 procedure 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 the single delay in cons-stream can cause great confusion, as illustrated by exercises 3.54 and 3.55. Even a single delay in the tail of a pair can cause great confusion, as illustrated by exercises 3.54 and 3.55. 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. languages.
map a given procedure function proc fun over all the elements in a sequenceby a single higher-order procedure function such as stream-map. stream_map. Rather, we would need a different mapping procedure function for each different combination of argument and result data types that might be specified for a proc. fun. Maintaining a practical notion of
data typein the presence of higher-order procedures functions raises many difficult issues. One way of dealing with this problem is illustrated by the language ML (
polymorphic data types
parametrically polymorphic data typesinclude templates for higher-order transformations between data types. Moreover, data types for most procedures functions in ML are never explicitly declared by the programmer. Instead, ML includes a type-inferencing mechanism that uses information in the environment to deduce the data types for newly defined procedures. functions. Today, statically typed programming languages have evolved to typically support some form of type inference as well as parametric polymorphism, with varying degrees of power. Haskell couples an expressive type system with powerful type inference.