<-- Home |--lisp |--programming-language

003 颠倒先生的数学表达式

程序员taxonomy

颠倒先生成为了一个程序员,然后学会了开发库程序,然后学会了开发应用程序,然后阅读了Lisp入门编程教程。

这都不重要,重要的是,颠倒先生是一个什么样的程序员。

前面粗鲁先生展示了如何用Lisp开发一个应用程序,并且把应用程序编译成一个可执行文件,其实,这是大部分程序设计语言的使用场景。开发应用程序的程序员,也就是大部分程序员,也就是调包侠。本质上,我们跟业务关系比较大、自己喜欢用程序设计语言进行自动化日常工作的人,都属于调包侠。Python能够如此流行,就是因为调包侠成长快、能力强。

而懒惰先生,是另外一种程序员,也就是库开发程序员。Python能够如此流行,就是因为C语言的库程序员为Python提供了大量的、功能强大的库。

graph TD
    A[程序员] --> B[专业程序员]
    A --> C[业余程序员]
    C --> D[调包侠:粗鲁先生]
    B --> F[专业应用开发程序员]
    B --> E[专业库程序员]
    C --> G[伪装成库程序员的调包侠:懒惰先生]

颠倒先生的救赎:Julian时间

一般的Lisp教程,总是不会从数学开始,而是从符号和列表这写更加基础(或者高深)的概念开始,从conscarcdr这些列表操作的基石概念,逐步引入lambdadefun这些概念,最后再讲宏、S表达式、lexical scope等等。

颠倒先生就只是想要用Lisp来做一点点计算。因为,颠倒先生,怎么说呢,都说他颠三倒四,那不就是时间前后颠倒了吗?那么,颠倒先生就想利用Lisp来理顺一下时间。

Julian时间,是一种历法,是一种计算时间的方法。Julian时间是从公元前4713年1月1日12时开始的,这个时间点被称为儒略日的起点。只要确定一个时间点,就可以计算出这个时间点对应的儒略日。同样,知道一个儒略日,也可以计算出对应的时间点。下面,用Lisp来实现这个计算过程,代码如下。

 1(defun days (mm dd yyyy)
 2  (+ dd (* 31 (+ mm (* 12 yyyy)))))
 3
 4(defconstant IGREG (days 10 15 1582))
 5
 6(defun julday (mm dd yyyy)
 7  "julday returns the Julian Day Number that begins at noon of the calendar date
 8specified by month mm, day id, and year iyyy, all integer variables. Positive year signifies A.D.;
 9negative, B.C. Remember that the year after 1 B.C. was 1 A.D."
10  (let* ((current-days (days mm dd yyyy))
11         (jy-orig (cond ((eq yyyy 0) (error "julday: there is no year 0"))
12                        ((< yyyy 0) (+ yyyy 1))
13                        (t yyyy)))
14         (jy (if (> mm 2) jy-orig (- jy-orig 1)))
15         (jm (if (> mm 2) (+ mm 1) (+ mm 13)))
16         (ja (floor (/ jy 100.0)))
17         (jul-delta (if (>= current-days IGREG)
18                        (+ (- 2 ja) (floor (* 0.25 ja)))
19                        0))
20         (jul (floor (+
21                      (floor (* 365.25 jy))
22                      (floor (* 30.6001 jm))
23                      dd
24                      1720995
25                      jul-delta))))
26    jul))
27
28(defun caldat (julian)
29  "Inverse of the function julday given above. Here julian is input as a Julian Day Number, and
30the routine outputs mm,id, and iyyy as the month, day, and year on which the specified Julian
31Day started at noon."
32  (let* ((IGREG_J 2299161) ;; <==(julday 10 15 1582)
33         (ja (cond ((>= julian IGREG_J)
34                     ;; Gregorian calendar
35                     (let ((jalpha (floor (/ (- (- julian 1867216) 0.25d0) 36524.25d0))))
36                       (- (+ julian 1 jalpha) (floor (* 0.25d0 jalpha)))))
37                   ((< julian 0)
38                     ;; Julian centuries
39                     (+ julian (* 36525 (- 1 (floor (/ julian 36525))))))
40                   (t julian)))
41         (jb (+ ja 1524))
42         (jc (floor (+ 6680.0d0 (/ (- (- jb 2439870) 122.1d0) 365.25d0))))
43         (jd (floor (+ (* 365 jc) (* 0.25d0 jc))))
44         (je (floor (/ (- jb jd) 30.6001d0)))
45         (dd (- jb jd (floor (* 30.6001d0 je))))
46         (mm-pre (- je 1))
47         (mm (if (> mm-pre 12) (- mm-pre 12) mm-pre))
48         (yyyy-pre (- jc 4715 (if (> mm 2) 1 0)))
49         (yyyy-pre2 (if (<= yyyy-pre 0) (- yyyy-pre 1) yyyy-pre))
50         (yyyy (if (< julian 0) (- yyyy-pre2 (* 100 (- 1 (floor (/ julian 36525))))) yyyy-pre2)))
51    (values mm dd yyyy)))

调用上面的两个函数就,就可以打印出一些时间的对应关系。比如,下面的代码,打印出了1900年到1902年的1月到3月所有日期对应的儒略日。

1(loop for yyyy from 1900 to 1902
2      do (loop for mm from 1 to 3
3               do (loop for dd from 1 to 31
4                        do (let* ((julian (julday mm dd yyyy))
5                                  (result (multiple-value-list (caldat julian)))
6                                  (mm-c (first result))
7                                  (dd-c (second result)))
8                             (when (and (eq mm-c mm) (eq dd-c dd))
9                                   (format t "~4,d-~02,d-~02,d ~10,d~%" yyyy mm-c dd-c julian))))))

这个里面有一个判断,比如2月只有28日,那么2月29日对应就是3月1日,转换到Julian时间,时间就是连续的,没有跳跃的日期。啊,颠倒先生觉得自己有救了!感谢Lisp!

颠倒先生的一点点小问题

即便颠倒先生是一个颠三倒四的人,经常把1 + 1写成1 1 +

但是,前面Lisp代码中间很长很长数学表达式,例如(+ dd (* 31 (+ mm (* 12 yyyy)))),连颠倒先生都觉得眼睛不舒服。颠倒先生觉得,这个表达式应该是dd + 31 * (mm + 12 * yyyy),这样看起来更加直观。颠倒先生就该庆幸,因为还有更加颠三倒四的写法,dd 31 mm 12 yyyy * + * +

这三个表达数学计算的方式分别是:

  1. 中缀表达式:dd + 31 * (mm + 12 * yyyy)
  2. 前缀表达式:(+ dd (* 31 (+ mm (* 12 yyyy))))
  3. 后缀表达式:dd 31 mm 12 yyyy * + * +

把计算算符放到前面,是Lisp的一大特色,其解释也很简单,如果把所有的计算算符表示为函数,把函数放在括号外面,就可以很容易写成:+(dd, *(31, +(mm, *(12, yyyy)))),这就容易理解多了,括号数量也没有改变。

虽然如此,颠倒先生感觉前缀表达式很符合他的胃口,可是看前面两个例子就知道,简直太难调试。

中缀表达式转前缀表达式

颠倒先生想要一个工具,可以把中缀表达式转换成前缀表达式。这个工具,就是infix-math,代码如下。

1(ql:quickload :infix-math)

这个工具包提供了一个宏(暂时也当做函数吧),infix-math/infix-math:$,这个宏接受一个中缀表达式,返回一个按照对应中缀语法计算得到的结果。

1(infix-math/infix-math:$ 2 + 3)
2;; => 5

还有一个问题,如果这个式子访问起来都必须用这样的方式,那实在是太麻烦了。有没有什么办法,可以直接用($ 2 + 3)?这是每一个调包侠都要解决的问题。

有三种方式:

  1. 使用in-package,这样就可以直接使用包中的符号。
  2. 在定义包的时候使用:use关键字,这样就可以直接使用包中的符号。
  3. 使用import,这样就可以直接使用包中的符号。

第三种方式就是直接使用import,这样就可以直接使用包中的符号。

1;; 载入infix-math包
2(require 'infix-math)
3
4;; 导入infix-math包中的符号$
5(import 'infix-math:$)
6
7;; 在当前包中使用$符号
8($ 2 + 3)
9;; => 5

这第二种方式是开发库程序时常用的方式:

1(defpackage :infix-math-test
2  (:use :cl :infix-math))
3
4(in-package :infix-math-test)
5
6($ 2 + 3)
7;; => 5

第一种方式是把Lisp当做计算器(利用REPL快速验证概念)时常用的方式:

1(require 'infix-math)
2
3(in-package :infix-math)
4;; #<PACKAGE "INFIX-MATH/INFIX-MATH">
5
6($ 2 + 3)
7;; => 5

那么,调包侠颠倒先生,就可以直接使用($ 2 + 3)这样的方式,来计算中缀表达式了。但是有两个疑问:

  1. infix-math/infix-mathinfix-math有什么区别?就是怎么回事?
  2. in-package之后怎么退出包?怎么知道自己在哪个包里面?

调包侠必备技能组

问题二的答案很简单,有一个变量*package*,这个变量就是当前包。问题一的答案,就是infix-math/infix-math的昵称叫做:infix-math

1(find-package :infix-math)
2;; #<PACKAGE "INFIX-MATH/INFIX-MATH">
3
4(package-nicknames (find-package :infix-math/infix-math))
5;; ("INFIX-MATH")

上面这两个函数,分别可以找到包和包的昵称。这样,颠倒先生就可以知道,infix-math/infix-mathinfix-math是同一个包,只是昵称不同。这在定义包的时候,可以用一个关键字来设定昵称,以简化访问。

1(defpackage :infix-math/infix-math
2  (:use :cl)
3  (:nicknames :infix-math))

我们最常见的cl包,其实就是common-lisp包的昵称。因为可以同时设置多个昵称,所以得到的昵称是一个列表。

1(package-nicknames (find-package :common-lisp-user))      
2;; ("CL-USER")
3(package-nicknames (find-package :common-lisp))      
4;; ("CL")

此外,还有一系列函数来让我们查看当前包的信息:

1(list-all-packages)
2;; ...
3;; ...
4
5(package-name *package*)
6;; ("COMMON-LISP-USER")
7
8(package-nicknames *package*)
9;; ("CL-USER")

除此之外,调包侠还需要如何找到函数,并且了解函数的帮助。

那么比如对于’infix-math’这个包,我们可以使用apropos函数来查看:

1(apropos 'infix-math)
2
3;; INFIX-MATH
4;; :INFIX-MATH = :INFIX-MATH
5;; :INFIX-MATH/DATA = :INFIX-MATH/DATA
6;; :INFIX-MATH/INFIX-MATH = :INFIX-MATH/INFIX-MATH
7;; :INFIX-MATH/SYMBOLS = :INFIX-MATH/SYMBOLS

这就对这个包的内容有一定的了解。那么要知道infix-math导出了哪些符号,可以使用do-external-symbols函数:

1(do-external-symbols (s (find-package :infix-math))
2  (format t "~a~%" s))
3;; $
4;; OVER
5;; DECLARE-UNARY-OPERATOR
6;; DECLARE-BINARY-OPERATOR
7;; ^

这样就知道,哪些是我们可以调用的函数(宏)。那么对于$,我们怎么得到一些帮助呢?

1(describe '$)
2;; INFIX-MATH/INFIX-MATH:$
3;;   [symbol]
4;; 
5;; $ names a macro:
6;;   Lambda-list: (&REST FORMULA)
7;;   Documentation:
8;;     Compile a mathematical formula in infix notation.
9;;   Source file: C:/Users/User/quicklisp/dists/quicklisp/software/infix-math-20211020-git/infix-math.lisp

颠倒先生的数学表达式

有了这个工具,颠倒先生就可以直接使用($ 2 + 3)这样的方式,来计算中缀表达式了。前面的两个函数顿时变成:

 1(defun days (mm dd yyyy)
 2  ($ dd + 31 * (mm + 12 * yyyy)))
 3
 4
 5(defconstant IGREG (days 10 15 1582))
 6
 7(defun julday (mm dd yyyy)
 8  "julday returns the Julian Day Number that begins at noon of the calendar date
 9specified by month mm, day id, and year iyyy, all integer variables. Positive year signifies A.D.;
10negative, B.C. Remember that the year after 1 B.C. was 1 A.D."
11  (let* ((current-days (days mm dd yyyy))
12         (jy-orig (cond ((eq yyyy 0) (error "julday: there is no year 0"))
13                        ((< yyyy 0) ($ yyyy + 1))
14                        (t yyyy)))
15         (jy (if (> mm 2) jy-orig ($ jy-orig - 1)))
16         (jm (if (> mm 2) ($ mm + 1) ($ mm + 13)))
17         (ja (floor ($ jy / 100.0)))
18         (jul-delta (if (>= current-days IGREG)
19                        ($ (2 - ja) + (floor (0.25 * ja)))
20                        0))
21         (jul (floor ($
22                       (floor (365.25 * jy)) +
23                       (floor (30.6001 * jm)) +
24                       dd +
25                       1720995 +
26                       jul-delta))))
27    jul))
28
29(defun caldat (julian)
30  "Inverse of the function julday given above. Here julian is input as a Julian Day Number, and
31the routine outputs mm,id, and iyyy as the month, day, and year on which the specified Julian
32Day started at noon."
33  (let* ((IGREG_J (julday 10 15 1582))
34         (ja (cond ((>= julian IGREG_J)
35                     ;; Gregorian calendar
36                     (let ((jalpha (floor ($ (julian - 1867216 - 0.25d0) / 36524.25d0))))
37                       ;  (format t "jalpha = ~A ~%" jalpha)
38                       ($ julian + 1 + jalpha - (floor (0.25d0 * jalpha)))))
39                   ((< julian 0)
40                     ;; Julian centuries
41                     ($ julian + 36525 * (1 - (floor (julian / 36525)))))
42                   (t julian)))
43         (jb ($ ja + 1524))
44         (jc (floor ($ 6680.0d0 + (jb - 2439870 - 122.1d0) / 365.25d0)))
45         (jd (floor ($ (365 * jc) + (0.25d0 * jc))))
46         (je (floor ($ (jb - jd) / 30.6001d0)))
47         (dd ($ jb - jd - (floor (30.6001d0 * je))))
48         (mm-pre ($ je - 1))
49         (mm (if (> mm-pre 12) ($ mm-pre - 12) mm-pre))
50         (yyyy-pre ($ jc - 4715 - (if (> mm 2) 1 0)))
51         (yyyy-pre2 (if (<= yyyy-pre 0) ($ yyyy-pre - 1) yyyy-pre))
52         (yyyy (if (< julian 0) ($ yyyy-pre2 - (100 * (1 - (floor (julian / 36525))))) yyyy-pre2)))
53    ; (format t "ja=~A jb=~A jc=~A jd=~A ~%" ja jb jc jd)
54    ; (format t "dd=~A julian=~A yyyy=~A ~%" dd julian yyyy)
55    ; (format t "~A ~A ~A ~A ~%" mm-pre mm yyyy-pre yyyy-pre2)
56    (values mm dd yyyy)))

这样看起来是不是好多了呢……(颠倒先生学的并没有,如果能够写成逆序表达式就好了!)

总结

  1. 调包是专业程序员和业余程序员重要的工作方式,也是程序员的重要技能。
  2. 调用包内的函数,在(require 'package-name)之后,可以使用全称前缀方式访问,也可以使用in-packageuseimport等方式把符号引入到当前包内。
  3. Lisp的包中的函数,可以通过aproposdo-external-symbols来查看。
  4. 可以通过describe来查看函数的帮助。

文章标签

|-->lisp |-->编程 |-->实用主义 |-->入门 |-->教程 |-->math |-->infix |-->prefix |-->postfix


GitHub