From e9bede6dda1a7beb1fbdc36576bb173e652b3704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petar=20Kapri=C5=A1?= Date: Mon, 17 Mar 2025 19:44:37 +0100 Subject: [PATCH] Add exercise 2.58 This was quite a long one, since I decided to both implement support for constructing exponent ops, and also made sure that the expressions get reasonably optimized once they are constructed. --- ex-2.58.scm | 272 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 ex-2.58.scm diff --git a/ex-2.58.scm b/ex-2.58.scm new file mode 100644 index 0000000..1254c47 --- /dev/null +++ b/ex-2.58.scm @@ -0,0 +1,272 @@ +#lang sicp + +; Thoughts before the exercise: +; The first thing that I think might be an issue here is unnecessarily nested +; expressions: +; example: (((1 + 2 * 3))). We didn't have to think about these when we were simply +; using Lisp notation, since every list was basically guarranteed to have an +; operator. In our new case, every list with a length > 1 is going to have to +; have an operator, but we need a way to deal naturally with expressions of the +; form (x), where the result of the expression is obviously x. + +; One idea would be to have a function that tries to access the innermost list. +; And then use it throughout all the other functions. But this would be quite +; painful. A better idea might be to consider this list to be a new kind of +; expression, called a grouping, we could have a predicate "grouping?", a +; selector "group-mem", and a constructor "group" (but there's no reason to ever +; use the constructor.) + +; Okay, once we know how to deal with those, everything else should be clear: a +; sum is simply a list that contains +, a product is a list that contains * but +; not +, an exponentiation is a list which contains only the ** operator +; (speaking at the top level for all of these, of course). + +; It's clear how to construct a sum, it's also clear how to get the addend and +; augend, and construction, in essence, is not complicated. However, if we want +; to try and keep the expressions somewhat simplified, like we did in previous +; exercises, we may need to do more work on the constructors. + +(define op-order '(+ * **)) + +(define (list-index lst elem) + (define (li-rec lst elem index) + (cond ((null? lst) -1) + ((eq? (car lst) elem) index) + (else (li-rec (cdr lst) elem (+ index 1))))) + (li-rec lst elem 0)) + +;compares precedence of ops +(define (lower-or-eq-op op1 op2) + (<= (list-index op-order op1) + (list-index op-order op2))) + +; finds the-top-level op for an expression list: +; e.g. (1 + (2 * 4) * 3) => + +(define (top-op exp) + (define (top-op-rec exp op-order) + (cond ((null? op-order) nil) + ((memq (car op-order) exp) (car op-order)) + (else (top-op-rec exp (cdr op-order))))) + (top-op-rec exp op-order)) + +(define (make-group exp) + (list exp)) + +; a group is just a list of length 1 +(define (group? exp) + (and (pair? exp) (null? (cdr exp)))) + +(define (degroup exp) + (car exp)) + +; not the real deconstructor, but very useful +(define (degroup-repeated exp) + (if (group? exp) + (degroup-repeated (degroup exp)) + exp)) + +; We're gonna be doing this, a lot +; turns a list such as (1 (2 * 4) 3) into (1 + (2 * 4) + 3) +(define (splice-op arglist op) + (cond ((null? arglist) nil) ; 0 args + ((null? (cdr arglist)) arglist) ; 1 arg + (else (cons (car arglist) + (cons op + (splice-op (cdr arglist) op)))))) + +; This does the opposite, assumes op is the top-op of the expression +; eg. (1 + 2 * 4 + 3) => (1 (2 * 4) 3) +(define (extract-op exp op) + (cond ((null? exp) nil) + ((null? (cdr exp)) exp) + ((null? (cddr exp)) (error "Extract-op from two-member list" exp)) + ((not (memq (cadr exp) op-order)) (error "Extract-op, second element not an op" exp)) + ((eq? (cadr exp) op) (cons (car exp) + (extract-op (cddr exp) op))) + (else (cons (grab-subexp exp op) + (extract-op (drop-subexp exp op)))))) + +; two helper functions for extract-op, extract a beginning subexpression, which uses a +; higher precedence op +; e.g. exp. (2 * 4 + 3 + 5), op: + +; (grab-subexp exp op) => (2 * 4) +; (drop-subexp exp op) => (3 + 5) +; +; exp. (2 * 4 + 3), op: + +; (grab-subexp exp op) => (2 * 4) +; (drop-subexp exp op) => (3) +; +; exp. (2 * 4), op: + +; (grab-subexp exp op) => (2 * 4) +; (drop-subexp exp op) => () +(define (grab-subexp exp op) + (if (or (null? exp) (eq? op (car exp))) + nil + ; if we reach the end of the list, or the top-level op, we stop adding elements + (cons (car exp) + (grab-subexp (cdr exp) op)))) + +(define (drop-subexp exp op) + (let ((list-from-op (memq op exp))) + (if (not list-from-op) + nil + (cdr list-from-op)))) + +; accepts a complex expression containing at least one operations +; removes parentheses around subexpressions which use an operator with higher +; precedence +(define (remove-parens list op) + (cond ((null? list) nil) + ((or (number? (car list)) + (variable? (car list)) + ; we don't want to remove parentheses around ops of the same precedence + ; since we did that while extracting argument lists. And if our operation + ; is not associative (e.g. **) this could cause an error + (lower-or-eq-op (top-op (car list)) op)) + (cons (car list) (remove-parens (cdr list) op))) + ; in any other case, it's either a grouping, or a higher-precedence op + ; and we can get rid of the parens + (else (append (car list) (remove-parens (cdr list) op))))) + +(define (variable? x) (symbol? x)) +(define (same-variable? v1 v2) + (and (variable? v1) (variable? v2) (eq? v1 v2))) + +(define (=number? exp num) (and (number? exp) (= exp num))) + +(define (not-number? x) (not (number? x))) + +(define (filter predicate sequence) + (cond ((null? sequence) nil) + ((predicate (car sequence)) + (cons (car sequence) + (filter predicate (cdr sequence)))) + (else (filter predicate (cdr sequence))))) + +; instead of the "fold" function in the previous exercise, here we have several +; "optimize" functions for arglists +(define (optimize-+ args) + (let* ((consts (filter number? args)) + (rest (filter not-number? args)) + (folded-const (apply + consts))) + (if (= folded-const 0) + rest + (append (list folded-const) rest)))) + +(define (optimize-* args) + (let* ((consts (filter number? args)) + (rest (filter not-number? args)) + (folded-const (apply * consts))) + (cond ((= folded-const 1) rest) + ((= folded-const 0) '(0)) + (else (append (list folded-const) rest))))) + +(define (optimize-** args) + (cond ((null? args) (error "Empty arg list for ** in optimize-**")) + ((null? (cdr args)) args) + (else + (let* ((new-args (cons (car args) ; if len(args)>=2, we first try to + ; compute the right hand side recursively + ; if afterwards len(args)==2, we try that too + (optimize-** (cdr args)))) + (len=2 (null? (cddr new-args))) + (fst (car new-args)) + (snd (cadr new-args))) + (cond ((and len=2 (number? fst) (number? snd)) ; both constant + (list (expt (car new-args) (cadr new-args)))) + ((and len=2 (=number? snd 1)) + (list fst)) + (else new-args)))))) + +(define (make-op op x1 x2) + (let* ((ux1 (degroup-repeated x1)) ; now we know our args are either simple nums/vars or + (ux2 (degroup-repeated x2)) ; they're an operation (+,*,**) + (exp->arglist + (lambda (exp) ; as long as our subexpression: exp. is not an operation of the exact + ; same type as the one we're creating, the list of extracted args + ; should just look like (exp). Otherwise, we need to extract the + ; operators from the list + ; e.g. op=+, x1 = (2 + 3 + 2 * 4) -> result=(2 3 (2 * 4)) + ; x2 = (2 * 4) -> result=((2 * 4)) + ; then we simply append the lists + (if (or (variable? exp) + (number? exp) + (not (eq? op (top-op exp)))) + (list exp) + (extract-op exp op)))) + (arglist1 (if (and (eq? op '**) ; if both the op we're making, and the left-hand side + (exponentiation? ux1)) ; are an exponentiation, we need to simply + (list ux1) ; parenthesise the left-hand side, don't extract args, + (exp->arglist ux1))) ; because ** is right-associative + (arglist2 (exp->arglist ux2)) + (args (append arglist1 arglist2)) + (opt-args (cond ((eq? op '+) (optimize-+ args)) + ((eq? op '*) (optimize-* args)) + ((eq? op '**) (optimize-** args))))) + ; keep in mind, after this step ** must have at least 1 argument, + ; +,* can have 0 + (cond ((and (eq? op '+) (= (length opt-args) 0)) 0) + ((and (eq? op '*) (= (length opt-args) 0)) 1) + ((= (length opt-args) 1) (car opt-args)) + (else (remove-parens (splice-op opt-args op) op))))) + +(define (make-sum a1 a2) + (make-op '+ a1 a2)) + +(define (make-product m1 m2) + (make-op '* m1 m2)) + +(define (sum? x) (and (pair? x) (eq? (top-op x) '+))) +; when we split the list using grab/drop-subexp, we might get a list of length +; 1 on either end +(define (addend s) (degroup-repeated (grab-subexp s '+))) +(define (augend s) (degroup-repeated (drop-subexp s '+))) + +(define (product? x) (and (pair? x) (eq? (top-op x) '*))) +(define (multiplier p) (degroup-repeated (grab-subexp p '*))) +(define (multiplicand p) (degroup-repeated (drop-subexp p '*))) + +; for the purposes of our exponentiation exercise, we need to determine whether +; the exponent is constant so that we can apply the rule correctly +(define (constant? x var) + (or (number? x) + (and (variable? x) (not (same-variable? x var))) + (and (sum? x) + (constant? (addend x) var) + (constant? (augend x) var)) + (and (product? x) + (constant? (multiplier x) var) + (constant? (multiplicand x) var)) + (and (exponentiation? x) + (constant? (base x) var) + (constant? (exponent x) var)))) + +(define (make-exponentiation b e) + (make-op '** b e)) + +(define (exponentiation? x) (and (pair? x) (eq? (top-op x) '**))) +(define (base e) (car e)) +(define (exponent e) (caddr e)) + +(define (deriv exp var) + (cond ((number? exp) 0) + ((variable? exp) (if (same-variable? exp var) 1 0)) + ((sum? exp) (make-sum (deriv (addend exp) var) + (deriv (augend exp) var))) + ((product? exp) + (make-sum + (make-product (multiplier exp) + (deriv (multiplicand exp) var)) + (make-product (deriv (multiplier exp) var) + (multiplicand exp)))) + ((exponentiation? exp) + (if (constant? (exponent exp) var) + (make-product (make-product (exponent exp) + (make-exponentiation (base exp) + (make-sum + (exponent exp) + -1))) + (deriv (base exp) var)) + (error "non-constant exponents not yet supported: DERIV" exp))) + (else + (error "unknown expression type: DERIV" exp))))