We made the extract-dx-part procedure generic (page 110) so we could extend it for values other than differential objects and functions. Extend extract-dx-part to work with derivatives of functions that return vectors. Note: You also have to extend the replace-dx generic procedure (page 122) in the extractor. # **3.4 Efficient generic procedures** In section 3.2.3 we dispatched to a handler by finding an applicable rule using the dispatch store provided in the metadata: ``` (define (generic-procedure-dispatch metadata args) (let ((handler (get-generic-procedure-handler metadata args))) (apply handler args))) ``` The implementation of the dispatch store (on page 98) we used (on page 89) to make the simple-generic-procedure constructor was rather crude. The simple dispatch store maintains the rule set as a list of rules. Each rule is represented as a pair of an applicability and a handler. The applicability is a list of lists of predicates to apply to tendered arguments. The way a generic procedure constructed by simple-generic-procedure finds an appropriate handler is to sequentially scan the list of rules looking for an applicability that is satisfied by the arguments. This is seriously inefficient, because the applicability of many rules may have the same predicate in a given operand position: For example, for multiplication in a system of numerical and symbolic arithmetic there may be many rules whose first predicate is number?. So the number? predicate may be applied many times before finding an applicable rule. It would be good to organize the rules so that finding an applicable one does not perform redundant tests. This is usually accomplished by the use of an index. #### **3.4.1 Tries** One simple index mechanism is based on the *trie*. 28 A trie is traditionally a tree structure, but more generally it may be a directed graph. Each node in the trie has edges connecting to successor nodes. Each edge has an associated predicate. The data being tested is a linear sequence of features, in this case the arguments to a generic procedure. Starting at the root of the trie, the first feature is taken from the sequence and is tested by each predicate on an edge emanating from the root node. The successful predicate's edge is followed to the next node, and the process repeats with the remainder of the sequence of features. When we run out of features, the current node will contain the associated value, in this case an applicable handler for the arguments. It is possible that at any node, more than one predicate may succeed. If this happens, then all of the successful branches must be followed. Thus there may be multiple applicable handlers, and there must be a separate means of deciding what to do. Here is how we can use a trie. Evaluating the following sequence of commands will [incrementally](#page-1-0) construct the trie shown in figure 3.1. ![](설계원칙-162-170_images/_page_1_Figure_5.jpeg) **[Figure](#page-1-1) 3.1** A trie can be used to classify sequences of features. A trie is a directed graph in which each edge has a predicate. Starting at the root, the first feature is tested by each predicate on an edge proceeding from the root. If a predicate is satisfied, the process moves to the node at the end of that edge and the next feature is tested. This is repeated with successive features. The classification of the sequence is the set of terminal nodes arrived at. ``` (define a-trie (make-trie)) ``` We can add an edge to this trie ``` (define s (add-edge-to-trie a-trie symbol?)) ``` where add-edge-to-trie returns the new node that is at the target end of the new edge. This node is reached by being matched against a symbol. We can make chains of edges, which are referenced by lists of the corresponding edge predicates ``` (define sn (add-edge-to-trie s number?)) ``` The node sn is reached from the root via the path (list symbol? number?). Using a path, there is a simpler way to make a chain of edges than repeatedly calling add-edge-to-trie: ``` (define ss (intern-path-trie a-trie (list symbol? symbol?))) ``` We can add a value to any node (here we show symbolic values, but we will later store values that are procedural handlers): ``` (trie-has-value? sn) #f (set-trie-value! sn '(symbol number)) (trie-has-value? sn) #t (trie-value sn) (symbol number) ``` We can also use a path-based interface to set values ``` (set-path-value! a-trie (list symbol? symbol?) '(symbol symbol)) (trie-value ss) (symbol symbol) ``` Note that both intern-path-trie and set-path-value! reuse existing nodes and edges when possible, adding edges and nodes where necessary. Now we can match a feature sequence against the trie we have constructed so far: ``` (equal? (list ss) (get-matching-tries a-trie '(a b))) #t (equal? (list s) (get-matching-tries a-trie '(c))) #t ``` We can also combine matching with value fetching. The procedure get-a-value finds all matching nodes, picks one that has a value, and returns that value. ``` (get-a-value a-trie '(a b)) (symbol symbol) ``` But not all feature sequences have an associated value: ``` (get-a-value a-trie '(-4)) ;Unable to match features: (-4) ``` We can incrementally add values to nodes in the trie: ``` (set-path-value! a-trie (list negative-number?) '(negative-number)) (set-path-value! a-trie (list even-number?) '(even-number)) (get-all-values a-trie '(-4)) ((even-number) (negative-number)) ``` where get-all-values finds all the nodes matching a given feature sequence and returns their values. Given this trie implementation, we can make a dispatch store that uses a trie as its index: ``` (define (make-trie-dispatch-store) (let ((delegate (make-simple-dispatch-store)) (trie (make-trie))) (define (get-handler args) (get-a-value trie args)) (define (add-handler! applicability handler) ((delegate 'add-handler!) applicability handler) ``` ``` (for-each (lambda (path) (set-path-value! trie path handler)) applicability)) (lambda (message) (case message ((get-handler) get-handler) ((add-handler!) add-handler!) (else (delegate message)))))) ``` We make this dispatch store simple by delegating most of the operations to a simple dispatch store. The operations that are not delegated are add-handler!, which simultaneously stores the handler in the simple dispatch store and also in the trie, and gethandler, which exclusively uses the trie for access. The simple dispatch store manages the default handler and also the set of rules, which is useful for debugging. This is a simple example of the use of delegation to extend an interface, as opposed to the better-known inheritance idea. ### **Exercise 3.13: Trie rules** To make it easy to experiment with different dispatch stores, we gave generic-procedure-constructor and make-genericarithmetic the dispatch store maker. For example, we can build a full generic arithmetic as on page 95 but using make-triedispatch-store as follows: ``` (define trie-full-generic-arithmetic (let ((g (make-generic-arithmetic make-trie-dispatch- store))) (add-to-generic-arithmetic! g numeric-arithmetic) (extend-generic-arithmetic! g function-extender) (add-to-generic-arithmetic! g (symbolic-extender numeric-arithmetic)) g)) (install-arithmetic! trie-full-generic-arithmetic) ``` **a.** Does this make any change to the dependence on order that we wrestled with in section 3.2.2? - **b.** In general, what characteristics of the predicates could produce situations where there is more than one appropriate handler for a sequence of arguments? - **c.** Are there any such situations in our generic arithmetic code? We have provided a crude tool to measure the effectiveness of our dispatch strategy. By wrapping any computation with withpredicate-counts we can find out how many times each dispatch predicate is called in an execution. For example, evaluating (fib 20) in a generic arithmetic with a trie-based dispatch store may yield something like this: 29 ``` (define (fib n) (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2))))) (with-predicate-counts (lambda () (fib 20))) (109453 number) (109453 function) (54727 any-object) (109453 symbolic) 6765 ``` ## **Exercise 3.14: Dispatch efficiency: gotcha!** Given this performance tool it is instructive to look at executions of ``` (define (test-stormer-counts) (define (F t x) (- x)) (define numeric-s0 (make-initial-history 0 .01 (sin 0) (sin -.01) (sin -.02))) (with-predicate-counts (lambda () (x 0 ((evolver F 'h stormer-2) numeric-s0 1))))) ``` for the rule-list–based dispatch in make-simple-dispatch-store, in the arithmetic you get by: ``` (define full-generic-arithmetic (let ((g (make-generic-arithmetic make-simple-dispatch- store))) (add-to-generic-arithmetic! g numeric-arithmetic) (extend-generic-arithmetic! g function-extender) (add-to-generic-arithmetic! g (symbolic-extender numeric-arithmetic)) g)) (install-arithmetic! full-generic-arithmetic) ``` and the trie-based version (exercise 3.13), in the arithmetic you get by: ``` (install-arithmetic! trie-full-generic-arithmetic) ``` For some problems the trie should have much better performance than the simple rule list. We expect that the performance will be better with the trie if we have a large number of rules with the same initial segment. Understanding this is important, because the fact that sometimes the trie does not help with the performance appears counterintuitive. We explicitly introduced the trie to avoid redundant calls. Explain this phenomenon in a concise paragraph. For an additional insight, look at the performance of (fib 20) in the two implementations. When more than one handler is applicable for a given sequence of arguments, it is not clear how to use those handlers; addressing this situation is the job of a *resolution policy*. There are many considerations when designing a resolution policy. For example, a policy that chooses the most specific handler is often a good policy; however, we need more information to implement such a policy. Sometimes it is appropriate to run all of the applicable handlers and compare their results. This can be used to catch errors and provide a kind of redundancy. Or if we have partial information provided by each handler, such as a numerical interval, the results of different handlers can be combined to provide better information. #### **3.4.2 Caching** With the use of tries we have eliminated redundant evaluation of argument predicates. We can do better by using abstraction to eliminate the evaluation of predicates altogether. A predicate identifies a set of objects that are distinguished from all other objects; in other words, the predicate and the set it distinguishes are effectively the same. In our trie implementation, we use the equality of the predicate procedures to avoid redundancy; otherwise we would have redundant edges in the trie and it would be no help at all. This is also why the use of combinations of predicates doesn't mix well with the trie implementation. The problem here is that we want to build an index that discriminates objects according to predicates, but the opacity of procedures makes them unreliable when used as keys to the index. What we'd really like is to assign a name to the set distinguished by a given predicate. If we had a way to get that name from a given object by superficial examination, we could avoid computing the predicate at all. This name is a "type"; but in order to avoid confusion we will refer to this name as a *tag*. Given a way to get a tag from an object, we can make a cache that saves the handler resulting from a previous dispatch and reuses it for other dispatches whose arguments have the same tag pattern. But in the absence of explicitly attached tags, there are limitations to this approach, because we can only discriminate objects that share an implementation-specified representation. For example, it's easy to distinguish between a number and a symbol, but it's not easy to distinguish a prime number, because it's unusual for an implementation to represent them specially. We will return to the problem of explicit tagging in section 3.5, but in the meantime it is still possible to make a useful cache using the representation tags from the Scheme implementation. Given an implementation-specific procedure implementation-type-name to obtain the representation tag of an object, we can make a cached dispatch store: ``` (define a-cached-dispatch-store (cache-wrapped-dispatch-store (make-trie-dispatch-store) implementation-type-name)) ``` This dispatch store wraps a cache around a trie dispatch store, but it could just as well wrap a simple dispatch store. The heart of the cached dispatch store is a memoizer built on a hash table. The key for the hash table is the list of representation tags extracted by the implementation-type-name procedure from the arguments. By passing implementation-type-name into this dispatch-store wrapper (as get-key) we can use it to make cached dispatch stores for more powerful tag mechanisms that we will develop soon. ``` (define (cache-wrapped-dispatch-store dispatch-store get-key) (let ((get-handler (simple-list-memoizer eqv? (lambda (args) (map get-key args)) (dispatch-store 'get-handler)))) (lambda (message) (case message ((get-handler) get-handler) (else (dispatch-store message)))))) ``` The call to simple-list-memoizer wraps a cache around its last argument, producing a memoized version of it. The second argument specifies how to get the cache key from the procedure's arguments. The eqv? argument specifies how the tags will be identified in the cache. ## **Exercise 3.15: Cache performance** Using the same performance tool we introduced for exercise 3.14 on page 130, make measurements for execution of (test-stormercounts) and (fib 20) in the cached version of dispatch with the same generic arithmetics explored in exercise 3.14. Record your results. How do they compare?