一次符号计算的尝试:基于Common Lisp的微分符号计算实现

发布时间 2023-10-03 15:43:28作者: suspended-monitor

绪论

背景

作为一门具有极强表达能力的语言,Common Lisp适合于编译器实现、符号计算等应用。符号计算对于自动做题机器等方面具有广泛的应用。由于Common Lisp代码本身即为定义良好的抽象语法树(AST),因此对于实现编译器、符号计算具有天然的优势。本文基于语义分析器(Sematic Analyzer),以微分运算为例,对符号运算作了一定的探讨,证明了编译原理用于符号计算程序的编写的可行性。

本文的主要内容与意义

本文得出用于符号计算的语义分析器抽象。基于语义分析器,以关于x的四则运算一元表达式以及微分运算为例,对符号运算作了一定的探讨。基于语义分析器,实现了表达式的一种简易的简化功能。并对于一具体的四则运算一元表达式验证了所实现的函数功能。证明了编译原理用于符号计算程序的编写的可行性。

语义分析器抽象

根据微分运算的一个早先版本抽象出一个语义分析器。

(defmacro sematic (expr manage-atom &rest op-list)
  "语义分析器抽象,用于符号计算"
  `(labels
       ((computation (,expr)
          (let ((expr ,expr))
            (if (atom expr)
                (funcall ,manage-atom expr) ; manage-atom: 原子处理方法
                (let* ((operator-name (car expr))
                       (arguments (cdr expr))
                       (evaluated-arguments (mapcar #'computation arguments)))
                  (case operator-name
                    ,@op-list ; op-list: 运算符列表
                    (t (error "Not an allowed operator. "))))))))
     (computation ,expr)))

该语义分析器以递归方式定义,其作用为自最内向外逐步对S表达式进行处理,从而完成对整个AST的处理。在本文中,用于符号计算的语义分析器的作用是根据给定的AST,基于一定的规则生成新的AST。

语义分析器有三个输入参数,分别为待处理表达式,原子(atom)处理方法,以及运算符列表。

  • 待处理表达式:为S表达式,天然为定义良好的AST。由于Common Lisp本身的语法特性,可以直接跳过词法分析、语法分析阶段,直接处理AST,而无需预先生成
  • 原子处理方法:Common Lisp中,原子为非列表的表达式,如数字、符号、字符串等。
  • 运算符列表:为Common Lisp中case语句的一部分。用于定义对于各运算符(+、-、×、÷),分别需要执行的对应操作。

微分计算

基于语义分析器,可以表达微分计算函数。以下分别介绍原子处理方法及运算符列表。

(defun derivation (expr)
  "求导函数,获取S表达式,解析S表达式,输出求导后的表达式"
  (sematic expr
           ;; 导数作用于常数、变量规则
           (lambda (expr)
             (cond
               ((numberp expr) 0)
               ((symbolp expr)
                (if (eq expr 'x) 1 (error "Only x is allowded symbol. ")))
               (t (error "Neither symbol x nor a number. "))))
           ;; 导数四则运算法则
           ('+ (cons '+ evaluated-arguments))
           (- (cons '- evaluated-arguments))
           (* (cond
                ((eq 2 (length arguments))
                 (list '+
                       (list '* (car evaluated-arguments) (cadr arguments))
                       (list '* (car arguments) (cadr evaluated-arguments))))
                ; 如果是连乘列表
                ((< 2 (length arguments))
                 (derivation
                  (list '* (car arguments) (cons '* (cdr arguments)))))
                (t (error "at least 2 arguments for each operator. "))))
           (/ (cond
                ((eq 2 (length arguments))
                 (list '/
                       (list '-
                             (list '* (car evaluated-arguments) (cadr arguments))
                             (list '* (car arguments) (cadr evaluated-arguments)))
                       (list '* (cadr arguments) (cadr arguments))))
                ; 如果是连除列表
                ((< 2 (length arguments))
                 (derivation
                  (list '/ (car arguments) (cons '* (cdr arguments)))))
                (t (error "at least 2 arguments for each operator. "))))))

原子处理方法

首先讨论原子处理方法,其在本函数中表现为微分运算作用于常数、变量的规则。

  1. 微分作用于常数,所得结果为0。

    \[(C)'=0 \]

  2. 微分作用于变量\(x\),所得结果为1。其中,由于仅考虑一元表达式,x是表达式中唯一允许的符号。

    \[(x)'=1 \]

运算符列表

然后讨论运算符列表,其在本函数中表现为导数的四则运算法则

  1. 导数作用于+、-

    \[(f(x)\pm g(x))'=f'(x)\pm g'(x) \]

  2. 导数作用于乘法

    \[(f(x)g(x))'=f'(x)g+f(x)g'(x) \]

  3. 导数作用于除法

    \[\left(\frac{f(x)}{g(x)}\right)'=\frac{f'(x)g(x)-f(x)g'(x)}{g^2(x)} \]

需要注意的是,上述规则默认限定一个运算符仅进行两个数的运算。但实际Common Lisp表达式,可能是一个运算符作用于多个数字的形式,例如

(+ 1 3 5)
(* (+ 1 3 5) (- 1 3 5) (/ 48 2 6) 2)

因此,需要对上述运算法则进行一定的拓展

  1. 导数作用于+、-

    \[\left(\sum_{i=1}^k \pm f_i(x)\right)'=\pm \sum_{i=1}^k f'_i(x) \]

  2. 导数作用于乘法

    \[\left(\prod_{i=1}^k f_i(x)\right)'=f'_1(x)\prod_{i=2}^k f_i(x) + f_1(x)\left(\prod_{i=2}^k f_i(x)\right)' \]

  3. 导数作用于除法

    \[\left(\frac{f_1(x)}{\prod_{i=2}^k f_i(x)}\right)' =\frac{\displaystyle f'_1(x)\prod_{i=2}^k f_i(x)-f_1(x)\left(\prod_{i=2}^k f_i(x)\right)'} {\displaystyle\left(\prod_{i=2}^k f_i(x)\right)^2}\]

其中,导数作用于乘法、除法的处理方式又采用了递归定义。

化简运算

尽管前文定义的微分函数已经具有正常的功能,但会输出未化简结果。需要再次借助语法分析器对AST进行处理,裁剪掉多余的分支。

基于0、1的特殊性的化简

本化简函数基于0和1的特殊性,即

  1. 加法中0可以消去,若表达式中除某量外全为0,则表达式的值为该量
  2. 减法的减数中,0可以消去,若减数全为零,则表达式的值为被减数
  3. 乘法中1可以消去,若表达式中除某量外全为1,则表达式的值为该量;
    但须考虑特殊情况:
    • 当乘法中出现0,则整个表达式值为0(0乘以任何数仍为0)
  4. 除法的除数中,1可以消去,若除数全为1,则表达式的值为被除数;
    但须考虑特殊情况:
    • 首先,若除数中含有0,则应当只保留0作为除数(认为\(2\div 0\div 1 \div 2\)等于\(2\div 0\)
    • 其次,若上述情形不存在,当被除数为0时,则表达式的值为0(0除以任何非零数仍为0)
(defun simplify-01 (expr)
  "表达式化简(0和1的特殊性),消去AST多余结构
  1. +: 消去所有的0, 若为(+ 0 non-zero-or-0 0),则值为non-zero-or-0
  2. -: 消去所有的0,若为(- the-first-elem 0),则值为the-first-elem
  3. *: 消去所有的1,若为(* 1 non-one-or-1 1), 则值为non-one-for-1
        所有数乘以0仍为零
  4. /: 消去所有的1,若为(/ the-first-elem 1),则值为the-first-elem
        除数中若有零,应当只保留0;0除以所有数均为0"
  `,expr
  (sematic expr
           ;; 化简作用于原子: 无操作
           (lambda (expr) expr)
           ;; 表达式化简规则
           ('+ (let ((non-zeros
                       (remove-if (lambda(x) (eq 0 x)) evaluated-arguments)))
                 (cond ((eq 0 (length non-zeros)) 0)
                       ((eq 1 (length non-zeros)) (car non-zeros))
                       (t (cons '+ non-zeros)))))
           (- (let ((right-non-zeros
                      (remove-if (lambda(x) (eq 0 x)) (cdr evaluated-arguments)))
                    (the-first-elem (car evaluated-arguments)))
                (cond ((eq 0 (length right-non-zeros)) the-first-elem)
                      ((eq 1 (length right-non-zeros))
                       (list '- the-first-elem (car right-non-zeros)))
                      (t (cons '- (cons the-first-elem right-non-zeros))))))
           (*  (if (member 0 evaluated-arguments)
                   0
                   ; 常规规则
                   (let ((non-ones
                           (remove-if (lambda(x) (eq 1 x)) evaluated-arguments)))
                     (cond ((eq 0 (length non-ones)) 1)
                           ((eq 1 (length non-ones)) (car non-ones))
                           (t (cons '* non-ones))))))
           (/ (let ((right-non-ones
                      (remove-if (lambda(x) (eq 0 x)) (cdr evaluated-arguments)))
                    (the-first-elem (car evaluated-arguments)))
                (cond
                  ; 特殊规则
                  ((member 0 right-non-ones) (list '- the-first-elem 0))
                  ((eq 0 the-first-elem) 0)
                  ; 常规规则
                  ((eq 0 (length right-non-ones)) the-first-elem)
                  ((eq 1 (length right-non-ones))
                   (list '- the-first-elem (car right-non-ones)))
                  (t (cons '- (cons the-first-elem right-non-ones))))))))

基于四则运算特性的化简

该化简函数仍出于设计阶段,尚未完成。

(defun simplify-+-*/ (expr)
  "表达式化简(四则运算的特性)
  1. +-: + - + - => ( + + ) - ( + )
  2. */: * / * / => ( * * ) / ( * )
  3. +: 乘法结合律
  4. -: 相消
  5. /: 约分"
  `,expr
  '(do-something-here))

示例分析

运行以下示例。

; f(x) = 2*x^2 - 1/x + 5x -1
; f'(x) = 4x + 1/(x^2) +5
(defparameter f '(+ (- (* 2 x x) (/ 1 x) 1) (* 5 x)))
(defparameter |f'| (simplify-01 (derivation f)))

其中,原函数为

\[f(x)=2x^2 - \frac{1}{x}+5x-1 \]

,则导函数应当为

\[f'(x)=4x+\frac{1}{x^2}+5 \]

运行程序,结果|f'|的值为(+ (- (* 2 (+ X X)) (- (- 0 1) (* x x))) 5),符合预期。

结论

本文提出了一种基于Common Lisp的微分符号计算功能实现,并对一关于x的四则运算一元表达式示例进行验证,符合预期。验证了编译原理可以用于符号计算的实现。将来可探讨更多化简表达式的函数的规则与实现,有望实现更为完善的符号计算程序。