Files
softwaredesign/raw/book/설계원칙-100-121.md
minsung 44e26d6972 feat: LLM Wiki 세컨드 브레인 초기 셋팅
- CLAUDE.md 생성 (볼트 운영 규칙, Karpathy LLM Wiki 10가지 규칙)
- 나의 핵심 맥락.md 생성 (아키텍트 프로필, 세컨드 브레인 목적, 핵심 소스)
- raw/ 구조 정립 (book/기존 설계원칙 보존, articles/repos/notes/ 추가)
- wiki/ 초기화 (index.md, log.md, concepts/sources/patterns/ 폴더)
- output/ 초기화
- LLMWiki/ 기존 프롬프트 패턴 파일 보존

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 14:34:29 +09:00

629 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# **Variations on an Arithmetic Theme**
In this chapter we introduce the extremely powerful but potentially dangerous flexibility technique of predicate-dispatched *generic procedures*. We start out in the relatively calm waters of arithmetic, modulating the meanings of the operator symbols.
We first generalize arithmetic to deal with symbolic algebraic expressions, and then to functions. We use a combinator system where the elements being combined are packages of arithmetic operations.
But soon we want even more flexibility. So we invent dynamically extensible generic procedures, where the applicability of a handler is determined by predicates on the supplied arguments. This is very powerful and great fun. Using generic procedures to extend the arithmetic to operate on "differential objects," we get automatic differentiation with very little work!
Predicate dispatch is pretty expensive, so we investigate ways to ameliorate that expense. In the process we invent a kind of tagged data, where a tag is just a way of memoizing the value of a predicate. To finish the chapter we demonstrate the power of generic procedures with the design of a simple, but easy to elaborate, adventure game.
# **3.1 Combining arithmetics**
Suppose we have a program that computes some useful numerical results. It depends on the meanings of the arithmetic operators that
are referenced by the program text. These operators can be extended to work on things other than the numbers that were expected by the program. With these extensions the program may do useful things that were not anticipated when the program was written. A common pattern is a program that takes numerical weights and other arguments and makes a linear combination by adding up the weighted arguments. If the addition and multiplication operators are extended to operate on tuples of numbers as well as on the original numbers, the program can make linear combinations of vectors. This kind of extension can work because the set of arithmetic operators is a well-specified and coherent entity. Extensions of numerical programs with more powerful arithmetic can work, unless the new quantities do not obey the constraints that were assumed by the author of the program. For example, multiplication of matrices does not commute, so extension of a numerical program that depends on the fact that multiplication of numbers is commutative will not work. We will ignore this problem for now.
#### **3.1.1 A simple ODE integrator**
A differential equation is a description of how the state of a system changes as an independent variable is varied; this is called the *evolution* of the system's state. <sup>1</sup> We can approximate the evolution of a system's state by sampling the independent variable at various points and approximating the state change at each sample point. This process of approximation is called *numerical integration*.
Let's investigate the generality of numerical operations in a numerical integrator for second-order ordinary differential equations. We will use an integrator that samples its independent variable at uniform intervals, each of which is called a *step*. Consider this equation:
$$D^2x(t) = F(t, x(t))$$
(3.1)
The essential idea is that a discrete approximation to the second derivative of the unknown function is a linear combination of second derivatives of some previous steps. The particular coefficients are chosen by numerical analysis and are not of interest here.
$$\frac{x(t+h) - 2x(t) + x(t-h)}{h^2} = \sum_{j=0}^{k} A(j)F(t-jh, x(t-jh))$$
(3.2)
where *h* is the step size and *A* is the array of magic coefficients. For example, Stormer's integrator of order 2 is
$$x(t+h) - 2x(t) + x(t-h)$$
$$= \frac{h^2}{12} (13F(t, x(t)) - 2F(t-h, x(t-h)) + F(t-2h, x(t-2h)))$$
(3.3)
To use this to compute the future of *x* we write a program. The procedure returned by stormer-2 is an integrator for a given function and step size, that given a history of values of *x*, produces an estimate of the value of *x* at the next time, *x*(*t* + *h*). The procedures t and x extract previous times and values of *x* from the history: (x 0 history) returns *x*(*t*), (x 1 history) returns *x*(*t h*), and (x 2 history) returns *x*(*t * 2*h*). We access the time of a step from a history similarly: (t 1 history) returns *t h*.
```
(define (stormer-2 F h)
(lambda (history)
(+ (* 2 (x 0 history))
(* -1 (x 1 history))
(* (/ (expt h 2) 12)
(+ (* 13 (F (t 0 history) (x 0 history)))
(* -2 (F (t 1 history) (x 1 history)))
(F (t 2 history) (x 2 history)))))))
```
The procedure returned by stepper takes a history and returns a new history advanced by *h* for the given integrator.
```
(define (stepper h integrator)
(lambda (history)
(extend-history (+ (t 0 history) h)
(integrator history)
history)))
```
The procedure stepper is used in the procedure evolver to produce a procedure step that will advance a history by one step. The step procedure is used in the procedure evolve that advances the history by a given number of steps of size *h*. We explicitly use specialized integer arithmetic here (the procedures named n:> and n:-) for counting steps. This will allow us to use different types of arithmetic for everything else without affecting simple counting. 2
```
(define (evolver F h make-integrator)
(let ((integrator (make-integrator F h)))
(let ((step (stepper h integrator)))
(define (evolve history n-steps)
(if (n:> n-steps 0)
(evolve (step history) (n:- n-steps 1))
history))
evolve)))
```
A second-order differential equation like equation 3.1 generally needs two initial conditions to determine a unique trajectory: *x*(*t*<sup>0</sup> ) and *x*(*t*<sup>0</sup> ) are sufficient to get *x*(*t*) for all *t*. But the Stormer multistep integrator we are using requires three history values, *x*(*t*<sup>0</sup> ), *x*(*t*<sup>0</sup> * h*), and *x*(*t*<sup>0</sup> ** 2*h*), to compute the next value *x*(*t*<sup>0</sup> + *h*). So to evolve the trajectory with this integrator we must start with an initial history that has three past values of *x*.
Consider the very simple differential equation:
$$D^2x(t) + x(t) = 0$$
In the form shown in equation 3.1 the right-hand side is:
```
(define (F t x) (- x))
```
Because all the solutions of this equation are linear combinations of sinusoids, we can get the simple sine function by initializing the history with three values of the sine function:
```
(define numeric-s0
(make-initial-history 0 .01 (sin 0) (sin -.01) (sin -.02)))
```
where the procedure make-initial-history takes the following arguments:
```
(make-initial-history t h x(t) x(t h) x(t 2h))
```
Using Scheme's built-in arithmetic, after 100 steps of size *h* = *.*01 we get a good approximation to sin(1):
```
(x 0 ((evolver F .01 stormer-2) numeric-s0 100))
.8414709493275624
(sin 1)
.8414709848078965
```
#### **3.1.2 Modulating arithmetic operators**
Let's consider the possibility of modulating what is meant by addition, multiplication, etc., for new data types unimagined by our example's programmer. Suppose we change our arithmetic operators to operate on and produce symbolic expressions rather than numerical values. This can be useful in debugging purely numerical calculations, because if we supply symbolic arguments we can examine the resulting symbolic expressions to make sure that the program is calculating what we intend it to. This can also be the basis of a partial evaluator for optimization of numerical programs.
Here is one way to accomplish this goal. We introduce the idea of an *arithmetic package*. An arithmetic package, or just *arithmetic*, is a map from operator names to their operations (implementations). We can install an arithmetic in the user's read-eval-print environment to replace the default bindings of the operators named in the arithmetic with the arithmetic's implementations.
The procedure make-arithmetic-1 generates a new arithmetic package. It takes a name for the new arithmetic, and an operationgenerator procedure that given an operator name constructs an *operation*, here a handler procedure, for that operator. The procedure make-arithmetic-1 calls the operation-generator procedure with each arithmetic operator, accumulating the results into a new arithmetic package. For symbolic arithmetic, the operation is implemented as a procedure that creates a symbolic expression by consing the operator name onto the list of its arguments.
```
(define symbolic-arithmetic-1
(make-arithmetic-1 'symbolic
(lambda (operator)
(lambda args (cons operator args)))))
```
To use this newly defined arithmetic, we install it. This redefines the arithmetic operators to use this arithmetic: 3
```
(install-arithmetic! symbolic-arithmetic-1)
```
install-arithmetic! changes the values of the user's global variables that are the names of the arithmetic operators defined in the arithmetic to their values in that arithmetic. For example, after this install:
```
(+ 'a 'b)
(+ a b)
(+ 1 2)
(+ 1 2)
```
Now we can observe the result of taking one step of the Stormer evolution: 4 5
```
(pp (x 0
((evolver F 'h stormer-2)
(make-initial-history 't 'h 'xt 'xt-h 'xt-2h)
1)))
(+ (+ (* 2 xt) (* -1 xt-h))
(* (/ (expt h 2) 12)
```
```
(+ (+ (* 13 (negate xt)) (* -2 (negate xt-h)))
(negate xt-2h))))
```
We could easily produce simplified expressions by replacing the cons in symbolic-arithmetic-1 with an algebraic simplifier, and then we would have a symbolic manipulator. (We will explore algebraic simplification in section 4.2.)
This transformation was ridiculously easy, and yet our original design didn't make any provisions for symbolic computation. We could just as easily add support for vector arithmetic, matrix arithmetic, etc.
#### **Problems with redefining operators**
The ability to redefine operators *after the fact* gives both extreme flexibility and ways to make whole new classes of bugs! (We anticipated such a problem in the evolver procedure and avoided it by using the special arithmetic operators n:> and n:- for counting steps.)
There are more subtle problems. A program that depends on the exactness of operations on integers may not work correctly for inexact floating-point numbers. This is exactly the risk that comes with the evolution of biological or technological systems— some mutations will be fatal! On the other hand, some mutations will be extremely valuable. But that risk must be balanced against the cost of narrow and brittle construction.
Indeed, it is probably impossible to prove very much about a program when the primitive procedures can be redefined, except that it will work when restricted to the types it was defined for. This is an easy but dangerous path for generalization.
## **3.1.3 Combining arithmetics**
The symbolic arithmetic cannot do numerical calculation, so we have broken our integration example by replacing the operator definitions. We really want an operator's action to depend on its arguments: for example, numerical addition for (+ 1 2) but
building a list for (+ 'a 'b). Thus the arithmetic packages must be able to determine which handler is appropriate for the arguments tendered.
#### **An improved arithmetic abstraction**
By annotating each operation with an *applicability specification*, often shortened to just an *applicability*, we can combine different kinds of arithmetic. For example, we can combine symbolic and numeric arithmetic so that a combined operation can determine which implementation is appropriate for its arguments.
An applicability specification is just a list of *cases*, each of which is a list of predicates, such as number? or symbolic?. A procedure is deemed applicable to a sequence of arguments if the arguments satisfy one of the cases—that is, if each predicate in the case is true of the corresponding argument. For example, for binary arithmetic operators, we would like the numeric operations to be applicable in just the case (number? number?) and the symbolic operations to be applicable in these cases: ((number? symbolic?) (symbolic? number?) (symbolic? symbolic?)).
We use make-operation to make an operation that includes an applicability for the handler procedure, like this:
```
(define (make-operation operator applicability procedure)
(list 'operation operator applicability procedure))
```
It is then possible to get the applicability for an operation:
```
(define (operation-applicability operation)
(caddr operation))
```
We introduce an abstraction for writing applicability information for an operation. The procedure all-args takes two arguments, the first being the number of arguments that the operation accepts (its *arity*, as on page 26), and the second being a predicate that must be true of each argument. It returns an applicability specification that can be used to determine if the operation is applicable to the
arguments supplied to it. In a numeric arithmetic, each operation takes numbers for each of its arguments.
Using all-args we can implement an operation constructor for the simplest operations:
```
(define (simple-operation operator predicate procedure)
(make-operation operator
(all-args (operator-arity operator)
predicate)
procedure))
```
We will also find it useful to have a *domain predicate* that is true for the objects (such as functions or matrices) that a given arithmetic's operations take as arguments—for example, number? for numeric arithmetic. To support this more elaborate idea we will create a constructor make-arithmetic for arithmetic packages. The procedure make-arithmetic is like make-arithmetic-1 (see page 71) but has additional arguments.
```
(make-arithmetic name
domain-predicate
base-arithmetic-packages
map-of-constant-name-to-constant
map-of-operator-name-to-operation)
```
An arithmetic package produced by make-arithmetic has a name that is useful for debugging. It has the domain predicate noted above. It has a list of arithmetic packages, called the *bases*, that the new arithmetic will be built from. In addition, the arithmetic will contain a set of named constants, and a set of operators along with their corresponding operations. The final two arguments are used to generate these sets.
An example of the use of a base arithmetic is vectors. A vector is represented as an ordered sequence of coordinates: consequently an arithmetic on vectors is defined in terms of arithmetic on its coordinates. So the base arithmetic for a vector arithmetic is the appropriate arithmetic for the vector's coordinates. A vector arithmetic with numeric coordinates will use a numeric arithmetic as its base, while a vector arithmetic with symbolic coordinates will
use a symbolic arithmetic as its base. For brevity, we often use the term "over" to specify the base, as in "vectors over numbers" or "vectors over symbols."
The base arithmetics also determine the constants and operators that the derived arithmetic will define. The defined constants will be the union of the constants defined by the bases, and the defined operators will be the union of their operators. If there are no bases, then standard sets of constant and operator names will be defined.
Using these new capabilities, we can define a numeric arithmetic with applicability information. Since numeric arithmetic is built on the Scheme substrate, the appropriate handler for the operator for Scheme number arguments is just the value of the operator symbol for the Scheme implementation. Also, certain symbols, such as the identity constants for addition and multiplication, are specially mapped.
```
(define numeric-arithmetic
(make-arithmetic 'numeric number? '()
(lambda (name) ;constant generator
(case name
((additive-identity) 0)
((multiplicative-identity) 1)
(else (default-object))))
(lambda (operator) ;operation generator
(simple-operation operator number?
(get-implementation-value
(operator->procedure-name operator))))))
```
The last two lines of this code find the procedure defined by the Scheme implementation that is named by the operator. 6
We can similarly write the symbolic-extender constructor to construct a symbolic arithmetic based on a given arithmetic.
```
(define (symbolic-extender base-arithmetic)
(make-arithmetic 'symbolic symbolic? (list base-arithmetic)
(lambda (name base-constant) ;constant generator
base-constant)
(let ((base-predicate
(arithmetic-domain-predicate base-arithmetic)))
(lambda (operator base-operation) ;operation generator
(make-operation operator
```
```
(any-arg (operator-arity operator)
symbolic?
base-predicate)
(lambda args
(cons operator args)))))))
```
One difference between this and the numeric arithmetic is that the symbolic arithmetic is applicable whenever *any* argument is a symbolic expression. <sup>7</sup> This is indicated by the use of any-arg rather than all-args; any-arg matches if at least one of the arguments satisfies the predicate passed as the second argument, and all the other arguments satisfy the predicate passed as the third argument. 8 Also notice that this symbolic arithmetic is based on a provided base-arithmetic, which will allow us to build a variety of such arithmetics.
Applicability specifications are not used as guards on the handlers: they do not prevent the application of a handler to the wrong arguments. The applicability specifications are used only to distinguish among the possible operations for an operator when arithmetics are combined, as explained below.
#### **A combinator for arithmetics**
The symbolic and numeric arithmetics are of the same shape, by construction. The symbolic-extender procedure produces an arithmetic with the same operators as the base arithmetic it is given. Making a combinator language for building composite arithmetics from parts might be a good approach.
The procedure add-arithmetics, below, is a combinator for arithmetics. It makes a new arithmetic whose domain predicate is the disjunction of the given arithmetics' domain predicates, and each of whose operators is mapped to the union of the operations for the given arithmetics. 9
```
(define (add-arithmetics . arithmetics)
(add-arithmetics* arithmetics))
(define (add-arithmetics* arithmetics)
```
```
(if (n:null? (cdr arithmetics))
(car arithmetics) ;only one arithmetic
(make-arithmetic 'add
(disjoin*
(map arithmetic-domain-predicate
arithmetics))
arithmetics
constant-union
operation-union)))
```
The third argument to make-arithmetic is a list of the arithmetic packages being combined. The arithmetic packages must be compatible in that they specify operations for the same named operators. The fourth argument is constant-union, which combines multiple constants. Here this selects one of the argument constants for use in the combined arithmetic; later we will elaborate on this. 10
```
(define (constant-union name . constants)
(let ((unique
(remove default-object?
(delete-duplicates constants eqv?))))
(if (n:pair? unique)
(car unique)
(default-object))))
```
The last argument is operation-union, which constructs the operation for the named operator in the resulting arithmetic. An operation is applicable if it is applicable in any of the arithmetics that were combined.
```
(define (operation-union operator . operations)
(operation-union* operator operations))
(define (operation-union* operator operations)
(make-operation operator
(applicability-union*
(map operation-applicability operations))
(lambda args
(operation-union-dispatch operator
operations
args))))
```
The procedure operation-union-dispatch must determine the operation to use based on the arguments supplied. It chooses the operation from the given arithmetics that is appropriate to the given arguments and applies it to the arguments. If more than one of the given arithmetics has an applicable operation, the operation from the first arithmetic in the arguments to add-arithmetics is chosen.
```
(define (operation-union-dispatch operator operations args)
(let ((operation
(find (lambda (operation)
(is-operation-applicable? operation args))
operations)))
(if (not operation)
(error "Inapplicable operation:" operator args))
(apply-operation operation args)))
```
A common pattern is to combine a base arithmetic with an extender on that arithmetic. The combination of numeric arithmetic and a symbolic arithmetic built on numeric arithmetic is such a case. So we provide an abstraction for that pattern:
```
(define (extend-arithmetic extender base-arithmetic)
(add-arithmetics base-arithmetic
(extender base-arithmetic)))
```
We can use extend-arithmetic to combine the numeric arithmetic and the symbolic arithmetic. Since the applicability cases are disjoint—all numbers for numeric arithmetic and at least one symbolic expression for symbolic arithmetic—the order of arguments to add-arithmetics is irrelevant here, except for possible performance issues.
```
(define combined-arithmetic
(extend-arithmetic symbolic-extender numeric-arithmetic))
(install-arithmetic! combined-arithmetic)
```
Let's try the composite arithmetic:
```
(+ 1 2)
3
```
```
(+ 1 'a)
(+ 1 a)
(+ 'a 2)
(+ a 2)
(+ 'a 'b)
(+ a b)
```
The integrator still works numerically (compare page 70):
```
(define numeric-s0
(make-initial-history 0 .01 (sin 0) (sin -.01) (sin -.02)))
(x 0 ((evolver F .01 stormer-2) numeric-s0 100))
.8414709493275624
```
It works symbolically (compare page 72):
```
(pp (x 0
((evolver F 'h stormer-2)
(make-initial-history 't 'h 'xt 'xt-h 'xt-2h)
1)))
(+ (+ (* 2 xt) (* -1 xt-h))
(* (/ (expt h 2) 12)
(+ (+ (* 13 (negate xt)) (* -2 (negate xt-h)))
(negate xt-2h))))
```
And it works in combination, with numeric history but symbolic step size h:
```
(pp (x 0 ((evolver F 'h stormer-2) numeric-s0 1)))
(+ 9.999833334166664e-3
(* (/ (expt h 2) 12)
-9.999750002487318e-7))
```
Notice the power here. We have combined code that can do symbolic arithmetic and code that can do numeric arithmetic. We have created a system that can do arithmetic that depends on both abilities. This is not just the union of the two abilities— it is the cooperation of two mechanisms to solve a problem that neither could solve by itself.
#### **3.1.4 Arithmetic on functions**
Traditional mathematics extends arithmetic on numerical quantities to many other kinds of objects. Over the centuries "arithmetic" has been extended to complex numbers, vectors, linear transformations and their representations as matrices, etc. One particularly revealing extension is to functions. We can combine functions of the same type using arithmetic operators:
```
(f + g)(x) = f (x) + g(x)
(f g)(x) = f (x) g(x)
(fg)(x) = f (x)g(x)
(f/g)(x) = f (x)/g(x)
```
The functions that are combined must have the same domain and codomain, and an arithmetic must be defined on the codomain.
The extension to functions is not hard. Given an arithmetic package for the codomain of the functions that we wish to combine, we can make an arithmetic package that implements the function arithmetic, assuming that functions are implemented as procedures.
```
(define (pure-function-extender codomain-arithmetic)
(make-arithmetic 'pure-function function?
(list codomain-arithmetic)
(lambda (name codomain-constant) ; *** see below
(lambda args codomain-constant))
(lambda (operator codomain-operation)
(simple-operation operator function?
(lambda functions
(lambda args
(apply-operation codomain-operation
(map (lambda (function)
(apply function args))
functions))))))))
```
Notice that the constant generator (with comment \*\*\*) must produce a constant function for each codomain constant. For example, the additive identity for functions must be the function of any number of arguments that returns the codomain additive
identity. Combining a functional arithmetic with the arithmetic that operates on the codomains makes a useful package:
```
(install-arithmetic!
(extend-arithmetic pure-function-extender
numeric-arithmetic))
((+ cos sin) 3)
-.8488724885405782
(+ (cos 3) (sin 3))
-.8488724885405782
```
By building on combined-arithmetic we can get more interesting results:
```
(install-arithmetic!
(extend-arithmetic pure-function-extender
combined-arithmetic))
((+ cos sin) 3)
-.8488724885405782
((+ cos sin) 'a)
(+ (cos a) (sin a))
(* 'b ((+ cos sin) (+ (+ 1 2) 'a)))
(* b (+ (cos (+ 3 a)) (sin (+ 3 a))))
```
The mathematical tradition also allows one to mix numerical quantities with functions by treating the numerical quantities as constant functions of the same type as the functions they will be combined with.
$$(f+1)(x) = f(x) + 1 (3.4)$$
We can implement the coercion of numerical quantities to constant functions quite easily, by minor modifications to the procedure pure-function-extender:
```
(define (function-extender codomain-arithmetic)
(let ((codomain-predicate
(arithmetic-domain-predicate codomain-arithmetic)))
```
```
(make-arithmetic 'function
(disjoin codomain-predicate function?)
(list codomain-arithmetic)
(lambda (name codomain-constant)
codomain-constant)
(lambda (operator codomain-operation)
(make-operation operator
(any-arg (operator-arity operator)
function?
codomain-predicate)
(lambda things
(lambda args
(apply-operation codomain-operation
(map (lambda (thing)
;; here is the coercion:
(if (function? thing)
(apply thing args)
thing))
things)))))))))
```
To allow the coercion of codomain quantities, such as numbers, to constant functions, the domain of the new function arithmetic must contain both the functions and the elements of the codomain of the functions (the possible values of the functions). The operator implementation is applicable if any of the arguments is a function; and functions are applied to the arguments that are given. Note that the constant generator for the make-arithmetic doesn't need to rewrite the codomain constants as functions, since the constants can now be used directly.
With this version we can
```
(install-arithmetic!
(extend-arithmetic function-extender combined-arithmetic))
((+ 1 cos) 'a)
(+ 1 (cos a))
(* 'b ((+ 4 cos sin) (+ (+ 1 2) 'a)))
(* b (+ 4 (cos (+ 3 a)) (sin (+ 3 a))))
```
This raises an interesting problem: we have symbols, such as a and b, that represent literal numbers, but nothing to represent literal functions. For example, if we write
```
(* 'b ((+ 'c cos sin) (+ 3 'a)))
```
our arithmetic will treat c as a literal number. But we might wish to have c be a literal function that combines as a function. It's difficult to do this with our current design, because c carries no type information, and the context is insufficient to distinguish usages.
But we can make a literal function that has no properties except for a name. Such a function just attaches its name to the list of its arguments.
```
(define (literal-function name)
(lambda args
(cons name args)))
```
With this definition we can have a literal function c correctly combine with other functions:
```
(* 'b ((+ (literal-function 'c) cos sin) (+ (+ 1 2) 'a)))
(* b (+ (+ (c (+ 3 a)) (cos (+ 3 a))) (sin (+ 3 a))))
```
This is a narrow solution that handles a useful case.
#### **3.1.5 Problems with combinators**
The arithmetic structures we have been building up to now are an example of the use of combinators to build complex structures by combining simpler ones. But there are some serious drawbacks to building this system using combinators. First, some properties of the structure are determined by the means of combination. For example, we pointed out that add-arithmetics prioritized its arguments, such that their order can matter. Second, the layering implicit in this design, such that the codomain arithmetic must be constructed prior to the function arithmetic, means that it's impossible to augment the codomain arithmetic after the function arithmetic has been constructed. Finally, we might wish to define an arithmetic for functions that return functions. This cannot be done in a general way within this framework, without introducing another mechanism for self reference, and self reference is cumbersome to arrange.
Combinators are powerful and useful, but a system built of combinators is not very flexible. One problem is that the shapes of the parts must be worked out ahead of time: the generality that will be available depends on the detailed plan for the shapes of the parts, and there must be a localized plan for how the parts are combined. This is not a problem for a well-understood domain, such as arithmetic, but it is not appropriate for open-ended construction. In section 3.2 we will see how to add new kinds of arithmetic incrementally, without having to decide where they go in a hierarchy, and without having to change the existing parts that already work.
Other problems with combinators are that the behavior of any part of a combinator system must be independent of its context. A powerful source of flexibility that is available to a designer is to build systems that *do* depend upon their context. By varying the context of a system we can obtain variation of the behavior. This is quite dangerous, because it may be hard to predict how a variation will behave. However, carefully controlled variations can be useful.
# **Exercise 3.1: Warmup with boolean arithmetic**
In digital design the boolean operations *and*, *or*, and *not* are written with the operators \*, +, and -, respectively.
There is a Scheme predicate boolean? that is true only of #t and #f. Use this to make a boolean arithmetic package that can be combined with the arithmetics we have. Note that all other arithmetic operators are undefined for booleans, so the appropriate result of applying something like cos to a boolean is to report an error.
The following template could help get you started:
```
(define boolean-arithmetic
(make-arithmetic 'boolean boolean? '()
(lambda (name)
(case name
((additive-identity) #f)
((multiplicative-identity) #t)
```
```
(else (default-object))))
(lambda (operator)
(let ((procedure
(case operator
((+) <...>)
((-) <...>)
((*) <...>)
((negate) <...>)
(else
(lambda args
(error "Operator undefined in Boolean"
operator))))))
(simple-operation operator boolean? procedure)))))
```
In digital design the operator - is typically used only as a unary operator and is realized as negate. When an arithmetic is installed, the binary operators +, \*, -, and / are generalized to be *n*-ary operators.
The unary application (- *operand*) is transformed by the installer into (negate *operand*). Thus to make - work, you will need to define the unary boolean operation for the operator negate.
## **Exercise 3.2: Vector arithmetic**
We will make and install an arithmetic package on geometric vectors. This is a big assignment that will bring to the surface many of the difficulties and inadequacies of the system we have developed so far.
**a.** We will represent a vector as a Scheme vector of numerical quantities. The elements of a vector are coordinates relative to some Cartesian axes. There are a few issues here. Addition (and subtraction) is defined only for vectors of the same dimension, so your arithmetic must know about dimensions. First, make an arithmetic that defines only addition, negation, and subtraction of vectors over a base arithmetic of operations applicable to the coordinates of vectors. Applying any other operation to a vector should report an error. Hint: The following procedures will be helpful:
```
(define (vector-element-wise element-procedure)
(lambda vecs ; Note: this takes multiple vectors
(ensure-vector-lengths-match vecs)
(apply vector-map element-procedure vecs)))
(define (ensure-vector-lengths-match vecs)
(let ((first-vec-length (vector-length (car vecs))))
(if (any (lambda (v)
(not (n:= (vector-length v)
first-vec-length)))
vecs)
(error "Vector dimension mismatch:" vecs))))
```
The use of apply here is subtle. One way to think about it is to imagine that the language supported an ellipsis like this:
```
(define (vector-element-wise element-procedure)
(lambda (v1 v2 ...)
(vector-map element-procedure v1 v2 ...)))
```
Build the required arithmetic and show that it works for numerical vectors and for vectors with mixed numerical and symbolic coordinates.
**b.** Your vector addition required addition of the coordinates. The coordinate addition procedure could be the value of the + operator that will be made available in the user environment by install-arithmetic!, or it could be the addition operation from the base arithmetic of your vector extension. Either of these would satisfy many tests, and using the installed addition may actually be more general. Which did you use? Show how to implement the other choice. How does this choice affect your ability to make future extensions to this system? Explain your reasoning.
Hint: A nice way to control the interpretation of operators in a procedure is to provide the procedure to use for each operator as arguments to a "maker procedure" that returns the procedure needed. For example, to control the arithmetic operations used in vector-magnitude one might write:
```
(define (vector-magnitude-maker + * sqrt)
(let ((dot-product (dot-product-maker + *)))
(define (vector-magnitude v)
(sqrt (dot-product v v)))
vector-magnitude))
```
- **c.** What shall we do about multiplication? First, for two vectors it is reasonable to define multiplication to be their dot product. But there is a bit of a problem here. You need to be able to use both the addition and multiplication operations, perhaps from the arithmetic on the coordinates. This is not hard to solve. Modify your vector arithmetic to define multiplication of two vectors as their dot product. Show that your dot product works.
- **d.** Add vector magnitude to your vector arithmetic, extending the numerical operator magnitude to give the length of a vector. The code given above is most of the work!
- **e.** Multiplication of a vector by a scalar or multiplication of a scalar by a vector should produce the scalar product (the vector with each coordinate multiplied by the scalar). So multiplication can mean either dot product or scalar product, depending on the types of its arguments. Modify your vector arithmetic to make this work. Show that your vector arithmetic can handle both dot product and scalar product. Hint: The operation-union procedure on page 78 enables a very elegant way to solve this problem.
## **Exercise 3.3: Ordering of extensions**
Consider two possible orderings for combining your vector extension (exercise 3.2) with the existing arithmetics:
```
(define vec-before-func
(extend-arithmetic
function-extender
(extend-arithmetic vector-extender combined-arithmetic)))
(define func-before-vec
```