003 颠倒先生的数学表达式
程序员taxonomy
颠倒先生成为了一个程序员,然后学会了开发库程序,然后学会了开发应用程序,然后阅读了Lisp入门编程教程。
这都不重要,重要的是,颠倒先生是一个什么样的程序员。
前面粗鲁先生展示了如何用Lisp开发一个应用程序,并且把应用程序编译成一个可执行文件,其实,这是大部分程序设计语言的使用场景。开发应用程序的程序员,也就是大部分程序员,也就是调包侠。本质上,我们跟业务关系比较大、自己喜欢用程序设计语言进行自动化日常工作的人,都属于调包侠。Python能够如此流行,就是因为调包侠成长快、能力强。
而懒惰先生,是另外一种程序员,也就是库开发程序员。Python能够如此流行,就是因为C语言的库程序员为Python提供了大量的、功能强大的库。
graph TD A[程序员] --> B[专业程序员] A --> C[业余程序员] C --> D[调包侠:粗鲁先生] B --> F[专业应用开发程序员] B --> E[专业库程序员] C --> G[伪装成库程序员的调包侠:懒惰先生]
颠倒先生的救赎:Julian时间
一般的Lisp教程,总是不会从数学开始,而是从符号和列表这写更加基础(或者高深)的概念开始,从cons
、car
、cdr
这些列表操作的基石概念,逐步引入lambda
、defun
这些概念,最后再讲宏、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 * + * +
。
这三个表达数学计算的方式分别是:
- 中缀表达式:
dd + 31 * (mm + 12 * yyyy)
- 前缀表达式:
(+ dd (* 31 (+ mm (* 12 yyyy))))
- 后缀表达式:
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)
?这是每一个调包侠都要解决的问题。
有三种方式:
- 使用
in-package
,这样就可以直接使用包中的符号。 - 在定义包的时候使用
:use
关键字,这样就可以直接使用包中的符号。 - 使用
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)
这样的方式,来计算中缀表达式了。但是有两个疑问:
infix-math/infix-math
和infix-math
有什么区别?就是怎么回事?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-math
和infix-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)))
这样看起来是不是好多了呢……(颠倒先生学的并没有,如果能够写成逆序表达式就好了!)
总结
- 调包是专业程序员和业余程序员重要的工作方式,也是程序员的重要技能。
- 调用包内的函数,在
(require 'package-name)
之后,可以使用全称前缀方式访问,也可以使用in-package
,use
,import
等方式把符号引入到当前包内。 - Lisp的包中的函数,可以通过
apropos
和do-external-symbols
来查看。 - 可以通过
describe
来查看函数的帮助。
文章标签
|-->lisp |-->编程 |-->实用主义 |-->入门 |-->教程 |-->math |-->infix |-->prefix |-->postfix
- 本站总访问量:次
- 本站总访客数:人
- 可通过邮件联系作者:Email大福
- 也可以访问技术博客:大福是小强
- 也可以在知乎搞抽象:知乎-大福
- Comments, requests, and/or opinions go to: Github Repository