Начнем с более простой части А упражнения. При записи в инфиксной форме с расстановкой всех скобок операции сложения и умножения принимают всегда по два аргумента. Причем порядок следования элементов в списке всегда такой: первый аргумент, знак операции, второй аргумент.
Таким образом мы сразу же достаточно прямолинейным способом получаем определения предикатов, селекторов и конструкторов (в конструкторах не реализовано упрощение выражений для большей наглядности):
(define (sum? e)
(and (pair? e) (eq? (cadr e) '+)))
(define (addend e)
(car e))
(define (augend e)
(caddr e))
(define (make-sum a1 a2)
(list a1 '+ a2))
(define (product? e)
(and (pair? e) (eq? (cadr e) '*)))
(define (multiplier e)
(car e))
(define (multiplicand e)
(caddr e))
(define (make-product m1 m2)
(list m1 '* m2))
Теперь перейдем к части Б упражнения. Здесь ситуация осложняется тем, что каждая операция уже не задается жестко списком из трех элементов в фиксированном порядке. Для того, чтобы определить, какая операция перед нами, нужно найти то действие, которое будет выполняться первым и, соответственно, по нему определить, что это за операция и к каким операндам она применяется. Для этого введем вспомогательную процедуру split, которая, получив на вход элемент и последовательность, разбивает последовательность на список элементов до заданного элемента и список элементов после заданного элемента и возвращает список из этих двух списков. Если же элемент отсутствует в последовательности, split возвращает null:
(define (split el seq)
(define (split-iter before after)
(cond ((null? after) null)
((eq? (car after) el) (list before (cdr after)))
(else (split-iter (append before (list (car after)))
(cdr after)))))
(split-iter '() seq))
С помощью процедуры split мы теперь сможем разделять выражение для суммы или произведения по операции + или * и тем самым определить предикаты и селекторы:
(define (sum? e)
(and (pair? e)
(not (null? (split '+ e)))))
(define (omit-parenthesis e)
(if (null? (cdr e))
(car e)
e))
(define (addend e)
(omit-parenthesis (car (split '+ e))))
(define (augend e)
(omit-parenthesis (cadr (split '+ e))))
(define (product? e)
(and (pair? e)
(not (sum? e))
(not (null? (split '* e)))))
(define (multiplier e)
(omit-parenthesis (car (split '* e))))
(define (multiplicand e)
(omit-parenthesis (cadr (split '* e))))
Обратите внимание на две вещи:
- При проверке на то, является ли выражение произведением, мы сначала убеждаемся в том, что оно не является суммой. Это необходимо делать, так как сложение имеет более низкий приоритет, а значит выражение вида A*B+C является суммой, а не произведением.
- Мы вводим процедуру omit-parenthesis, которая убирает ненужные скобки вокруг переменных и чисел при получении аргументов сложения и умножения.
Теперь определим конструкторы для сложения и умножения. Здесь нам снова понадобятся вспомогательные процедуры:
(define (add-parenthesis predicate? e)
(if (predicate? e)
e
(list e)))
(define (sum-or-product? e)
(pair? e))
(define (make-operation op predicate? arg1 arg2)
(append (add-parenthesis predicate? arg1)
(list op)
(add-parenthesis predicate? arg2)))
Процедура add-parenthesis в некотором смысле обратна omit-parenthesis. Она окружает скобками выражения, не удовлетворяющую предикату. Процедура sum-or-product? является предикатом, возвращающим истину на сумме и произведении и ложь на всем остальном. В нашем случае он может быть записан очень просто. Наконец, make-operation выполняет основную работу, соединяя выражения-операнды, окружая их скобками при необходимости.
Записать конструкторы для сложения и умножения очень просто:
(define (make-sum a1 a2)
(make-operation '+ sum-or-product? a1 a2))
(define (make-product m1 m2)
(make-operation '* product? m1 m2))
На этом адаптация программы дифференцирования для записей в инфиксной форме завершена. Проверим, как она работает на каком-нибудь примере:
> (deriv '(x + 3 * (x + y + 2)) 'x)
(1 + 3 * (1 + 0 + 0) + 0 * (x + y + 2))
Как видим, расчет производится верно, но полученное в результате выражение можно упростить. Для этого просто слегка модифицируем процедуры-конструкторы, добавив в них абсолютно те же условия, которые были в старых конструкторах, работавших с префиксной формой:
(define (make-sum a1 a2)
(cond ((=number? a1 0) a2)
((=number? a2 0) a1)
((and (number? a1) (number? a2)) (+ a1 a2))
(else (make-operation '+ sum-or-product? a1 a2))))
(define (make-product m1 m2)
(cond ((or (=number? m1 0) (=number? m2 0)) 0)
((=number? m1 1) m2)
((=number? m2 1) m1)
((and (number? m1) (number? m2)) (* m1 m2))
(else (make-operation '* product? m1 m2))))
Теперь результат вычисления производной упрощается:
> (deriv '(x + 3 * (x + y + 2)) 'x)
4
То, что правила упрощения не изменились при смене представления выражений, - абсолютно нормально. Это даже подсказывает, что правила упрощения в реальной системе символьного дифференцирования были бы отделены от конструкторов и селекторов и вынесены на более высокий уровень абстракции.