-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathatom.xml
591 lines (249 loc) · 465 KB
/
atom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>小打小闹写点bug</title>
<subtitle>乍听之下,不无道理;仔细揣摩,胡说八道</subtitle>
<link href="https://liutos.github.io/atom.xml" rel="self"/>
<link href="https://liutos.github.io/"/>
<updated>2024-12-08T09:42:13.766Z</updated>
<id>https://liutos.github.io/</id>
<author>
<name>Liutos</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>用 Prolog 开发 WEB 服务</title>
<link href="https://liutos.github.io/2024/11/17/%E7%94%A8Prolog%E5%BC%80%E5%8F%91WEB%E6%9C%8D%E5%8A%A1/"/>
<id>https://liutos.github.io/2024/11/17/%E7%94%A8Prolog%E5%BC%80%E5%8F%91WEB%E6%9C%8D%E5%8A%A1/</id>
<published>2024-11-17T13:05:00.000Z</published>
<updated>2024-12-08T09:42:13.766Z</updated>
<content type="html"><![CDATA[<h2 id="序言"><a href="#序言" class="headerlink" title="序言"></a>序言</h2><p>在绝大多数互联网行业开发者看来,Prolog 不是一门会被用在本职开发工作中的语言。更多的时候,谈论起 Prolog,人们联想到的往往是诸如“逻辑编程”、“人工智能”等词语,将它与 SQL 放在一起,视为一种 DSL,而非像 Java、Python 这样的通用编程语言。因此,我一直很好奇能否使用 Prolog 来开发一些更偏向于业务系统的程序。</p><p>答案是肯定的。基于 SWI-Prolog 这个实现和它的标准库,我开发出了一个简单的短链服务,验证了 Prolog 的确可以满足开发一个业务系统的各种功能需求。</p><h2 id="Prolog-基础知识"><a href="#Prolog-基础知识" class="headerlink" title="Prolog 基础知识"></a>Prolog 基础知识</h2><p>由于本文的大多数读者对 Prolog 应当是比较陌生的,因此在开始讲解如何用它开发一个 WEB 应用之前,必须稍作科普,介绍一下 Prolog 的基础知识,包括但不限于:</p><ol><li>Prolog 程序的基本结构;</li><li>运行 Prolog 脚本;</li><li>编译 Prolog 程序。</li></ol><h3 id="Hello-World"><a href="#Hello-World" class="headerlink" title="Hello World"></a>Hello World</h3><p>Prolog 是一门语言而不是一个具体的解释器或者编译器,为了可以运行 Prolog 脚本或编译源代码,我选择使用 SWI-Prolog。有了它,就可以运行经典的 Hello World 程序了</p><figure class="highlight prolog"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">:- initialization(main, main).</span><br><span class="line"></span><br><span class="line">main(<span class="symbol">_</span>) :-</span><br><span class="line"> format(<span class="string">"Hello, world!~n"</span>).</span><br></pre></td></tr></table></figure><p>假设上述源代码被保存在文件<code>hello_world.pl</code>中,那么执行它的命令如下</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">swipl ./hello_world.pl</span><br></pre></td></tr></table></figure><p>可以看到它打印出了所期望的文本</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20241208/hello_world%E7%9A%84%E6%95%88%E6%9E%9C.jpg" alt="hello_world的效果"></p><p>现在让我来稍微介绍一下上述代码中的细节。<code>:- initialization(main, main).</code>是一个给 SWI-Prolog 的“指示”,可以理解为其声明了程序启动后的入口是一个叫做<code>main</code>的函数。而</p><figure class="highlight prolog"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">main(<span class="symbol">_</span>) :-</span><br><span class="line"> format(<span class="string">"Hello, world!~n"</span>).</span><br></pre></td></tr></table></figure><p>则是函数<code>main</code>的定义,它调用内置的函数<code>format</code>来打印一个字符串到标准输出。</p><h3 id="编译-Prolog-程序"><a href="#编译-Prolog-程序" class="headerlink" title="编译 Prolog 程序"></a>编译 Prolog 程序</h3><p>利用 SWI-Prolog 可以像运行 Python 脚本一般来运行 Prolog 程序,当然,也可以像 C 程序一样将其从文本形态的源代码编译为一个独立的可执行文件。仍然以前文的源文件<code>hello_world.pl</code>为例,编译的命令如下</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">swipl --stand_alone=true -o hello_world -c hello_world.pl</span><br></pre></td></tr></table></figure><p>效果如下图所示</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20241208/%E7%BC%96%E8%AF%91prolog%E7%A8%8B%E5%BA%8F.jpg" alt="编译prolog程序"></p><p>在 C 语言中,被编译的程序的入口是约定俗成的,即函数<code>main</code>。而由于在文件<code>hello_world.pl</code>中用指令<code>:- initialization(main, main).</code></p><h2 id="Prolog-的使用"><a href="#Prolog-的使用" class="headerlink" title="Prolog 的使用"></a>Prolog 的使用</h2><p>在开发 WEB 服务的过程中,还会遇到许多与 WEB 无关的、Prolog 自身在其它领域的应用知识,例如:</p><ol><li>如何读写磁盘文件;</li><li>如何处理 JSON 格式的数据;</li><li>如何读写 MySQL;</li><li>如何读写 Redis;</li></ol><p>因此在这一章节中,将会分别介绍在 Prolog 中如何做到上面的这些事情。</p><h3 id="读取磁盘文件"><a href="#读取磁盘文件" class="headerlink" title="读取磁盘文件"></a>读取磁盘文件</h3><p>要读取磁盘文件的全部内容,可以使用 SWI-Prolog 的库提供的函数<a href="https://www.swi-prolog.org/pldoc/doc_for?object=read_file_to_string/3">read_file_to_string/3</a>。假设要读取的文件为<code>/tmp/demo.txt</code>,其内容如下</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Shopping List:</span><br><span class="line"></span><br><span class="line">- Milk</span><br><span class="line">- Bread</span><br><span class="line">- Eggs</span><br><span class="line">- Apples</span><br><span class="line">- Coffee</span><br></pre></td></tr></table></figure><p>那么<code>read_file_to_string/3</code>的用法如下</p><figure class="highlight prolog"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">:- use_module(library(readutil)).</span><br><span class="line"></span><br><span class="line">:- initialization(main, main).</span><br><span class="line"></span><br><span class="line">main(<span class="symbol">_</span>) :-</span><br><span class="line"> read_file_to_string(<span class="string">"/tmp/demo.txt"</span>, <span class="symbol">String</span>, []),</span><br><span class="line"> format(<span class="string">"file content is: ~s~n"</span>, [<span class="symbol">String</span>]).</span><br></pre></td></tr></table></figure><p>这样就可以将读到的文件内容完全打印到控制台上,如下图所示</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20241208/%E8%AF%BB%E5%8F%96%E7%A3%81%E7%9B%98%E6%96%87%E4%BB%B6.jpg" alt="读取磁盘文件"></p><h3 id="写入磁盘文件"><a href="#写入磁盘文件" class="headerlink" title="写入磁盘文件"></a>写入磁盘文件</h3><p>如果要将数据写入到磁盘文件中——例如,在每次处理完请求后记录日志,那么可以使用函数<code>write</code>。以将前文中的字符串<code>Hello, world!</code>写入到文件中为例,示例代码如下</p><figure class="highlight prolog"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">:- initialization(main, main).</span><br><span class="line"></span><br><span class="line">main(<span class="symbol">_</span>) :-</span><br><span class="line"> <span class="symbol">LogContent</span> = <span class="string">"Hello, world!"</span>,</span><br><span class="line"> open(<span class="string">"/tmp/access.log"</span>, write, <span class="symbol">Stream</span>),</span><br><span class="line"> write(<span class="symbol">Stream</span>, <span class="symbol">LogContent</span>),</span><br><span class="line"> close(<span class="symbol">Stream</span>).</span><br></pre></td></tr></table></figure><p>效果如下图所示</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20241208/%E5%86%99%E5%85%A5%E7%A3%81%E7%9B%98%E6%96%87%E4%BB%B6.jpg" alt="写入磁盘文件"></p><h3 id="解析-JSON-格式"><a href="#解析-JSON-格式" class="headerlink" title="解析 JSON 格式"></a>解析 JSON 格式</h3><p>JSON 已经是应用最广泛的数据交互格式之一了,因此如果一门语言要能够投产于业务系统的开发,必然离不开对 JSON 数据的处理能力。假设要处理的 JSON 数据如下</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"mysql"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"driver_string"</span><span class="punctuation">:</span> <span class="string">"DRIVER={MySQL ODBC 8.0 Driver};String Types=Unicode;password=1234567;port=3306;server=mysql;user=root"</span></span><br><span class="line"> <span class="punctuation">}</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"redis"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"hostname"</span><span class="punctuation">:</span> <span class="string">"redis"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"port"</span><span class="punctuation">:</span> <span class="number">6379</span></span><br><span class="line"> <span class="punctuation">}</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></table></figure><p>这些内容存储在文件<code>/tmp/config.json</code>中,那么下列代码会取出其中的叶子节点来输出</p><figure class="highlight prolog"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">:- use_module(library(http/json)).</span><br><span class="line"></span><br><span class="line">:- initialization(main, main).</span><br><span class="line"></span><br><span class="line">main(<span class="symbol">_</span>) :-</span><br><span class="line"> <span class="symbol">ConfigPath</span> = <span class="string">"/tmp/config.json"</span>,</span><br><span class="line"> read_file_to_string(<span class="symbol">ConfigPath</span>, <span class="symbol">String</span>, []),</span><br><span class="line"> <span class="comment">% 按照 JSON 格式反序列化为字典类型的数据。</span></span><br><span class="line"> atom_json_dict(<span class="symbol">String</span>, <span class="symbol">JSONDict</span>, []),</span><br><span class="line"> format(<span class="string">"mysql.driver_string = ~s~n"</span>, [<span class="symbol">JSONDict</span>.mysql.driver_string]),</span><br><span class="line"> format(<span class="string">"redis.hostname = ~s~n"</span>, [<span class="symbol">JSONDict</span>.redis.hostname]),</span><br><span class="line"> format(<span class="string">"redis.port = ~d~n"</span>, [<span class="symbol">JSONDict</span>.redis.port]).</span><br></pre></td></tr></table></figure><p>上文中的函数<a href="https://www.swi-prolog.org/pldoc/doc_for?object=atom_json_dict/3">atom_json_dict</a>将字符串类型的变量<code>String</code>反序列化为变量<code>JSONDict</code>。从<a href="https://www.swi-prolog.org/pldoc/man?section=ext-dict-functions">这里</a>可以看到,SWI-Prolog 为字典类型提供一个中缀操作符<code>.</code>,使得我们可以像在多数主流语言中引用类的成员变量一般,用简单的语法来获取字典内的字段——即<code>JSONDict.mysql.driver_string</code>这样的代码。</p><p>上面的代码的运行效果如下图所示</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20241208/%E8%A7%A3%E6%9E%90JSON%E5%AD%97%E5%85%B8%E7%9A%84%E6%95%88%E6%9E%9C.jpg" alt="解析JSON字典的效果"></p><h3 id="自动导入的库"><a href="#自动导入的库" class="headerlink" title="自动导入的库"></a>自动导入的库</h3><p>如果分别查看函数<code>read_file_to_string</code>和<code>atom_json_dict</code>的文档(分别在<a href="https://www.swi-prolog.org/pldoc/doc_for?object=read_file_to_string/3">这里</a>和<a href="https://www.swi-prolog.org/pldoc/doc_for?object=atom_json_dict/3">这里</a>),就会发现前者的页面上写着<code>can be autoloaded</code>,而后者没有</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20241208/%E8%87%AA%E5%8A%A8%E5%AF%BC%E5%85%A5%E7%9A%84%E6%8F%90%E7%A4%BA.jpg" alt="自动导入的提示"></p><p>所以前文关于<code>read_file_to_string</code>的例子中,即便不写上<code>:- use_module(library(readutil)).</code>,也是可以正常调用的</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20241208/%E4%B8%8D%E9%9C%80%E8%A6%81%E5%AF%BC%E5%85%A5%E7%9A%84%E4%BE%8B%E5%AD%90.jpg" alt="不需要导入的例子"></p><h3 id="事实与全局变量"><a href="#事实与全局变量" class="headerlink" title="事实与全局变量"></a>事实与全局变量</h3><p>在将磁盘上的配置文件的内容加载到内存中后,最好可以将其赋值为一个全局变量以便在所有的函数中访问到。要做到这一点,可以利用 Prolog 的一个特性:事实。</p><p>在很多 Prolog 的入门教程中,都会介绍经典的、如何用 Prolog 来回答两个人是否为某种关系的例子。例如,在<a href="https://www.ruanyifeng.com/blog/2019/01/prolog.html">这个教程</a>中,就给出了如何判断两个人是否为朋友的示例,如下图所示</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20241208/%E5%88%A4%E6%96%AD%E6%98%AF%E5%90%A6%E4%B8%BA%E6%9C%8B%E5%8F%8B%E7%9A%84%E4%BE%8B%E5%AD%90.jpg" alt="判断是否为朋友的例子"></p><p>其中,像</p><figure class="highlight prolog"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">friend(john, julia).</span><br><span class="line">friend(john, jack).</span><br><span class="line">friend(julia, sam).</span><br><span class="line">friend(julia, molly).</span><br></pre></td></tr></table></figure><p>这样的代码就是 Prolog 中的“事实”。其中,<code>friend</code>在 Prolog 中被称为“谓词”,也就是前文中一直提到的函数。因此,如果想要定义一个全局变量,可以:</p><ol><li>用<code>dynamic/1</code>声明一个只有一个参数的“动态”谓词;</li><li>用<code>asserta/1</code>新增一个事实;</li><li>在别的位置,用通常的归一语法就可以绑定全局的值到一个变量上了。</li></ol><p>就像下面这样子</p><figure class="highlight prolog"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">:- initialization(main, main).</span><br><span class="line"></span><br><span class="line"><span class="comment">% 声明为动态的以便允许使用 asserta 修改。</span></span><br><span class="line">:- dynamic odbc_driver_string/<span class="number">1.</span></span><br><span class="line"></span><br><span class="line">main(<span class="symbol">_</span>) :-</span><br><span class="line"> asserta(odbc_driver_string(<span class="string">"This is a global variable"</span>)),</span><br><span class="line"> odbc_driver_string(<span class="symbol">DriverString</span>),</span><br><span class="line"> format(<span class="string">"DriverString = ~s~n"</span>, [<span class="symbol">DriverString</span>]).</span><br></pre></td></tr></table></figure><p>效果如下</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20241208/%E7%94%A8%E4%BA%8B%E5%AE%9E%E6%9D%A5%E5%AE%9A%E4%B9%89%E5%85%A8%E5%B1%80%E5%8F%98%E9%87%8F.jpg" alt="用事实来定义全局变量"></p><h3 id="在-Docker-中运行-Prolog"><a href="#在-Docker-中运行-Prolog" class="headerlink" title="在 Docker 中运行 Prolog"></a>在 Docker 中运行 Prolog</h3><p>在之后的例子中,我还会介绍如何使用 Prolog 来读写数据库。但在摸索的过程中,我发现在 macOS 上无法运行成功,只有在 Docker 内才可行,因此这一节将会先介绍如何在 Docker 中运行 Prolog。</p><p>以前文中的 Hello World 程序为例,在已经有了源文件<code>hello_world.pl</code>的前提下,准备如下的<code>hello_world.dockerfile</code>文件</p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> swipl:stable</span><br><span class="line"></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> . /app/</span></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /app</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 编译源代码</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> swipl --goal=main --stand_alone=<span class="literal">true</span> -o hello_world -c hello_world.pl \</span></span><br><span class="line"><span class="language-bash"> && <span class="built_in">cp</span> ./hello_world /bin/</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">CMD</span><span class="language-bash"> [ <span class="string">"hello_world"</span> ]</span></span><br></pre></td></tr></table></figure><p>然后基于这份配置来构建镜像</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker build -f ./hello_world.dockerfile -t hello_world .</span><br></pre></td></tr></table></figure><p>如果无法拉取镜像<code>docker.io/library/swipl:stable</code>,可以先通过 DaoCloud 下载,然后替换掉标签</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">docker pull docker.m.daocloud.io/library/swipl:stable</span><br><span class="line">docker tag b84634ddb907 docker.io/library/swipl:stable # 此处的镜像 ID b84634ddb907 是在我的机器上的效果。</span><br></pre></td></tr></table></figure><p>然后就可以运行这个镜像了</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run hello_world</span><br></pre></td></tr></table></figure><p>效果如下图所示</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20241208/%E7%94%A8docker%E8%BF%90%E8%A1%8CProlog%E4%BB%A3%E7%A0%81.jpg" alt="用docker运行Prolog代码"></p><h2 id="读写数据库"><a href="#读写数据库" class="headerlink" title="读写数据库"></a>读写数据库</h2><p>如果一门语言无法读写数据库,尤其是关系型数据库,那么用它来开发业务系统必然是捉襟见肘的。这一章中,将会介绍如何用 SWI-Prolog 读写 MySQL 中的数据。</p><h3 id="连接-MySQL"><a href="#连接-MySQL" class="headerlink" title="连接 MySQL"></a>连接 MySQL</h3><p>通过 SWI-Prolog 的<a href="https://www.swi-prolog.org/pldoc/doc_for?object=section(%27packages/odbc.html%27)">文档</a>我们可以了解到,要操作关系型数据库,需要用到 ODBC,这一小节以连接 MySQL、调用函数<code>version</code>为例进行讲解。首先需要有一个 DSN 字符串来指定 MySQL 的连接参数,假设:</p><ul><li>密码为<code>1234567</code>;</li><li>端口号为<code>3306</code>;</li><li>主机名为<code>mysql</code>;</li><li>用户名为<code>root</code>;</li></ul><p>那么这串 DSN 可以是<code>DRIVER={MySQL ODBC 8.0 Driver};String Types=Unicode;password=1234567;port=3306;server=mysql;user=root</code>。将其传递给谓词<code>odbc_driver_connect</code>即可连接上 MySQL。然后可以用谓词<code>odbc_query</code>来提交 SQL 语句给 MySQL,并获取执行结果。完整的代码如下所示(其中数据库的密码被我替换为了星号)</p><figure class="highlight prolog"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">:- use_module(library(odbc)).</span><br><span class="line"></span><br><span class="line">:- initialization(main, main).</span><br><span class="line"></span><br><span class="line">main(<span class="symbol">_</span>) :-</span><br><span class="line"> <span class="symbol">Dsn</span> = <span class="string">"DRIVER={MySQL ODBC 8.0 Driver};String Types=Unicode;password=******;port=3306;server=host.docker.internal;user=root"</span>,</span><br><span class="line"> odbc_driver_connect(<span class="symbol">Dsn</span>, <span class="symbol">Connection</span>, []),</span><br><span class="line"> <span class="symbol">Sql</span> = <span class="string">"SELECT VERSION()"</span>,</span><br><span class="line"> odbc_query(<span class="symbol">Connection</span>, <span class="symbol">Sql</span>, row(<span class="symbol">Version</span>)),</span><br><span class="line"> odbc_disconnect(<span class="symbol">Connection</span>),</span><br><span class="line"> format(<span class="string">"Version is ~s~n"</span>, [<span class="symbol">Version</span>]).</span><br></pre></td></tr></table></figure><p>为了运行它,还得在容器中安装 MySQL 的驱动。修改后的完整 Dockerfile 文件如下</p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> swipl:stable</span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> apt-get clean \</span></span><br><span class="line"><span class="language-bash"> && apt-get update \</span></span><br><span class="line"><span class="language-bash"> && apt-get install -y unixodbc-dev</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> . /app/</span></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /app</span></span><br><span class="line"><span class="comment"># 下列代码来自[这里](https://stackoverflow.com/questions/68590463/linux-installing-mysql-odbc-driver-error),在容器内安装 ODBC 驱动程序。</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> tar -C /tmp/ -xzvf mysql-connector-odbc-8.4.0-linux-glibc2.28-aarch64.tar.gz \</span></span><br><span class="line"><span class="language-bash"> && <span class="built_in">cd</span> /tmp/ \</span></span><br><span class="line"><span class="language-bash"> && <span class="built_in">cp</span> -r ./mysql-connector-odbc-8.4.0-linux-glibc2.28-aarch64/bin/* /usr/local/bin \</span></span><br><span class="line"><span class="language-bash"> && <span class="built_in">cp</span> -r ./mysql-connector-odbc-8.4.0-linux-glibc2.28-aarch64/lib/* /usr/local/lib \</span></span><br><span class="line"><span class="language-bash"> && myodbc-installer -a -d -n <span class="string">"MySQL ODBC 8.0 Driver"</span> -t <span class="string">"Driver=/usr/local/lib/libmyodbc8w.so"</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 编译源代码</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> swipl --goal=main --stand_alone=<span class="literal">true</span> -o query_version -c query_version.pl \</span></span><br><span class="line"><span class="language-bash"> && <span class="built_in">cp</span> ./query_version /bin/</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">CMD</span><span class="language-bash"> [ <span class="string">"query_version"</span> ]</span></span><br></pre></td></tr></table></figure><p>然后构建镜像并运行即可,效果如下图所示</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20241208/%E8%BF%9E%E6%8E%A5%E6%95%B0%E6%8D%AE%E5%BA%93.jpg" alt="连接数据库"></p>]]></content>
<summary type="html"></summary>
<category term="prolog" scheme="https://liutos.github.io/tags/prolog/"/>
<category term="swi-prolog" scheme="https://liutos.github.io/tags/swi-prolog/"/>
<category term="web" scheme="https://liutos.github.io/tags/web/"/>
</entry>
<entry>
<title>单测要用 Redis 集群?教你用 docker 一键部署</title>
<link href="https://liutos.github.io/2024/06/30/%E7%94%A8dockercompose%E9%83%A8%E7%BD%B2Redis%E9%9B%86%E7%BE%A4/"/>
<id>https://liutos.github.io/2024/06/30/%E7%94%A8dockercompose%E9%83%A8%E7%BD%B2Redis%E9%9B%86%E7%BE%A4/</id>
<published>2024-06-30T01:32:10.000Z</published>
<updated>2024-06-30T05:37:50.000Z</updated>
<content type="html"><![CDATA[<h2 id="序言"><a href="#序言" class="headerlink" title="序言"></a>序言</h2><p>在目前我参与开发的代码仓库中,当需要使用 Redis 时,基本上用的都是 Redis 集群。因此,我在办公电脑上也搭建了一个 Redis 集群,以便让我为这些仓库编写的单元测试能成功运行起来。</p><p>尽管 Redis 官方提供了部署集群的<a href="https://redis.io/docs/latest/operate/oss_and_stack/management/scaling/">指引</a>,但这要求团队内的每位成员都要依次执行如下操作:</p><ol><li>用 Homebrew 安装 Redis;</li><li>创建 6 个目录;</li><li>创建 6 份<code>redis.conf</code>配置文件;</li><li>启动 6 个<code>redis-server</code>进程;</li><li>用<code>redis-cli</code>创建集群。</li></ol><p>为了可以让更多的人愿意执行单元测试,必须要降低部署 Redis 集群的操作难度。因此,本文旨在提供一种基于 Docker 的、一键部署 Redis 集群的办法。</p><h2 id="启动单个-Redis-容器"><a href="#启动单个-Redis-容器" class="headerlink" title="启动单个 Redis 容器"></a>启动单个 Redis 容器</h2><p>Redis 集群需要至少 6 个实例,那么首先要解决的问题便是如何启动单个 Redis 容器。从前文提到的部署集群的指引中可以知道,要启动一个集群模式的 Redis 实例,所需要的配置文件<code>redis.conf</code>的内容如下</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">port 8000</span><br><span class="line">cluster-enabled yes</span><br><span class="line">cluster-config-file nodes.conf</span><br><span class="line">cluster-node-timeout 5000</span><br><span class="line">appendonly yes</span><br></pre></td></tr></table></figure><p>参考官方的 redis 镜像的<a href="https://hub.docker.com/_/redis">文档</a>的方式,让<code>redis-server</code>读取来自于宿主机的配置文件,示例代码如下</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">选项 --<span class="built_in">rm</span> 使该容器在退出后(例如按下 ctrl-c)可以被删除,毕竟这里只是先做个演示,不需要留下它。</span></span><br><span class="line">sudo docker run --rm -v "`pwd`:/usr/local/etc/redis" redis redis-server "/usr/local/etc/redis/redis.conf"</span><br></pre></td></tr></table></figure><p>效果如下图所示</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20240630/%E5%90%AF%E5%8A%A8%E5%8D%95%E4%B8%AARedis%E5%AE%9E%E4%BE%8B.jpeg" alt="启动单个 Redis 容器"></p><h2 id="启动-6-个-Redis-容器"><a href="#启动-6-个-Redis-容器" class="headerlink" title="启动 6 个 Redis 容器"></a>启动 6 个 Redis 容器</h2><p>接下来以相似的方式启动全部的 6 个 Redis 容器。首先创建 6 个目录</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">这里我用 Redis 监听的端口号作为目录名,但这并非强制要求。</span></span><br><span class="line">mkdir 8000 8001 8002 8003 8004 8005</span><br></pre></td></tr></table></figure><p>然后在每一个目录中都创建配置文件<code>redis.conf</code></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">for port in 8000 8001 8002 8003 8004 8005 ;</span><br><span class="line">do</span><br><span class="line"> cat << EOF > "./${port}/redis.conf"</span><br><span class="line">port ${port}</span><br><span class="line">cluster-enabled yes</span><br><span class="line">cluster-config-file nodes.conf</span><br><span class="line">cluster-node-timeout 5000</span><br><span class="line">appendonly yes</span><br><span class="line">EOF</span><br><span class="line">done</span><br></pre></td></tr></table></figure><p>最后启动它们</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">创建出名为 some-network 的容器间网络。</span></span><br><span class="line">docker network create some-network</span><br><span class="line"></span><br><span class="line">for port in 8000 8001 8002 8003 8004 8005 ;</span><br><span class="line">do</span><br><span class="line"> # --name 选项让这 6 个容器拥有确定的且不同的主机名,以便之后可以在 redis-cli 中指定它们。</span><br><span class="line"> # --network 选项让这 6 个容器处于同一个网络下,以便集群内的节点可以互相通信。</span><br><span class="line"> docker run --name "some-redis-${port}" --network some-network --rm -d -v "`pwd`/${port}:/usr/local/etc/redis" redis redis-server '/usr/local/etc/redis/redis.conf'</span><br><span class="line">done</span><br></pre></td></tr></table></figure><p>效果如下图所示</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20240630/%E5%90%AF%E5%8A%A86%E4%B8%AARedis%E5%AE%B9%E5%99%A8.jpeg" alt="启动 6 个 Redis 容器"></p><h2 id="创建集群"><a href="#创建集群" class="headerlink" title="创建集群"></a>创建集群</h2><p>此时 6 个运行中的 Redis 实例还不构成一个集群,还需要执行<code>redis-cli</code>的命令<code>--cluster create</code>才行。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">给 redis-cli 的选项 --cluster-yes 使其默认接受集群内节点的分配情况。这在脚本执行、无法通过标准输出敲入 <span class="built_in">yes</span> 的时候很有用。</span></span><br><span class="line">docker run --network some-network --rm -i -t redis redis-cli --cluster create some-redis-8000:8000 some-redis-8001:8001 some-redis-8002:8002 some-redis-8003:8003 some-redis-8004:8004 some-redis-8005:8005 --cluster-replicas 1 --cluster-yes</span><br></pre></td></tr></table></figure><p>但命令<code>--cluster create</code>并非幂等的,只能在创建集群的时候使用一次。因此,如果希望通过脚本一键搭建、启动集群,则必须在创建前先检查集群是否曾经被创建过。借助<code>redis-cli</code>的命令<code>--cluster check</code>可以实现</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">docker run --network some-network --rm -i -t redis redis-cli --cluster check some-redis-8000:8000 | grep 'All 16384 slots covered.' > /dev/null</span><br><span class="line">if [[ "$?" == '0' ]]; then</span><br><span class="line"> echo "Redis 集群已经创建好了。"</span><br><span class="line">else</span><br><span class="line"> echo "开始创建 Redis 集群。"</span><br><span class="line"> docker run --network some-network --rm -i -t redis redis-cli --cluster create some-redis-8000:8000 some-redis-8001:8001 some-redis-8002:8002 some-redis-8003:8003 some-redis-8004:8004 some-redis-8005:8005 --cluster-replicas 1 --cluster-yes</span><br><span class="line">fi</span><br></pre></td></tr></table></figure><p>创建成功后,只要新的容器也使用网络<code>some-network</code>,就可以读写集群中的数据了</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run --network some-network --rm -i -t redis redis-cli -c -h some-redis-8000 -p 8000</span><br></pre></td></tr></table></figure><p>效果如下图所示</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20240630/%E8%AF%BB%E5%86%99Redis%E9%9B%86%E7%BE%A4.jpeg" alt="读写 Redis 集群"></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>我将上面的内容集成到了<code>redis_cluster</code>这个 GitHub <a href="https://github.com/Liutos/redis_cluster">仓库</a>中,只需要克隆到本地并执行脚本<code>start.sh</code>即可,效果如下图所示</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20240630/%E4%B8%80%E9%94%AE%E5%90%AF%E5%8A%A8Redis%E9%9B%86%E7%BE%A4.jpeg" alt="一键启动 Redis 集群"></p><p>全文完。</p>]]></content>
<summary type="html"></summary>
<category term="docker" scheme="https://liutos.github.io/tags/docker/"/>
<category term="redis" scheme="https://liutos.github.io/tags/redis/"/>
</entry>
<entry>
<title>从 Ruby 的 method_missing 到杂鱼 Common Lisp</title>
<link href="https://liutos.github.io/2024/05/05/getattr-%E5%9C%A8CommonLisp%E4%B8%AD%E7%9A%84%E8%BF%91%E4%BC%BC%E7%89%B9%E6%80%A7/"/>
<id>https://liutos.github.io/2024/05/05/getattr-%E5%9C%A8CommonLisp%E4%B8%AD%E7%9A%84%E8%BF%91%E4%BC%BC%E7%89%B9%E6%80%A7/</id>
<published>2024-05-05T13:13:00.000Z</published>
<updated>2024-05-05T13:14:19.000Z</updated>
<content type="html"><![CDATA[<h1 id="从-Ruby-的-method-missing-到杂鱼-Common-Lisp"><a href="#从-Ruby-的-method-missing-到杂鱼-Common-Lisp" class="headerlink" title="从 Ruby 的 method_missing 到杂鱼 Common Lisp"></a>从 Ruby 的 method_missing 到杂鱼 Common Lisp</h1><p>在 Ruby 中当调用一个对象不存在的方法时,会触发解释器调用该对象的<code>method_missing</code>方法。例如下面的代码</p><figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># -*- encoding: UTF-8 -*-</span></span><br><span class="line"><span class="keyword">class</span> A</span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">method_missing</span>(<span class="params">m, *args, &block</span>)</span><br><span class="line"> puts <span class="string">'now you see me'</span></span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">A.new().demo()</span><br></pre></td></tr></table></figure><p>运行到方法调用<code>demo()</code>时,由于该方法未定义,于是解释器会转而调用方法<code>method_missing</code>,并将相同的方法名(即<code>demo</code>)、参数列表等传递给它。其运行结果便是在标准输出中打印出<code>now you see me</code>这句话。</p><p>在 Python 中有<code>method_missing</code>的等价物——<code>__getattr__</code>方法。与 Ruby 不同的是,调用不存在的方法对于 Python 解释器而言,只是一次寻常的<code>AttributeError</code>异常,然后解释器会调用对象的<code>__getattr__</code>方法。与前文的 Ruby 代码类似的写法如下</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">A</span>:</span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">__getattr__</span>(<span class="params">self, name</span>):</span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">replacement</span>(<span class="params">*args, **kwargs</span>):</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">'now you see me'</span>)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> replacement</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">A().demo()</span><br></pre></td></tr></table></figure><p>利用<code>__getattr__</code>可以实现一个透明缓存。例如,假设有一个类<code>Slow</code>,它提供了<code>a</code>、<code>b</code>,以及<code>c</code>等几个比较耗时的方法。那么可以实现一个类<code>Cached</code>,由它来代理对<code>Slow</code>类的实例方法的调用、将结果缓存起来加速下一次的调用,再返回给调用方,示例代码如下</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> json</span><br><span class="line"><span class="keyword">import</span> time</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Slow</span>:</span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">a</span>(<span class="params">self</span>):</span><br><span class="line"> time.sleep(<span class="number">1</span>)</span><br><span class="line"> <span class="keyword">return</span> <span class="number">2</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">b</span>(<span class="params">self</span>):</span><br><span class="line"> time.sleep(<span class="number">1</span>)</span><br><span class="line"> <span class="keyword">return</span> <span class="number">23</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">c</span>(<span class="params">self</span>):</span><br><span class="line"> time.sleep(<span class="number">1</span>)</span><br><span class="line"> <span class="keyword">return</span> <span class="number">233</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Cached</span>:</span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, slow: Slow</span>):</span><br><span class="line"> <span class="variable language_">self</span>._slow = slow</span><br><span class="line"></span><br><span class="line"> <span class="variable language_">self</span>._cache = {}</span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">__getattr__</span>(<span class="params">self, name</span>):</span><br><span class="line"> f = <span class="built_in">getattr</span>(<span class="variable language_">self</span>._slow, name)</span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">replacement</span>(<span class="params">*args, **kwargs</span>):</span><br><span class="line"> key = json.dumps([args, kwargs])</span><br><span class="line"> <span class="keyword">if</span> key <span class="keyword">in</span> <span class="variable language_">self</span>._cache:</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">self</span>._cache[key]</span><br><span class="line"></span><br><span class="line"> v = f(*args, **kwargs)</span><br><span class="line"> <span class="variable language_">self</span>._cache[key] = v</span><br><span class="line"> <span class="keyword">return</span> v</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> replacement</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">run_and_timing</span>(<span class="params">f, label</span>):</span><br><span class="line"> begin_at = time.time()</span><br><span class="line"> v = f()</span><br><span class="line"> duration = time.time() - begin_at</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">'%s 耗时 %s 秒'</span> % (label, duration))</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">'__main__'</span>:</span><br><span class="line"> cached = Cached(Slow())</span><br><span class="line"> run_and_timing(<span class="keyword">lambda</span>: cached.a(), <span class="string">'第一次'</span>)</span><br><span class="line"> run_and_timing(<span class="keyword">lambda</span>: cached.a(), <span class="string">'第二次'</span>)</span><br></pre></td></tr></table></figure><p>在我的机器上运行的结果为</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">第一次 耗时 1.0018281936645508 秒</span><br><span class="line">第二次 耗时 2.8848648071289062e-05 秒</span><br></pre></td></tr></table></figure><p>在 Common Lisp 中有没有与<code>__getattr__</code>对应的特性呢?有的,那便是广义函数<code>slot-missing</code>。但可惜的是,它并不适用于调用一个不存在的方法的场景,因为在 Common Lisp 中方法并不属于作为第一个参数的实例对象,而是属于广义函数的(即 Common Lisp 不是单派发、而是多派发的,可以参见<a href="https://liutos.github.io/2021/10/15/%E4%B8%BA%E4%BB%80%E4%B9%88%E7%AE%A1Java%E5%8F%AB%E5%8D%95%E6%B4%BE%E5%8F%91%E4%BB%A5%E5%8F%8A%E4%BB%80%E4%B9%88%E6%98%AF%E5%A4%9A%E6%B4%BE%E5%8F%91/">这篇文章</a>)。所以调用一个不存在的方法不会导致调用<code>slot-missing</code>,而是会调用<code>no-applicable-method</code>。如下列代码所示</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defgeneric</span> demo-gf (<span class="name">a</span>)</span><br><span class="line"> (<span class="symbol">:documentation</span> <span class="string">"用于演示的广义函数。"</span>))</span><br><span class="line"></span><br><span class="line">(<span class="name">defclass</span> A ()</span><br><span class="line"> ())</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">(<span class="name">defclass</span> B ()</span><br><span class="line"> ())</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">(<span class="name">defmethod</span> demo-gf ((<span class="name">a</span> A))</span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"这是类 A 的实例方法。~%"</span>))</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">(<span class="name">defmethod</span> no-applicable-method ((<span class="name">gf</span> (<span class="name">eql</span> #'demo-gf)) <span class="symbol">&rest</span> args)</span><br><span class="line"> (<span class="name">declare</span> (<span class="name">ignorable</span> args gf))</span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"now you see me"</span>))</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">(<span class="name">defun</span> main ()</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">a</span> (<span class="name">make-instance</span> 'B)))</span><br><span class="line"> (<span class="name">demo-gf</span> a)))</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">(<span class="name">main</span>)</span><br></pre></td></tr></table></figure><p>假设上述代码保存在文件<code>no_applicable_method_demo.lisp</code>中,可以像下面这样运行它们</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">ros run --load ./no_applicable_method_demo.lisp -q</span></span><br><span class="line">now you see me</span><br></pre></td></tr></table></figure><p>当代码运行到<code>(demo-gf a)</code>时,由于没有为广义函数<code>demo-gf</code>定义过参数列表的类型为<code>(B)</code>的方法,因此 SBCL 调用了广义函数<code>no-applicable-method</code>,后者有<a href="http://www.ai.mit.edu/projects/iiip/doc/CommonLISP/HyperSpec/Body/glo_a.html#applicable_method">applicable 的方法</a>,因此会调用它并打印出<code>now you see me</code>。</p><p>如果想利用这一特性来实现透明缓存,那么必须:</p><ol><li>为每一个需要缓存的广义函数都编写其<code>no-applicable-method</code>方法;</li><li>手动检查参数列表的第一个参数的类型是否为特定的类。</li></ol><p>如下列代码所示</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defgeneric</span> a (<span class="name">a</span>))</span><br><span class="line">(<span class="name">defgeneric</span> b (<span class="name">a</span>))</span><br><span class="line">(<span class="name">defgeneric</span> c (<span class="name">a</span>))</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">(<span class="name">defclass</span> Slow ()</span><br><span class="line"> ())</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">(<span class="name">defclass</span> Cached ()</span><br><span class="line"> ((<span class="name">cache</span></span><br><span class="line"> <span class="symbol">:accessor</span> cached-cache</span><br><span class="line"> <span class="symbol">:initform</span> (<span class="name">make-hash-table</span> <span class="symbol">:test</span> #'equal))</span><br><span class="line"> (<span class="name">slow</span></span><br><span class="line"> <span class="symbol">:accessor</span> cached-slow</span><br><span class="line"> <span class="symbol">:initarg</span> <span class="symbol">:slow</span>)))</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">(<span class="name">defmethod</span> a ((<span class="name">a</span> Slow))</span><br><span class="line"> (<span class="name">sleep</span> <span class="number">1</span>)</span><br><span class="line"> <span class="number">2</span>)</span><br><span class="line">(<span class="name">defmethod</span> b ((<span class="name">a</span> Slow))</span><br><span class="line"> (<span class="name">sleep</span> <span class="number">1</span>)</span><br><span class="line"> <span class="number">23</span>)</span><br><span class="line">(<span class="name">defmethod</span> c ((<span class="name">a</span> Slow))</span><br><span class="line"> (<span class="name">sleep</span> <span class="number">1</span>)</span><br><span class="line"> <span class="number">233</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">(<span class="name">defmethod</span> no-applicable-method ((<span class="name">gf</span> (<span class="name">eql</span> #'a)) <span class="symbol">&rest</span> args)</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">instance</span> (<span class="name">first</span> args)))</span><br><span class="line"> (<span class="name">if</span> (<span class="name">typep</span> instance 'Cached)</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">slow</span> (<span class="name">cached-slow</span> instance))</span><br><span class="line"> (<span class="name">key</span> (<span class="name">rest</span> args)))</span><br><span class="line"> (<span class="name">multiple-value-bind</span> (<span class="name">v</span> foundp)</span><br><span class="line"> (<span class="name">gethash</span> key (<span class="name">cached-cache</span> instance))</span><br><span class="line"> (<span class="name">if</span> foundp</span><br><span class="line"> v</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">v</span> (<span class="name">apply</span> gf slow (<span class="name">rest</span> args))))</span><br><span class="line"> (<span class="name">setf</span> (<span class="name">gethash</span> key (<span class="name">cached-cache</span> instance)) v)</span><br><span class="line"> v))))</span><br><span class="line"> (<span class="name">call-next-method</span>))))</span><br></pre></td></tr></table></figure><p>在我的机器上运行的结果为</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">CL-USER> (time (a *cached*))</span><br><span class="line">Evaluation took:</span><br><span class="line"> 1.001 seconds of real time</span><br><span class="line"> 0.001527 seconds of total run time (0.000502 user, 0.001025 system)</span><br><span class="line"> 0.20% CPU</span><br><span class="line"> 2,210,843,642 processor cycles</span><br><span class="line"> 0 bytes consed</span><br><span class="line"> </span><br><span class="line">2</span><br><span class="line">CL-USER> (time (a *cached*))</span><br><span class="line">Evaluation took:</span><br><span class="line"> 0.000 seconds of real time</span><br><span class="line"> 0.000015 seconds of total run time (0.000014 user, 0.000001 system)</span><br><span class="line"> 100.00% CPU</span><br><span class="line"> 29,024 processor cycles</span><br><span class="line"> 0 bytes consed</span><br><span class="line"> </span><br><span class="line">2</span><br></pre></td></tr></table></figure><p>如果想要让透明缓存对函数<code>b</code>和<code>c</code>也起作用,则需要重新定义<code>b</code>和<code>c</code>各自的<code>no-applicable-method</code>方法。通过编写一个宏可以简化这部分重复的代码,示例如下</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defmacro</span> define-cached-method (<span class="name">generic-function</span>)</span><br><span class="line"> <span class="string">"为函数 GENERIC-FUNCTION 定义它的缓存版本的方法。"</span></span><br><span class="line"> (<span class="name">let</span> ((<span class="name">gf</span> (<span class="name">gensym</span>))</span><br><span class="line"> (<span class="name">args</span> (<span class="name">gensym</span>)))</span><br><span class="line"> `(defmethod no-applicable-method ((,gf (eql ,generic-function)) &rest ,args)</span><br><span class="line"> (let ((instance (first ,args)))</span><br><span class="line"> (if (typep instance 'Cached)</span><br><span class="line"> (let ((slow (cached-slow instance))</span><br><span class="line"> (key ,args))</span><br><span class="line"> (multiple-value-bind (v foundp)</span><br><span class="line"> (gethash key (cached-cache instance))</span><br><span class="line"> (if foundp</span><br><span class="line"> v</span><br><span class="line"> (let ((v (apply ,gf slow (rest ,args))))</span><br><span class="line"> (setf (gethash key (cached-cache instance)) v)</span><br><span class="line"> v))))</span><br><span class="line"> (call-next-method))))))</span><br></pre></td></tr></table></figure><p>然后就可以直接用这个新的宏来为函数<code>a</code>、<code>b</code>、<code>c</code>定义相应的带缓存的方法了,示例代码如下</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">define-cached-method</span> #'a)</span><br><span class="line">(<span class="name">define-cached-method</span> #'b)</span><br><span class="line">(<span class="name">define-cached-method</span> #'c)</span><br></pre></td></tr></table></figure><p>用函数<code>b</code>演示一下,效果如下</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">CL-USER> (time (b *cached*))</span><br><span class="line"></span><br><span class="line">Evaluation took:</span><br><span class="line"> 1.003 seconds of real time</span><br><span class="line"> 0.002518 seconds of total run time (0.001242 user, 0.001276 system)</span><br><span class="line"> 0.30% CPU</span><br><span class="line"> 2,216,371,640 processor cycles</span><br><span class="line"> 334,064 bytes consed</span><br><span class="line"> </span><br><span class="line">23</span><br><span class="line">CL-USER> (time (b *cached*))</span><br><span class="line">Evaluation took:</span><br><span class="line"> 0.000 seconds of real time</span><br><span class="line"> 0.000064 seconds of total run time (0.000063 user, 0.000001 system)</span><br><span class="line"> 100.00% CPU</span><br><span class="line"> 135,008 processor cycles</span><br><span class="line"> 0 bytes consed</span><br><span class="line"> </span><br><span class="line">23</span><br></pre></td></tr></table></figure><p>全文完。</p>]]></content>
<summary type="html"></summary>
<category term="common lisp" scheme="https://liutos.github.io/tags/common-lisp/"/>
<category term="lisp" scheme="https://liutos.github.io/tags/lisp/"/>
<category term="编程语言" scheme="https://liutos.github.io/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
<category term="元编程" scheme="https://liutos.github.io/tags/%E5%85%83%E7%BC%96%E7%A8%8B/"/>
</entry>
<entry>
<title>在CommonLisp中支持JSON语法</title>
<link href="https://liutos.github.io/2023/05/23/%E5%9C%A8CommonLisp%E4%B8%AD%E6%94%AF%E6%8C%81JSON%E8%AF%AD%E6%B3%95/"/>
<id>https://liutos.github.io/2023/05/23/%E5%9C%A8CommonLisp%E4%B8%AD%E6%94%AF%E6%8C%81JSON%E8%AF%AD%E6%B3%95/</id>
<published>2023-05-23T15:47:22.000Z</published>
<updated>2023-05-23T15:49:45.000Z</updated>
<content type="html"><![CDATA[<h1 id="在-Lisp-中使用-reader-macro-支持-JSON-语法"><a href="#在-Lisp-中使用-reader-macro-支持-JSON-语法" class="headerlink" title="在 Lisp 中使用 reader macro 支持 JSON 语法"></a>在 Lisp 中使用 reader macro 支持 JSON 语法</h1><h2 id="什么是-reader-macro?"><a href="#什么是-reader-macro?" class="headerlink" title="什么是 reader macro?"></a>什么是 reader macro?</h2><p>Reader macro 是 Common Lisp 提供的众多有趣特性之一,它让语言的使用者能够自定义词法分析的逻辑,使其在读取源代码时,如果遇到了特定的一两个字符,可以调用相应的函数来个性化处理。此处所说的“特定的一两个字符”,被称为 macro character,而“相应的函数”则被称为 reader macro function。举个例子,单引号<code>'</code>就是一个 macro character,可以用函数<code>get-macro-character</code>来获取它对应的 reader macro function。</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">CL-USER> (<span class="name">get-macro-character</span> #\')</span><br><span class="line">#<FUNCTION SB-IMPL::READ-QUOTE></span><br><span class="line">NIL</span><br></pre></td></tr></table></figure><p>借助单引号,可以简化一些代码的写法,例如表达一个符号<code>HELLO</code>本身可以写成这样。</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">CL-USER> 'hello</span><br><span class="line">HELLO</span><br></pre></td></tr></table></figure><p>而不是下面这种等价但更繁琐的形式。</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">CL-USER> (<span class="name">quote</span> hello)</span><br><span class="line">HELLO</span><br></pre></td></tr></table></figure><p>Common Lisp 中还定义了由两个字符构成的 reader macro,例如用于书写<code>simple-vector</code>字面量的<code>#(</code>。借助它,如果想要表达一个依次由数字 1、2、3 构成的<code>simple-vector</code>类型的对象,不需要显式地调用函数<code>vector</code>并传给它 1、2、3,而是可以写成<code>#(1 2 3)</code>。</p><h2 id="支持-JSON-语法后有什么效果?"><a href="#支持-JSON-语法后有什么效果?" class="headerlink" title="支持 JSON 语法后有什么效果?"></a>支持 JSON 语法后有什么效果?</h2><p>合法的 JSON 文本不一定是合法的 Common Lisp 源代码。例如,<code>[1, 2, 3]</code>在 JSON 标准看来是一个由数字 1、2、3 组成的数组,但在 Common Lisp 中,这段代码会触发 condition。(condition 就是 Common Lisp 中的“异常”、“出状况”了)</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">CL-USER> (<span class="name">let</span> ((<span class="name">eof-value</span> (<span class="name">gensym</span>)))</span><br><span class="line"> (<span class="name">with-input-from-string</span> (<span class="name">stream</span> <span class="string">"[1, 2, 3]"</span>)</span><br><span class="line"> (<span class="name">block</span> <span class="literal">nil</span></span><br><span class="line"> (<span class="name">loop</span></span><br><span class="line"> (<span class="name">let</span> ((<span class="name">expr</span> (<span class="name">read</span> stream <span class="literal">nil</span> eof-value)))</span><br><span class="line"> (<span class="name">when</span> (<span class="name">eq</span> expr eof-value)</span><br><span class="line"> (<span class="name">return</span>))</span><br><span class="line"></span><br><span class="line"> (<span class="name">print</span> expr))))))</span><br><span class="line"></span><br><span class="line">[<span class="number">1</span> <span class="comment">; Evaluation aborted on #<SB-INT:SIMPLE-READER-ERROR "Comma not inside a backquote." {1003AAD863}>.</span></span><br></pre></td></tr></table></figure><p>这是因为按照 Common Lisp 的<a href="http://www.lispworks.com/documentation/lw70/CLHS/Body/02_b.htm">读取算法</a>,左方括号<code>[</code>和数字 1 都是标准中所指的 constituent character,它们可以组成一个 token,并且最终被解析为一个符号类型的对象。而紧接着的字符是逗号<code>,</code>,<a href="http://www.lispworks.com/documentation/lw70/CLHS/Body/02_ad.htm">它是一个 terminating macro char</a>,按照<a href="http://www.lispworks.com/documentation/lw60/CLHS/Body/02_dg.htm">标准</a>,如果不是在一个反引号表达式中使用它将会是无效的,因此触发了 condition。</p><p>假如存在一个由两个字符<code>#J</code>定义的 reader macro、允许开发者使用 JSON 语法来描述紧接着的对象的话,那么就可以写出下面这样的代码。</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">CL-USER> (<span class="name">progn</span></span><br><span class="line"> (<span class="name">print</span> #jfalse)</span><br><span class="line"> (<span class="name">print</span> #jtrue)</span><br><span class="line"> (<span class="name">print</span> #j233.<span class="number">666</span>)</span><br><span class="line"> (<span class="name">print</span> #jnull)</span><br><span class="line"> (<span class="name">print</span> #j[<span class="number">1</span>, <span class="number">2</span>, [<span class="number">3</span>], [<span class="number">4</span>, <span class="number">5</span>]])</span><br><span class="line"> (<span class="name">print</span> #j{<span class="string">"a"</span>: [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>]})</span><br><span class="line"> (<span class="name">print</span> (<span class="name">gethash</span> <span class="string">"a"</span> #j{<span class="string">"a"</span>: [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>]})))</span><br><span class="line"></span><br><span class="line">YASON:FALSE </span><br><span class="line">YASON:TRUE </span><br><span class="line"><span class="number">233.666d0</span> </span><br><span class="line">:NULL </span><br><span class="line">#(<span class="number">1</span> <span class="number">2</span> #(<span class="number">3</span>) #(<span class="number">4</span> <span class="number">5</span>)) </span><br><span class="line">#<HASH-TABLE :TEST EQUAL :COUNT <span class="number">1</span> {<span class="number">1003889963</span>}> </span><br><span class="line">#(<span class="number">1</span> <span class="number">2</span> <span class="number">3</span>) </span><br><span class="line">#(<span class="number">1</span> <span class="number">2</span> <span class="number">3</span>)</span><br></pre></td></tr></table></figure><p>显然,用上述语法表示一个哈希表,要比下面这样的代码简单得多</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">CL-USER> (<span class="name">let</span> ((<span class="name">obj</span> (<span class="name">make-hash-table</span> <span class="symbol">:test</span> #'equal)))</span><br><span class="line"> (<span class="name">setf</span> (<span class="name">gethash</span> <span class="string">"a"</span> obj) #(<span class="number">1</span> <span class="number">2</span> <span class="number">3</span>))</span><br><span class="line"> obj)</span><br><span class="line"></span><br><span class="line">#<HASH-TABLE :TEST EQUAL :COUNT <span class="number">1</span> {<span class="number">1003</span>CB7643}></span><br></pre></td></tr></table></figure><h2 id="如何用-reader-macro-解析-JSON?"><a href="#如何用-reader-macro-解析-JSON?" class="headerlink" title="如何用 reader macro 解析 JSON?"></a>如何用 reader macro 解析 JSON?</h2><p>Common Lisp 并没有预置<code>#J</code>这个 reader macro,但这门语言允许使用者定义自己的 macro character,因此前面的示例代码是可以实现的。要自定义出<code>#J</code>这个读取器宏,需要使用函数<a href="http://www.lispworks.com/documentation/lw70/CLHS/Body/f_set__1.htm#set-dispatch-macro-character"><code>set-dispatch-macro-character</code></a>。它的前两个参数分别为构成 macro character 的前两个字符,即<code>#</code>和<code>J</code>——其中<code>J</code>即便是写成了小写,也会被转换为大写后再使用。第三个参数则是 Lisp 的词法解析器在遇到了<code>#J</code>时将会调用的参数。<code>set-dispatch-macro-character</code>会传给这个函数三个参数:</p><ol><li>用于读取源代码的字符输入流;</li><li>构成 macro character 的第二个字符(即<code>J</code>);</li><li>非必传的、夹在<code>#</code>和<code>J</code>之间的数字。</li></ol><p>百闻不如一见,一段能够实现上一个章节中的示例代码的<code>set-dispatch-macro-character</code>用法如下</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">set-dispatch-macro-character</span></span><br><span class="line"> #\#</span><br><span class="line"> #\j</span><br><span class="line"> (<span class="name">lambda</span> (<span class="name">stream</span> char p)</span><br><span class="line"> (<span class="name">declare</span> (<span class="name">ignorable</span> char p))</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">parsed</span> (<span class="name">yason</span><span class="symbol">:parse</span> stream</span><br><span class="line"> <span class="symbol">:json-arrays-as-vectors</span> <span class="literal">t</span></span><br><span class="line"> <span class="symbol">:json-booleans-as-symbols</span> <span class="literal">t</span></span><br><span class="line"> <span class="symbol">:json-nulls-as-keyword</span> <span class="literal">t</span>)))</span><br><span class="line"> (<span class="name">if</span> (<span class="name">or</span> (<span class="name">symbolp</span> parsed)</span><br><span class="line"> (<span class="name">consp</span> parsed))</span><br><span class="line"> (<span class="name">list</span> 'quote parsed)</span><br><span class="line"> parsed))))</span><br></pre></td></tr></table></figure><p>在<code>set-dispatch-macro-character</code>的回调函数中,我是用了开源的第三方库<code>yason</code>提供的函数<code>parse</code>,从输入流<code>stream</code>中按照 JSON 语法解析出一个值。函数<code>parse</code>的三个关键字参数的含义参见<a href="https://xh4.github.io/yason/#mapping">这里</a>,此处不再赘述。由于 reader macro 的结果会被用于构造源代码的表达式,因此如果函数<code>parse</code>返回了符号或者<code>cons</code>类型,为了避免被编译器求值,需要将它们“引用”起来,因此将它们放到第一元素为<code>quote</code>的列表中。其它情况下,直接返回<code>parse</code>的返回值即可,因此它们是“自求值”的,求值结果是它们自身。</p><h2 id="尾声"><a href="#尾声" class="headerlink" title="尾声"></a>尾声</h2><p>本文我借助了现成的库<code>yason</code>来解析 JSON 格式的字符串,如果你对如何从零开始实现这样的 reader macro 感兴趣的话,可以参考<a href="https://gist.github.com/chaitanyagupta/9324402">这篇文章</a>。</p><p>全文完。</p>]]></content>
<summary type="html"></summary>
<category term="common lisp" scheme="https://liutos.github.io/tags/common-lisp/"/>
<category term="lisp" scheme="https://liutos.github.io/tags/lisp/"/>
<category term="编程语言" scheme="https://liutos.github.io/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
</entry>
<entry>
<title>使用 call/cc 实现计数循环</title>
<link href="https://liutos.github.io/2023/05/07/impl-dotimes-by-callcc/"/>
<id>https://liutos.github.io/2023/05/07/impl-dotimes-by-callcc/</id>
<published>2023-05-07T08:52:26.000Z</published>
<updated>2023-05-07T09:00:18.000Z</updated>
<content type="html"><![CDATA[<h1 id="使用-call-cc-实现计数循环"><a href="#使用-call-cc-实现计数循环" class="headerlink" title="使用 call/cc 实现计数循环"></a>使用 call/cc 实现计数循环</h1><h2 id="什么是计数循环"><a href="#什么是计数循环" class="headerlink" title="什么是计数循环"></a>什么是计数循环</h2><p>计数循环就是从一个数字$i$开始一直遍历到另一个数字$j$为止的循环过程。例如,下面的 Python 代码就会遍历从 0 到 9 这 10 个整数并逐个打印它们</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">10</span>):</span><br><span class="line"> <span class="built_in">print</span>(i)</span><br></pre></td></tr></table></figure><p>如果是在 C 语言中实现同样的功能,代码会更显著一些</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><stdio.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">int</span> argc, <span class="type">char</span> *argv[])</span></span><br><span class="line">{</span><br><span class="line"> <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i < <span class="number">10</span>; i++) {</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\n"</span>, i);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在 C 语言的例子中,显式地指定了计数器变量<code>i</code>从 0 开始并且在等于 10 的时候结束循环,比之 Python 版本更有循环的味道。</p><h2 id="拆开循环计数的语法糖"><a href="#拆开循环计数的语法糖" class="headerlink" title="拆开循环计数的语法糖"></a>拆开循环计数的语法糖</h2><p>使用 C 语言的<code>while</code>语句同样可以实现计数循环,示例代码如下</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><stdio.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">int</span> argc, <span class="type">char</span> *argv[])</span></span><br><span class="line">{</span><br><span class="line"> <span class="type">int</span> i = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">while</span> (i < <span class="number">10</span>) {</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\n"</span>, i);</span><br><span class="line"> i++;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如果将<code>while</code>也视为<code>if</code>和<code>goto</code>的语法糖的话,可以进一步将计数循环写成更原始的形式</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><stdio.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">int</span> argc, <span class="type">char</span> *argv[])</span></span><br><span class="line">{</span><br><span class="line"> <span class="type">int</span> i = <span class="number">0</span>;</span><br><span class="line">label0:</span><br><span class="line"> <span class="keyword">if</span> (i >= <span class="number">10</span>) {</span><br><span class="line"> <span class="keyword">goto</span> label1;</span><br><span class="line"> }</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\n"</span>, i);</span><br><span class="line"> i++;</span><br><span class="line"> <span class="keyword">goto</span> label0;</span><br><span class="line">label1:</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="Common-Lisp-中的-go-与续延"><a href="#Common-Lisp-中的-go-与续延" class="headerlink" title="Common Lisp 中的 go 与续延"></a>Common Lisp 中的 go 与续延</h2><p>在 Common Lisp 中也有与 C 语言的<code>goto</code>特性相近的 special form,那就是<a href="http://clhs.lisp.se/Body/s_tagbod.htm"><code>tagbody</code></a>和<a href="http://www.lispworks.com/documentation/lw70/CLHS/Body/s_go.htm"><code>go</code></a>。使用它们可以将 C 代码直白地翻译为对应的 Common Lisp 版本</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">let</span> ((<span class="name">i</span> <span class="number">0</span>))</span><br><span class="line"> (<span class="name">tagbody</span></span><br><span class="line"> label0</span><br><span class="line"> (<span class="name">when</span> (<span class="name">>=</span> i <span class="number">10</span>)</span><br><span class="line"> (<span class="name">go</span> label1))</span><br><span class="line"></span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"~D~%"</span> i)</span><br><span class="line"> (<span class="name">incf</span> i)</span><br><span class="line"> (<span class="name">go</span> label0)</span><br><span class="line"> label1))</span><br></pre></td></tr></table></figure><p>聪明的你一定已经发现了,此处的第二个符号<code>label1</code>其实是丝毫不必要的,只要写成下面的形式即可</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">let</span> ((<span class="name">i</span> <span class="number">0</span>))</span><br><span class="line"> (<span class="name">tagbody</span></span><br><span class="line"> label0</span><br><span class="line"> (<span class="name">when</span> (<span class="name"><</span> i <span class="number">10</span>)</span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"~D~%"</span> i)</span><br><span class="line"> (<span class="name">incf</span> i)</span><br><span class="line"> (<span class="name">go</span> label0))))</span><br></pre></td></tr></table></figure><p>这个形式不仅仅是更简单了,而且它暴露出了一个事实:<code>label0</code>所表示的,其实就是在将变量<code>i</code>绑定为 0<strong>之后</strong>要执行的代码的位置。换句话说,它标识了一个续延(continuation)。</p><h2 id="用-call-cc-重新实现计数循环"><a href="#用-call-cc-重新实现计数循环" class="headerlink" title="用 call/cc 重新实现计数循环"></a>用 call/cc 重新实现计数循环</h2><p>如果你用的语言中支持 first-class 的续延,那么便可以用来实现计数循环,例如<a href="https://github.com/Liutos/LiutCL">233-lisp</a>。在 233-lisp 中,提供了特殊操作符<code>call/cc</code>来捕捉当前续延对象,这个名字借鉴自 Scheme。借助这个操作符,即便没有<code>tagbody</code>和<code>go</code>,也可以实现计数循环。</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20230507/%E7%94%A8callcc%E6%A8%A1%E6%8B%9F%E8%AE%A1%E6%95%B0%E5%BE%AA%E7%8E%AF.jpg" alt="用callcc模拟计数循环"></p><p>在上面的代码中,<code>call/cc</code>捕捉到的续延就是“赋值给局部变量<code>i</code>”。在将这个续延<code>k</code>保存到变量<code>next</code>之后,用 0 初始化变量<code>i</code>。之后只要<code>i</code>还小于 10,就将它打印到标准输出,并启动保存在了变量<code>next</code>中的续延,回到给变量<code>i</code>赋值的地方。此时传递给续延的参数为<code>(+ i 1)</code>,就实现了变量<code>i</code>的自增操作。当<code>(< i 10)</code>不再成立时,也就不会启动续延“回到过去”了,至此,进程结束。</p><p>在 233-lisp 中,将<code>dotimes</code>作为一个内置的宏用<code>call/cc</code>实现了一遍,参见<a href="https://github.com/Liutos/LiutCL/blob/6342693cb682b6d2f47067c3d3c9c6ce46eb891c/src/interpreter.lisp#L568">这里</a>,其代码如下</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defun</span> expand-dotimes-to-call/cc (<span class="name">expr</span>)</span><br><span class="line"> <span class="string">"将 DOTIMES 语句 EXPR 编译为等价的 CALL/CC 语句。"</span></span><br><span class="line"> (<span class="name">assert</span> (<span class="name">eq</span> (<span class="name">first</span> expr) 'dotimes))</span><br><span class="line"> (<span class="name">destructuring-bind</span> ((<span class="name">var</span> count-form) <span class="symbol">&rest</span> statements)</span><br><span class="line"> (<span class="name">rest</span> expr)</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">a</span> (<span class="name">gensym</span>))</span><br><span class="line"> (<span class="name">count-form-result</span> (<span class="name">gensym</span>))</span><br><span class="line"> (<span class="name">next</span> (<span class="name">gensym</span>)))</span><br><span class="line"> `(let ((,count-form-result ,count-form)) ; 由于目前 LET 只支持一个绑定,因此这里要写多个 LET。</span><br><span class="line"> (let ((,next <span class="number">0</span>)) ; 由于 <span class="number">233</span>-lisp 中尚未支持 NIL,因此这里填个 <span class="number">0</span>。</span><br><span class="line"> (let ((,var (call/cc (k)</span><br><span class="line"> (progn</span><br><span class="line"> (setf ,next k)</span><br><span class="line"> <span class="number">0</span>)))) ; 计数循环从 <span class="number">0</span> 开始。</span><br><span class="line"> (if (< ,var ,count-form-result)</span><br><span class="line"> (progn</span><br><span class="line"> ,@statements</span><br><span class="line"> (,next (+ ,var <span class="number">1</span>)))</span><br><span class="line"> <span class="number">0</span>))))))) <span class="comment">; 由于目前没有 NIL,因此返回一个数字 0 来代替。</span></span><br></pre></td></tr></table></figure><p>变量<code>count-form-result</code>和<code>next</code>分别表示在宏展开后的代码中的计数上限和被捕捉的续延。之所以让它们以<code>(gensym)</code>的方式来命名,是为了避免多次求值<code>count-form</code>表达式,以及避免存储续延的变量名恰好出乎意料地与<code>statements</code>中的变量名冲突了,这也算是编写 Common Lisp 的宏时的最佳实践了。</p><h2 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h2><p>直接用<code>call/cc</code>来一个个实现 Common Lisp 中的各种控制流还是太繁琐了,更好的方案是用<code>call/cc</code>先实现<code>tagbody</code>和<code>go</code>,然后再用后两者继续实现<code>do</code>,最后用<code>do</code>分别实现<code>dolist</code>和<code>dotimes</code>。当然了,这些都是后话了。</p>]]></content>
<summary type="html"></summary>
<category term="lisp" scheme="https://liutos.github.io/tags/lisp/"/>
<category term="编程语言" scheme="https://liutos.github.io/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
<category term="宏" scheme="https://liutos.github.io/tags/%E5%AE%8F/"/>
</entry>
<entry>
<title>如何在CommonLisp中解析命令行参数</title>
<link href="https://liutos.github.io/2022/08/21/%E5%A6%82%E4%BD%95%E5%9C%A8CommonLisp%E4%B8%AD%E8%A7%A3%E6%9E%90%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%8F%82%E6%95%B0/"/>
<id>https://liutos.github.io/2022/08/21/%E5%A6%82%E4%BD%95%E5%9C%A8CommonLisp%E4%B8%AD%E8%A7%A3%E6%9E%90%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%8F%82%E6%95%B0/</id>
<published>2022-08-21T07:27:55.000Z</published>
<updated>2022-08-21T07:39:50.000Z</updated>
<content type="html"><![CDATA[<h1 id="clingon"><a href="#clingon" class="headerlink" title="clingon"></a>clingon</h1><p>clingon 是一个 Common Lisp 的命令行选项的解析器,它可以轻松地解析具有复杂格式的命令行选项。例如,下面的代码可以打印给定次数的打招呼信息</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/sh</span></span><br><span class="line">#|-*- mode:lisp -*-|#</span><br><span class="line">#|</span><br><span class="line">exec ros -Q -- $<span class="number">0</span> <span class="string">"$@"</span></span><br><span class="line">|#</span><br><span class="line">(<span class="name">progn</span> <span class="comment">;;init forms</span></span><br><span class="line"> (<span class="name">ros</span><span class="symbol">:ensure-asdf</span>)</span><br><span class="line"> #+quicklisp(<span class="name">ql</span><span class="symbol">:quickload</span> '(clingon) <span class="symbol">:silent</span> <span class="literal">t</span>)</span><br><span class="line"> )</span><br><span class="line"></span><br><span class="line">(<span class="name">defpackage</span> <span class="symbol">:ros</span>.script.hello.<span class="number">3868869124</span></span><br><span class="line"> (<span class="symbol">:use</span> <span class="symbol">:cl</span></span><br><span class="line"> <span class="symbol">:clingon</span>))</span><br><span class="line">(<span class="name">in-package</span> <span class="symbol">:ros</span>.script.hello.<span class="number">3868869124</span>)</span><br><span class="line"></span><br><span class="line">(<span class="name">defun</span> top-level/handler (<span class="name">cmd</span>)</span><br><span class="line"> (<span class="name">check-type</span> cmd clingon<span class="symbol">:command</span>)</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">count</span> (<span class="name">clingon</span><span class="symbol">:getopt</span> cmd <span class="symbol">:count</span>))</span><br><span class="line"> (<span class="name">name</span> (<span class="name">first</span> (<span class="name">clingon</span><span class="symbol">:command-arguments</span> cmd))))</span><br><span class="line"> (<span class="name">dotimes</span> (<span class="name">_</span> count)</span><br><span class="line"> (<span class="name">declare</span> (<span class="name">ignorable</span> _))</span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"Hello ~A!~%"</span> name))))</span><br><span class="line"></span><br><span class="line">(<span class="name">defun</span> main (<span class="name">&rest</span> argv)</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">app</span> (<span class="name">clingon</span><span class="symbol">:make-command</span></span><br><span class="line"> <span class="symbol">:handler</span> #'top-level/handler</span><br><span class="line"> <span class="symbol">:name</span> <span class="string">"hello"</span></span><br><span class="line"> <span class="symbol">:options</span> (<span class="name">list</span></span><br><span class="line"> (<span class="name">clingon</span><span class="symbol">:make-option</span></span><br><span class="line"> <span class="symbol">:integer</span></span><br><span class="line"> <span class="symbol">:description</span> <span class="string">"number of greetings"</span></span><br><span class="line"> <span class="symbol">:initial-value</span> <span class="number">1</span></span><br><span class="line"> <span class="symbol">:key</span> <span class="symbol">:count</span></span><br><span class="line"> <span class="symbol">:long-name</span> <span class="string">"count"</span>)))))</span><br><span class="line"> (<span class="name">clingon</span><span class="symbol">:run</span> app argv)))</span><br><span class="line"><span class="comment">;;; vim: set ft=lisp lisp:</span></span><br></pre></td></tr></table></figure><p>稍微做一些解释。首先执行命令<code>ros init hello</code>生成上面的代码的雏形——加载依赖、包定义,以及空的函数<code>main</code>。为了加载 clingon,将其作为函数<code>ql:quickload</code>的参数。然后分别定义一个<code>command</code>、<code>handler</code>,以及<code>option</code>。</p><p>在 clingon 中,类<code>clingon:command</code>的实例对象表示一个可以在 shell 中被触发的命令,它们由函数<code>clingon:make-command</code>创建。每一个命令起码要有三个要素:</p><ol><li><code>:handler</code>,负责使用命令行选项、实现业务逻辑的函数;</li><li><code>:name</code>,命令的名字,一般会被展示在命令的用法说明中;</li><li><code>:options</code>,该命令所接受的选项。</li></ol><p>此处的<code>:handler</code>就是函数<code>top-level/handler</code>,它会被函数<code>clingon:run</code>调用(依赖注入的味道),并将一个合适的<code>clingon:command</code>对象传入。<code>:options</code>目前只承载了一个选项的定义,即</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">clingon</span><span class="symbol">:make-option</span></span><br><span class="line"> <span class="symbol">:integer</span></span><br><span class="line"> <span class="symbol">:description</span> <span class="string">"number of greetings"</span></span><br><span class="line"> <span class="symbol">:initial-value</span> <span class="number">1</span></span><br><span class="line"> <span class="symbol">:key</span> <span class="symbol">:count</span></span><br><span class="line"> <span class="symbol">:long-name</span> <span class="string">"count"</span>)</span><br></pre></td></tr></table></figure><p>它定义了一个值为整数的选项,在命令行中通过<code>--count</code>指定。如果没有传入该选项,那么在使用函数<code>clingon:getopt</code>取值时,会获得默认值 1。如果要从一个命令对象中取出这个选项的值,需要以它的<code>:key</code>参数的值作为参数来调用函数<code>clingon:getopt</code>,正如上面的函数<code>top-level/handler</code>所示。</p><h2 id="子命令"><a href="#子命令" class="headerlink" title="子命令"></a>子命令</h2><p>clingon 也可以实现诸如<code>git add</code>、<code>git branch</code>这样的子命令特性。像<code>add</code>、<code>branch</code>这样的子命令,对于 clingon 而言仍然是类<code>clingon:command</code>的实例对象,只不过它们不会传递给函数<code>clingon:run</code>调度,而是传递给函数<code>clingon:make-command</code>的参数<code>:sub-command</code>,如下列代码所示</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defun</span> top-level/handler (<span class="name">cmd</span>)</span><br><span class="line"> (<span class="name">declare</span> (<span class="name">ignorable</span> cmd)))</span><br><span class="line"></span><br><span class="line">(<span class="name">defun</span> main (<span class="name">&rest</span> argv)</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">app</span> (<span class="name">clingon</span><span class="symbol">:make-command</span></span><br><span class="line"> <span class="symbol">:handler</span> #'top-level/handler</span><br><span class="line"> <span class="symbol">:name</span> <span class="string">"cli"</span></span><br><span class="line"> <span class="symbol">:sub-commands</span> (<span class="name">list</span></span><br><span class="line"> (<span class="name">clingon</span><span class="symbol">:make-command</span></span><br><span class="line"> <span class="symbol">:handler</span> #'(lambda (cmd)</span><br><span class="line"> (declare (ignorable cmd))</span><br><span class="line"> (format <span class="literal">t</span> <span class="string">"Dropped the database~%"</span>))</span><br><span class="line"> <span class="symbol">:name</span> <span class="string">"dropdb"</span>)</span><br><span class="line"> (<span class="name">clingon</span><span class="symbol">:make-command</span></span><br><span class="line"> <span class="symbol">:handler</span> #'(lambda (cmd)</span><br><span class="line"> (declare (ignorable cmd))</span><br><span class="line"> (format <span class="literal">t</span> <span class="string">"Initialized the database~%"</span>))</span><br><span class="line"> <span class="symbol">:name</span> <span class="string">"initdb"</span>)))))</span><br><span class="line"> (<span class="name">clingon</span><span class="symbol">:run</span> app argv)))</span><br></pre></td></tr></table></figure><h2 id="选项与参数"><a href="#选项与参数" class="headerlink" title="选项与参数"></a>选项与参数</h2><p>在 clingon 中通过命令行传递给进程的信息分为选项和参数两种形态,选项是通过名字来引用,而参数则通过它们的下标来引用。例如在第一个例子中,就定义了一个名为<code>--count</code>的选项,它在解析结果中被赋予了<code>:count</code>这个关键字,可以通过函数<code>clingon:getopt</code>来引用它的值;与之相反,变量<code>name</code>是从命令行中解析了选项后、剩余的参数中的<strong>第一个</strong>,它是以位置来标识的。clingon 通过函数<code>clingon:make-option</code>来定义选项,它提供了丰富的控制能力。</p><h2 id="选项名称"><a href="#选项名称" class="headerlink" title="选项名称"></a>选项名称</h2><p>选项有好几种名字,一种叫做<code>:key</code>,是在程序内部使用的名字,用作函数<code>clingon:getopt</code>的参数之一;一种叫做<code>:long-name</code>,一般为多于一个字符的字符串,如<code>"count"</code>,在命令行该名称需要带上两个连字符的前缀来使用,如<code>--count 3</code>;最后一种叫做<code>:short-name</code>,为一个单独的字符,如<code>#\v</code>,在命令行中带上一个连字符前缀来使用,如<code>-v</code>。</p><h2 id="必要性与默认值"><a href="#必要性与默认值" class="headerlink" title="必要性与默认值"></a>必要性与默认值</h2><p>通过传入参数<code>:required t</code>给函数<code>clingon:make-option</code>,可以要求一个选项为必传的。例如下面的命令的选项<code>--n</code>就是必传的</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defun</span> top-level/handler (<span class="name">cmd</span>)</span><br><span class="line"> (<span class="name">dotimes</span> (<span class="name">i</span> (<span class="name">clingon</span><span class="symbol">:getopt</span> cmd <span class="symbol">:n</span>))</span><br><span class="line"> (<span class="name">declare</span> (<span class="name">ignorable</span> i))</span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"."</span>)))</span><br><span class="line"></span><br><span class="line">(<span class="name">defun</span> main (<span class="name">&rest</span> argv)</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">app</span> (<span class="name">clingon</span><span class="symbol">:make-command</span></span><br><span class="line"> <span class="symbol">:handler</span> #'top-level/handler</span><br><span class="line"> <span class="symbol">:name</span> <span class="string">"dots"</span></span><br><span class="line"> <span class="symbol">:options</span> (<span class="name">list</span></span><br><span class="line"> (<span class="name">clingon</span><span class="symbol">:make-option</span></span><br><span class="line"> <span class="symbol">:integer</span></span><br><span class="line"> <span class="symbol">:description</span> <span class="string">"打印的英文句号的数量"</span></span><br><span class="line"> <span class="symbol">:key</span> <span class="symbol">:n</span></span><br><span class="line"> <span class="symbol">:long-name</span> <span class="string">"n"</span></span><br><span class="line"> <span class="symbol">:required</span> <span class="literal">t</span>)))))</span><br><span class="line"> (<span class="name">clingon</span><span class="symbol">:run</span> app argv)))</span><br></pre></td></tr></table></figure><p>如果不希望在一些最简单的情况下也要繁琐地编写<code>--n 1</code>这样的命令行参数,可以用<code>:initial-value 1</code>来指定。除此之外,也可以让选项默认读取指定的环境变量中的值,使用<code>:env-vars</code>指定环境变量名即可</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defun</span> top-level/handler (<span class="name">cmd</span>)</span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"Hello ~A~%"</span> (<span class="name">clingon</span><span class="symbol">:getopt</span> cmd <span class="symbol">:username</span>)))</span><br><span class="line"></span><br><span class="line">(<span class="name">defun</span> main (<span class="name">&rest</span> argv)</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">app</span> (<span class="name">clingon</span><span class="symbol">:make-command</span></span><br><span class="line"> <span class="symbol">:handler</span> #'top-level/handler</span><br><span class="line"> <span class="symbol">:name</span> <span class="string">"greet"</span></span><br><span class="line"> <span class="symbol">:options</span> (<span class="name">list</span></span><br><span class="line"> (<span class="name">clingon</span><span class="symbol">:make-option</span></span><br><span class="line"> <span class="symbol">:string</span></span><br><span class="line"> <span class="symbol">:description</span> <span class="string">"用户名"</span></span><br><span class="line"> <span class="symbol">:env-vars</span> '(<span class="string">"GREETER_USERNAME"</span>)</span><br><span class="line"> <span class="symbol">:key</span> <span class="symbol">:username</span></span><br><span class="line"> <span class="symbol">:long-name</span> <span class="string">"username"</span>)))))</span><br><span class="line"> (<span class="name">clingon</span><span class="symbol">:run</span> app argv)))</span><br></pre></td></tr></table></figure><h2 id="可多次使用的选项"><a href="#可多次使用的选项" class="headerlink" title="可多次使用的选项"></a>可多次使用的选项</h2><p>像<code>curl</code>中的选项<code>-H</code>就是可以多次使用的,每指定一次就可以在请求中添加一个 HTTP 头部,如下图所示</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20220821/curl%E5%A4%9A%E6%AC%A1%E4%BC%A0%E5%85%A5-H%E7%9A%84%E6%95%88%E6%9E%9C.jpg" alt="curl多次传入-H的效果"></p><p>在 clingon 中可以通过往函数<code>clingon:make-option</code>传入<code>:list</code>来实现。当用<code>clingon:getopt</code>取出类型为<code>:list</code>的选项的值时,得到的是一个列表,其中依次存放着输入的值的字符串。</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defun</span> top-level/handler (<span class="name">cmd</span>)</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">messages</span> (<span class="name">clingon</span><span class="symbol">:getopt</span> cmd <span class="symbol">:message</span>)))</span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"~{~A~^~%~}"</span> messages)))</span><br><span class="line"></span><br><span class="line">(<span class="name">defun</span> main (<span class="name">&rest</span> argv)</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">app</span> (<span class="name">clingon</span><span class="symbol">:make-command</span></span><br><span class="line"> <span class="symbol">:handler</span> #'top-level/handler</span><br><span class="line"> <span class="symbol">:name</span> <span class="string">"commit"</span></span><br><span class="line"> <span class="symbol">:options</span> (<span class="name">list</span></span><br><span class="line"> (<span class="name">clingon</span><span class="symbol">:make-option</span></span><br><span class="line"> <span class="symbol">:list</span></span><br><span class="line"> <span class="symbol">:description</span> <span class="string">"提交的消息"</span></span><br><span class="line"> <span class="symbol">:key</span> <span class="symbol">:message</span></span><br><span class="line"> <span class="symbol">:long-name</span> <span class="string">"message"</span></span><br><span class="line"> <span class="symbol">:short-name</span> #\m)))))</span><br><span class="line"> (<span class="name">clingon</span><span class="symbol">:run</span> app argv)))</span><br></pre></td></tr></table></figure><p>另一种情况是尽管没有值,但仍然多次使用同一个选项。例如命令<code>ssh</code>的选项<code>-v</code>,使用的次数越多(最多为 3 次),则<code>ssh</code>打印的调试信息也就越详细。这种类型的选项在 clingon 中称为<code>:counter</code>。</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defun</span> top-level/handler (<span class="name">cmd</span>)</span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"Verbosity: ~D~%"</span> (<span class="name">clingon</span><span class="symbol">:getopt</span> cmd <span class="symbol">:verbose</span>)))</span><br><span class="line"></span><br><span class="line">(<span class="name">defun</span> main (<span class="name">&rest</span> argv)</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">app</span> (<span class="name">clingon</span><span class="symbol">:make-command</span></span><br><span class="line"> <span class="symbol">:handler</span> #'top-level/handler</span><br><span class="line"> <span class="symbol">:name</span> <span class="string">"log"</span></span><br><span class="line"> <span class="symbol">:options</span> (<span class="name">list</span></span><br><span class="line"> (<span class="name">clingon</span><span class="symbol">:make-option</span></span><br><span class="line"> <span class="symbol">:counter</span></span><br><span class="line"> <span class="symbol">:description</span> <span class="string">"啰嗦程度"</span></span><br><span class="line"> <span class="symbol">:key</span> <span class="symbol">:verbose</span></span><br><span class="line"> <span class="symbol">:long-name</span> <span class="string">"verbose"</span></span><br><span class="line"> <span class="symbol">:short-name</span> #\v)))))</span><br><span class="line"> (<span class="name">clingon</span><span class="symbol">:run</span> app argv)))</span><br></pre></td></tr></table></figure><h2 id="信号选项"><a href="#信号选项" class="headerlink" title="信号选项"></a>信号选项</h2><p>有一些选项只需要区分【有】和【没有】两种情况就可以了,而不需要在意这个选项的值——或者这类选项本身就不允许有值,例如<code>docker run</code>命令的选项<code>-d</code>和<code>--detach</code>。这种选项的类型为<code>:boolean/true</code>,如果指定了这个选项,那么取出来的值始终为<code>t</code>。与之相反,类型<code>:boolean/false</code>取出来的值始终为<code>nil</code>。</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defun</span> top-level/handler (<span class="name">cmd</span>)</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">rv</span> (<span class="name">software-type</span>)))</span><br><span class="line"> (<span class="name">when</span> (<span class="name">clingon</span><span class="symbol">:getopt</span> cmd <span class="symbol">:shout</span>)</span><br><span class="line"> (<span class="name">setf</span> rv (<span class="name">concatenate</span> 'string (<span class="name">string-upcase</span> rv) <span class="string">"!!!!111"</span>)))</span><br><span class="line"></span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"~A~%"</span> rv)))</span><br><span class="line"></span><br><span class="line">(<span class="name">defun</span> main (<span class="name">&rest</span> argv)</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">app</span> (<span class="name">clingon</span><span class="symbol">:make-command</span></span><br><span class="line"> <span class="symbol">:handler</span> #'top-level/handler</span><br><span class="line"> <span class="symbol">:name</span> <span class="string">"info"</span></span><br><span class="line"> <span class="symbol">:options</span> (<span class="name">list</span></span><br><span class="line"> (<span class="name">clingon</span><span class="symbol">:make-option</span></span><br><span class="line"> <span class="symbol">:boolean/true</span></span><br><span class="line"> <span class="symbol">:description</span> <span class="string">"大喊"</span></span><br><span class="line"> <span class="symbol">:key</span> <span class="symbol">:shout</span></span><br><span class="line"> <span class="symbol">:long-name</span> <span class="string">"shout"</span>)))))</span><br><span class="line"> (<span class="name">clingon</span><span class="symbol">:run</span> app argv)))</span><br></pre></td></tr></table></figure><h2 id="选择型选项"><a href="#选择型选项" class="headerlink" title="选择型选项"></a>选择型选项</h2><p>如果一个选项尽管接受的是字符串,但并非所有输入都是有意义的,例如命令<code>dot</code>的选项<code>-T</code>。从<code>dot</code>的 man 文档可以看到,它所支持的图片类型是有限的,如<code>ps</code>、<code>pdf</code>、<code>png</code>等。比起声明一个<code>:string</code>类型的选项,让 clingon 代劳输入值的有效性检查来得更轻松,这里可以使用<code>:choice</code>类型</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defun</span> top-level/handler (<span class="name">cmd</span>)</span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"~A~%"</span> (<span class="name">clingon</span><span class="symbol">:getopt</span> cmd <span class="symbol">:hash-type</span>)))</span><br><span class="line"></span><br><span class="line">(<span class="name">defun</span> main (<span class="name">&rest</span> argv)</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">app</span> (<span class="name">clingon</span><span class="symbol">:make-command</span></span><br><span class="line"> <span class="symbol">:handler</span> #'top-level/handler</span><br><span class="line"> <span class="symbol">:name</span> <span class="string">"digest"</span></span><br><span class="line"> <span class="symbol">:options</span> (<span class="name">list</span></span><br><span class="line"> (<span class="name">clingon</span><span class="symbol">:make-option</span></span><br><span class="line"> <span class="symbol">:choice</span></span><br><span class="line"> <span class="symbol">:description</span> <span class="string">"哈希类型"</span></span><br><span class="line"> <span class="symbol">:items</span> '(<span class="string">"MD5"</span> <span class="string">"SHA1"</span>)</span><br><span class="line"> <span class="symbol">:key</span> <span class="symbol">:hash-type</span></span><br><span class="line"> <span class="symbol">:long-name</span> <span class="string">"hash-type"</span>)))))</span><br><span class="line"> (<span class="name">clingon</span><span class="symbol">:run</span> app argv)))</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html"></summary>
<category term="common lisp" scheme="https://liutos.github.io/tags/common-lisp/"/>
<category term="lisp" scheme="https://liutos.github.io/tags/lisp/"/>
<category term="命令行" scheme="https://liutos.github.io/tags/%E5%91%BD%E4%BB%A4%E8%A1%8C/"/>
<category term="教程" scheme="https://liutos.github.io/tags/%E6%95%99%E7%A8%8B/"/>
</entry>
<entry>
<title>自己动手打印整数</title>
<link href="https://liutos.github.io/2022/05/02/%E8%87%AA%E5%B7%B1%E5%8A%A8%E6%89%8B%E6%89%93%E5%8D%B0%E6%95%B4%E6%95%B0/"/>
<id>https://liutos.github.io/2022/05/02/%E8%87%AA%E5%B7%B1%E5%8A%A8%E6%89%8B%E6%89%93%E5%8D%B0%E6%95%B4%E6%95%B0/</id>
<published>2022-05-02T03:14:03.000Z</published>
<updated>2022-05-02T03:21:24.000Z</updated>
<content type="html"><![CDATA[<p>在 Common Lisp 中,打印整数一般用函数<code>format</code>。例如,上面的代码会往标准输出中打印出233这个数字:</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~D"</span> <span class="number">233</span>)</span><br></pre></td></tr></table></figure><p>除此之外,<code>format</code>还可以控制打印内容的宽度、填充字符、是否打印正负号等方面。例如,要控制打印的内容至少占据6列的话,可以用如下代码</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~6D"</span> <span class="number">233</span>)</span><br></pre></td></tr></table></figure><p>如果不使用字符串形式的 DSL,而是以关键字参数的方式来实现一个能够达到同样效果的函数<code>format-decimal</code>,代码可能如下:</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defun</span> format-decimal (<span class="name">n</span></span><br><span class="line"> <span class="symbol">&key</span></span><br><span class="line"> mincol)</span><br><span class="line"> <span class="string">"打印整数 N 到标准输出。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">MINCOL 如果不为 NIL,则表示所打印的内容至少要占据的列数。"</span></span><br><span class="line"> <span class="comment">;; 通过取余的方式得到 N 的每一位并逐个入栈,之后出栈的顺序就是从左到右打印的顺序了。</span></span><br><span class="line"> (<span class="name">let</span> ((<span class="name">digits</span> '()))</span><br><span class="line"> (<span class="name">cond</span> ((<span class="name">zerop</span> n)</span><br><span class="line"> (<span class="name">push</span> <span class="number">0</span> digits))</span><br><span class="line"> (<span class="name">t</span></span><br><span class="line"> (<span class="name">do</span> ((<span class="name">n</span> n (<span class="name">truncate</span> n <span class="number">10</span>)))</span><br><span class="line"> ((<span class="name">zerop</span> n))</span><br><span class="line"> (<span class="name">push</span> (<span class="name">rem</span> n <span class="number">10</span>) digits))))</span><br><span class="line"> <span class="comment">;; 打印出填充用的空格。</span></span><br><span class="line"> (<span class="name">when</span> (<span class="name">and</span> (<span class="name">integerp</span> mincol) (<span class="name">></span> mincol (<span class="name">length</span> digits)))</span><br><span class="line"> (<span class="name">dotimes</span> (<span class="name">i</span> (<span class="name">-</span> mincol (<span class="name">length</span> digits)))</span><br><span class="line"> (<span class="name">declare</span> (<span class="name">ignorable</span> i))</span><br><span class="line"> (<span class="name">princ</span> #\Space)))</span><br><span class="line"></span><br><span class="line"> (<span class="name">dolist</span> (<span class="name">digit</span> digits)</span><br><span class="line"> (<span class="name">princ</span> (<span class="name">code-char</span> (<span class="name">+</span> digit (<span class="name">char-code</span> #\<span class="number">0</span>)))))))</span><br><span class="line"></span><br><span class="line">(<span class="name">format-decimal</span> <span class="number">233</span> <span class="symbol">:mincol</span> <span class="number">6</span>)</span><br></pre></td></tr></table></figure><p>如果要求用数字0而不是空格来填充左侧的列,用<code>format</code>的写法如下:</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~6,'0D"</span> <span class="number">233</span>)</span><br></pre></td></tr></table></figure><p><code>format-decimal</code>想要做到同样的事情,可以这么写:</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defun</span> format-decimal (<span class="name">n</span></span><br><span class="line"> <span class="symbol">&key</span></span><br><span class="line"> mincol</span><br><span class="line"> (<span class="name">padchar</span> #\Space))</span><br><span class="line"> <span class="string">"打印整数 N 到标准输出。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">MINCOL 如果不为 NIL,则表示所打印的内容至少要占据的列数。</span></span><br><span class="line"><span class="string">PADCHAR 表达式为了填充多余的列时所用的字符。"</span></span><br><span class="line"> (<span class="name">check-type</span> mincol (<span class="name">or</span> integer null))</span><br><span class="line"> (<span class="name">check-type</span> padchar character)</span><br><span class="line"> <span class="comment">;; 通过取余的方式得到 N 的每一位并逐个入栈,之后出栈的顺序就是从左到右打印的顺序了。</span></span><br><span class="line"> (<span class="name">let</span> ((<span class="name">digits</span> '()))</span><br><span class="line"> (<span class="name">cond</span> ((<span class="name">zerop</span> n)</span><br><span class="line"> (<span class="name">push</span> <span class="number">0</span> digits))</span><br><span class="line"> (<span class="name">t</span></span><br><span class="line"> (<span class="name">do</span> ((<span class="name">n</span> n (<span class="name">truncate</span> n <span class="number">10</span>)))</span><br><span class="line"> ((<span class="name">zerop</span> n))</span><br><span class="line"> (<span class="name">push</span> (<span class="name">rem</span> n <span class="number">10</span>) digits))))</span><br><span class="line"> <span class="comment">;; 打印出填充用的空格。</span></span><br><span class="line"> (<span class="name">when</span> (<span class="name">and</span> (<span class="name">integerp</span> mincol) (<span class="name">></span> mincol (<span class="name">length</span> digits)))</span><br><span class="line"> (<span class="name">dotimes</span> (<span class="name">i</span> (<span class="name">-</span> mincol (<span class="name">length</span> digits)))</span><br><span class="line"> (<span class="name">declare</span> (<span class="name">ignorable</span> i))</span><br><span class="line"> (<span class="name">princ</span> padchar)))</span><br><span class="line"></span><br><span class="line"> (<span class="name">dolist</span> (<span class="name">digit</span> digits)</span><br><span class="line"> (<span class="name">princ</span> (<span class="name">code-char</span> (<span class="name">+</span> digit (<span class="name">char-code</span> #\<span class="number">0</span>)))))))</span><br><span class="line"></span><br><span class="line">(<span class="name">format-decimal</span> <span class="number">233</span> <span class="symbol">:mincol</span> <span class="number">6</span> <span class="symbol">:padchar</span> #\<span class="number">0</span>)</span><br></pre></td></tr></table></figure><p><code>-D</code>默认是不会打印非负整数的符号的,可以用修饰符<code>@</code>来修改这个行为。例如,<code>(format t "~6,'0@D" 233)</code>会打印出<code>00+233</code>。稍微修改一下就可以在<code>format-decimal</code>中实现同样的功能</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defun</span> format-decimal (<span class="name">n</span></span><br><span class="line"> <span class="symbol">&key</span></span><br><span class="line"> mincol</span><br><span class="line"> (<span class="name">padchar</span> #\Space)</span><br><span class="line"> signed)</span><br><span class="line"> <span class="string">"打印整数 N 到标准输出。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">MINCOL 如果不为 NIL,则表示所打印的内容至少要占据的列数。</span></span><br><span class="line"><span class="string">PADCHAR 表达式为了填充多余的列时所用的字符。"</span></span><br><span class="line"> (<span class="name">check-type</span> mincol (<span class="name">or</span> integer null))</span><br><span class="line"> (<span class="name">check-type</span> padchar character)</span><br><span class="line"> (<span class="name">flet</span> ((<span class="name">to-digits</span> (<span class="name">n</span>)</span><br><span class="line"> <span class="comment">;; 通过取余的方式得到 N 的每一位并逐个入栈,之后出栈的顺序就是从左到右打印的顺序了。</span></span><br><span class="line"> (<span class="name">let</span> ((<span class="name">digits</span> '()))</span><br><span class="line"> (<span class="name">cond</span> ((<span class="name">zerop</span> n)</span><br><span class="line"> (<span class="name">push</span> #\<span class="number">0</span> digits))</span><br><span class="line"> (<span class="name">t</span></span><br><span class="line"> (<span class="name">do</span> ((<span class="name">n</span> n (<span class="name">truncate</span> n <span class="number">10</span>)))</span><br><span class="line"> ((<span class="name">zerop</span> n))</span><br><span class="line"> (<span class="name">push</span> (<span class="name">code-char</span> (<span class="name">+</span> (<span class="name">rem</span> n <span class="number">10</span>) (<span class="name">char-code</span> #\<span class="number">0</span>))) digits))))</span><br><span class="line"> digits)))</span><br><span class="line"> <span class="comment">;; 通过取余的方式得到 N 的每一位并逐个入栈,之后出栈的顺序就是从左到右打印的顺序了。</span></span><br><span class="line"> (<span class="name">let</span> ((<span class="name">digits</span> (<span class="name">to-digits</span> (<span class="name">abs</span> n))))</span><br><span class="line"> (<span class="name">when</span> (<span class="name">or</span> signed (<span class="name"><</span> n <span class="number">0</span>))</span><br><span class="line"> (<span class="name">push</span> (<span class="name">if</span> (<span class="name"><</span> n <span class="number">0</span>) #\- #\+) digits))</span><br><span class="line"> <span class="comment">;; 打印出填充用的空格。</span></span><br><span class="line"> (<span class="name">when</span> (<span class="name">and</span> (<span class="name">integerp</span> mincol) (<span class="name">></span> mincol (<span class="name">length</span> digits)))</span><br><span class="line"> (<span class="name">dotimes</span> (<span class="name">i</span> (<span class="name">-</span> mincol (<span class="name">length</span> digits)))</span><br><span class="line"> (<span class="name">declare</span> (<span class="name">ignorable</span> i))</span><br><span class="line"> (<span class="name">princ</span> padchar)))</span><br><span class="line"></span><br><span class="line"> (<span class="name">dolist</span> (<span class="name">digit</span> digits)</span><br><span class="line"> (<span class="name">princ</span> digit)))))</span><br><span class="line"></span><br><span class="line">(<span class="name">format-decimal</span> <span class="number">233</span> <span class="symbol">:mincol</span> <span class="number">6</span> <span class="symbol">:padchar</span> #\<span class="number">0</span> <span class="symbol">:signed</span> <span class="literal">t</span>)</span><br></pre></td></tr></table></figure><p>除了<code>@</code>之外,<code>:</code>也是一个<code>~D</code>的修饰符,它可以让<code>format</code>每隔3个数字就打印出一个逗号,方便阅读比较长的数字。例如,下列代码会打印出<code>00+23,333</code>:</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~9,'0@:D"</span> <span class="number">23333</span>)</span><br></pre></td></tr></table></figure><p>为此,给<code>format-decimal</code>新增一个关键字参数<code>comma-separated</code>来控制这一行为。</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defun</span> format-decimal (<span class="name">n</span></span><br><span class="line"> <span class="symbol">&key</span></span><br><span class="line"> comma-separated</span><br><span class="line"> mincol</span><br><span class="line"> (<span class="name">padchar</span> #\Space)</span><br><span class="line"> signed)</span><br><span class="line"> <span class="string">"打印整数 N 到标准输出。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">COMMA-SEPARATED 如果为 T,则每打印3个字符就打印一个逗号。</span></span><br><span class="line"><span class="string">MINCOL 如果不为 NIL,则表示所打印的内容至少要占据的列数。</span></span><br><span class="line"><span class="string">PADCHAR 表示填充多余的列时所用的字符。</span></span><br><span class="line"><span class="string">SIGNED 控制是否显示非负整数的加号。"</span></span><br><span class="line"> (<span class="name">check-type</span> comma-separated boolean)</span><br><span class="line"> (<span class="name">check-type</span> mincol (<span class="name">or</span> integer null))</span><br><span class="line"> (<span class="name">check-type</span> padchar character)</span><br><span class="line"> (<span class="name">check-type</span> signed boolean)</span><br><span class="line"> (<span class="name">flet</span> ((<span class="name">to-digits</span> (<span class="name">n</span>)</span><br><span class="line"> <span class="comment">;; 通过取余的方式得到 N 的每一位并逐个入栈,之后出栈的顺序就是从左到右打印的顺序了。</span></span><br><span class="line"> (<span class="name">let</span> ((<span class="name">digits</span> '()))</span><br><span class="line"> (<span class="name">cond</span> ((<span class="name">zerop</span> n)</span><br><span class="line"> (<span class="name">push</span> #\<span class="number">0</span> digits))</span><br><span class="line"> (<span class="name">t</span></span><br><span class="line"> (<span class="name">do</span> ((<span class="name">count</span> <span class="number">0</span> (<span class="number">1</span>+ count))</span><br><span class="line"> (<span class="name">n</span> n (<span class="name">truncate</span> n <span class="number">10</span>)))</span><br><span class="line"> ((<span class="name">zerop</span> n))</span><br><span class="line"> (<span class="name">when</span> (<span class="name">and</span> comma-separated (<span class="name">></span> count <span class="number">0</span>) (<span class="name">zerop</span> (<span class="name">rem</span> count <span class="number">3</span>)))</span><br><span class="line"> (<span class="name">push</span> #\, digits))</span><br><span class="line"> (<span class="name">push</span> (<span class="name">code-char</span> (<span class="name">+</span> (<span class="name">rem</span> n <span class="number">10</span>) (<span class="name">char-code</span> #\<span class="number">0</span>))) digits))))</span><br><span class="line"> digits)))</span><br><span class="line"> <span class="comment">;; 通过取余的方式得到 N 的每一位并逐个入栈,之后出栈的顺序就是从左到右打印的顺序了。</span></span><br><span class="line"> (<span class="name">let</span> ((<span class="name">digits</span> (<span class="name">to-digits</span> (<span class="name">abs</span> n))))</span><br><span class="line"> (<span class="name">when</span> (<span class="name">or</span> signed (<span class="name"><</span> n <span class="number">0</span>))</span><br><span class="line"> (<span class="name">push</span> (<span class="name">if</span> (<span class="name"><</span> n <span class="number">0</span>) #\- #\+) digits))</span><br><span class="line"> <span class="comment">;; 打印出填充用的空格。</span></span><br><span class="line"> (<span class="name">when</span> (<span class="name">and</span> (<span class="name">integerp</span> mincol) (<span class="name">></span> mincol (<span class="name">length</span> digits)))</span><br><span class="line"> (<span class="name">dotimes</span> (<span class="name">i</span> (<span class="name">-</span> mincol (<span class="name">length</span> digits)))</span><br><span class="line"> (<span class="name">declare</span> (<span class="name">ignorable</span> i))</span><br><span class="line"> (<span class="name">princ</span> padchar)))</span><br><span class="line"></span><br><span class="line"> (<span class="name">dolist</span> (<span class="name">digit</span> digits)</span><br><span class="line"> (<span class="name">princ</span> digit)))))</span><br><span class="line"></span><br><span class="line">(<span class="name">format-decimal</span> <span class="number">-23333</span> <span class="symbol">:comma-separated</span> <span class="literal">t</span> <span class="symbol">:mincol</span> <span class="number">9</span> <span class="symbol">:padchar</span> #\<span class="number">0</span> <span class="symbol">:signed</span> <span class="literal">t</span>)</span><br></pre></td></tr></table></figure><p>事实上,打印分隔符的步长,以及作为分隔符的逗号都是可以定制的。例如,可以改为每隔4个数字打印一个连字符</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~9,'0,'-,4@:D"</span> <span class="number">23333</span>)</span><br></pre></td></tr></table></figure><p>对于<code>format-decimal</code>来说这个修改现在很简单了</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defun</span> format-decimal (<span class="name">n</span></span><br><span class="line"> <span class="symbol">&key</span></span><br><span class="line"> (<span class="name">commachar</span> #\,)</span><br><span class="line"> (<span class="name">comma-interval</span> <span class="number">3</span>)</span><br><span class="line"> comma-separated</span><br><span class="line"> mincol</span><br><span class="line"> (<span class="name">padchar</span> #\Space)</span><br><span class="line"> signed)</span><br><span class="line"> <span class="string">"打印整数 N 到标准输出。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">COMMACHAR 表示当需要打印分隔符时的分隔符。</span></span><br><span class="line"><span class="string">COMMA-INTERVAL 表示当需要打印分隔符时需要间隔的步长。</span></span><br><span class="line"><span class="string">COMMA-SEPARATED 如果为 T,则每打印3个字符就打印一个逗号。</span></span><br><span class="line"><span class="string">MINCOL 如果不为 NIL,则表示所打印的内容至少要占据的列数。</span></span><br><span class="line"><span class="string">PADCHAR 表示填充多余的列时所用的字符。</span></span><br><span class="line"><span class="string">SIGNED 控制是否显示非负整数的加号。"</span></span><br><span class="line"> (<span class="name">check-type</span> commachar character)</span><br><span class="line"> (<span class="name">check-type</span> comma-interval integer)</span><br><span class="line"> (<span class="name">check-type</span> comma-separated boolean)</span><br><span class="line"> (<span class="name">check-type</span> mincol (<span class="name">or</span> integer null))</span><br><span class="line"> (<span class="name">check-type</span> padchar character)</span><br><span class="line"> (<span class="name">check-type</span> signed boolean)</span><br><span class="line"> (<span class="name">flet</span> ((<span class="name">to-digits</span> (<span class="name">n</span>)</span><br><span class="line"> <span class="comment">;; 通过取余的方式得到 N 的每一位并逐个入栈,之后出栈的顺序就是从左到右打印的顺序了。</span></span><br><span class="line"> (<span class="name">let</span> ((<span class="name">digits</span> '()))</span><br><span class="line"> (<span class="name">cond</span> ((<span class="name">zerop</span> n)</span><br><span class="line"> (<span class="name">push</span> #\<span class="number">0</span> digits))</span><br><span class="line"> (<span class="name">t</span></span><br><span class="line"> (<span class="name">do</span> ((<span class="name">count</span> <span class="number">0</span> (<span class="number">1</span>+ count))</span><br><span class="line"> (<span class="name">n</span> n (<span class="name">truncate</span> n <span class="number">10</span>)))</span><br><span class="line"> ((<span class="name">zerop</span> n))</span><br><span class="line"> (<span class="name">when</span> (<span class="name">and</span> comma-separated (<span class="name">></span> count <span class="number">0</span>) (<span class="name">zerop</span> (<span class="name">rem</span> count comma-interval)))</span><br><span class="line"> (<span class="name">push</span> commachar digits))</span><br><span class="line"> (<span class="name">push</span> (<span class="name">code-char</span> (<span class="name">+</span> (<span class="name">rem</span> n <span class="number">10</span>) (<span class="name">char-code</span> #\<span class="number">0</span>))) digits))))</span><br><span class="line"> digits)))</span><br><span class="line"> <span class="comment">;; 通过取余的方式得到 N 的每一位并逐个入栈,之后出栈的顺序就是从左到右打印的顺序了。</span></span><br><span class="line"> (<span class="name">let</span> ((<span class="name">digits</span> (<span class="name">to-digits</span> (<span class="name">abs</span> n))))</span><br><span class="line"> (<span class="name">when</span> (<span class="name">or</span> signed (<span class="name"><</span> n <span class="number">0</span>))</span><br><span class="line"> (<span class="name">push</span> (<span class="name">if</span> (<span class="name"><</span> n <span class="number">0</span>) #\- #\+) digits))</span><br><span class="line"> <span class="comment">;; 打印出填充用的空格。</span></span><br><span class="line"> (<span class="name">when</span> (<span class="name">and</span> (<span class="name">integerp</span> mincol) (<span class="name">></span> mincol (<span class="name">length</span> digits)))</span><br><span class="line"> (<span class="name">dotimes</span> (<span class="name">i</span> (<span class="name">-</span> mincol (<span class="name">length</span> digits)))</span><br><span class="line"> (<span class="name">declare</span> (<span class="name">ignorable</span> i))</span><br><span class="line"> (<span class="name">princ</span> padchar)))</span><br><span class="line"></span><br><span class="line"> (<span class="name">dolist</span> (<span class="name">digit</span> digits)</span><br><span class="line"> (<span class="name">princ</span> digit)))))</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">(<span class="name">format-decimal</span> <span class="number">-23333</span> <span class="symbol">:commachar</span> #\- <span class="symbol">:comma-interval</span> <span class="number">4</span> <span class="symbol">:comma-separated</span> <span class="literal">t</span> <span class="symbol">:mincol</span> <span class="number">9</span> <span class="symbol">:padchar</span> #\<span class="number">0</span> <span class="symbol">:signed</span> <span class="literal">t</span>)</span><br></pre></td></tr></table></figure><p>全文完。</p>]]></content>
<summary type="html"></summary>
<category term="common lisp" scheme="https://liutos.github.io/tags/common-lisp/"/>
<category term="lisp" scheme="https://liutos.github.io/tags/lisp/"/>
<category term="format" scheme="https://liutos.github.io/tags/format/"/>
</entry>
<entry>
<title>为什么管Java叫单派发以及什么是多派发</title>
<link href="https://liutos.github.io/2021/10/15/%E4%B8%BA%E4%BB%80%E4%B9%88%E7%AE%A1Java%E5%8F%AB%E5%8D%95%E6%B4%BE%E5%8F%91%E4%BB%A5%E5%8F%8A%E4%BB%80%E4%B9%88%E6%98%AF%E5%A4%9A%E6%B4%BE%E5%8F%91/"/>
<id>https://liutos.github.io/2021/10/15/%E4%B8%BA%E4%BB%80%E4%B9%88%E7%AE%A1Java%E5%8F%AB%E5%8D%95%E6%B4%BE%E5%8F%91%E4%BB%A5%E5%8F%8A%E4%BB%80%E4%B9%88%E6%98%AF%E5%A4%9A%E6%B4%BE%E5%8F%91/</id>
<published>2021-10-15T15:43:05.000Z</published>
<updated>2021-10-15T15:43:08.000Z</updated>
<content type="html"><![CDATA[<p>众所周知,在 Java 语言中支持基于子类型的多态,例如某百科全书中就给了一个基于<code>Animal</code>及其两个子类的例子(代码经过我微微调整)</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">abstract</span> <span class="keyword">class</span> <span class="title class_">Animal</span> {</span><br><span class="line"> <span class="keyword">abstract</span> String <span class="title function_">talk</span><span class="params">()</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Cat</span> <span class="keyword">extends</span> <span class="title class_">Animal</span> {</span><br><span class="line"> String <span class="title function_">talk</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"Meow!"</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Dog</span> <span class="keyword">extends</span> <span class="title class_">Animal</span> {</span><br><span class="line"> String <span class="title function_">talk</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"Woof!"</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Example</span> {</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">letsHear</span><span class="params">(<span class="keyword">final</span> Animal a)</span> {</span><br><span class="line"> System.out.println(a.talk());</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> {</span><br><span class="line"> letsHear(<span class="keyword">new</span> <span class="title class_">Cat</span>());</span><br><span class="line"> letsHear(<span class="keyword">new</span> <span class="title class_">Dog</span>());</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>基于子类型的多态要求在程序的运行期根据参数的类型,选择不同的具体方法——例如在上述例子中,当方法<code>letsHear</code>中调用了参数<code>a</code>的方法<code>talk</code>时,是依照变量<code>a</code>在运行期的类型(第一次为<code>Cat</code>,第二次为<code>Dog</code>)来选择对应的<code>talk</code>方法的实例的,而不是依照编译期的类型<code>Animal</code>。</p><p>但在不同的语言中,在运行期查找方法时,所选择的参数的个数是不同的。对于 Java 而言,它只取方法的第一个参数(即接收者),这个策略被称为 single dispatch。</p><h1 id="Java-的-single-dispatch"><a href="#Java-的-single-dispatch" class="headerlink" title="Java 的 single dispatch"></a>Java 的 single dispatch</h1><p>要演示为什么 Java 是 single dispatch 的,必须让示例代码中的方法接收两个参数(除了方法的接收者之外再来一个参数)</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 演示 Java 是 single dispatch 的。</span></span><br><span class="line"><span class="keyword">abstract</span> <span class="keyword">class</span> <span class="title class_">Shape</span> {}</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Circle</span> <span class="keyword">extends</span> <span class="title class_">Shape</span> {}</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Rectangle</span> <span class="keyword">extends</span> <span class="title class_">Shape</span> {}</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Triangle</span> <span class="keyword">extends</span> <span class="title class_">Shape</span> {}</span><br><span class="line"></span><br><span class="line"><span class="keyword">abstract</span> <span class="keyword">class</span> <span class="title class_">AbstractResizer</span> </span><br><span class="line">{</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="keyword">void</span> <span class="title function_">resize</span><span class="params">(Circle c)</span>;</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="keyword">void</span> <span class="title function_">resize</span><span class="params">(Rectangle r)</span>;</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="keyword">void</span> <span class="title function_">resize</span><span class="params">(Shape s)</span>;</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="keyword">void</span> <span class="title function_">resize</span><span class="params">(Triangle t)</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Resizer</span> <span class="keyword">extends</span> <span class="title class_">AbstractResizer</span></span><br><span class="line">{</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">resize</span><span class="params">(Circle c)</span> { System.out.println(<span class="string">"缩放圆形"</span>); }</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">resize</span><span class="params">(Rectangle r)</span> { System.out.println(<span class="string">"缩放矩形"</span>); }</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">resize</span><span class="params">(Shape s)</span> { System.out.println(<span class="string">"缩放任意图形"</span>); }</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">resize</span><span class="params">(Triangle t)</span> { System.out.println(<span class="string">"缩放三角形"</span>); }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Trial1</span></span><br><span class="line">{</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span></span><br><span class="line">{</span><br><span class="line"><span class="type">AbstractResizer</span> <span class="variable">resizer</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Resizer</span>();</span><br><span class="line">Shape[] shapes = {<span class="keyword">new</span> <span class="title class_">Circle</span>(), <span class="keyword">new</span> <span class="title class_">Rectangle</span>(), <span class="keyword">new</span> <span class="title class_">Triangle</span>()};</span><br><span class="line"><span class="keyword">for</span> (Shape shape : shapes)</span><br><span class="line">{</span><br><span class="line">resizer.resize(shape);</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>显然,类<code>Resizer</code>的实例方法<code>resize</code>就是接收两个参数的——第一个为<code>Resizer</code>类的实例对象,第二个则可能是<code>Shape</code>及其三个子类中的一种类的实例对象。假如 Java 的多态策略是 multiple dispatch 的,那么应当分别调用不同的三个版本的<code>resize</code>方法,但实际上并不是</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20211015/Java%E6%98%AFsingledispatch%E7%9A%84%E8%AF%81%E6%8D%AE.jpg"></p><p>通过 JDK 中提供的程序<code>javap</code>可以看到在<code>main</code>方法中调用<code>resize</code>方法时究竟用的是类<code>Resizer</code>中的哪一个版本,运行命令<code>javap -c -l -s -v Trial1</code>,可以看到调用<code>resize</code>方法对应的 JVM 字节码为<code>invokevirtual</code></p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20211015/invokevirtual%E5%91%BD%E4%BB%A4.jpg"></p><p>翻阅 JVM 规格文档可以找到对<a href="https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.invokevirtual">invokevirtual 指令的解释</a></p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20211015/invokevirtual%E7%9A%84%E6%9F%A5%E6%89%BE%E9%80%BB%E8%BE%91.jpg"></p><p>显然,由于在 JVM 的字节码中,<code>invokevirtual</code>所调用的方法的参数类型已经解析完毕——<code>LShape</code>表示是一个叫做<code>Shape</code>的类,因此在方法接收者,即类<code>Resizer</code>中查找的时候,也只会命中<code>resize(Shape s)</code>这个版本的方法。变量<code>s</code>的运行期类型在查找方法的时候,丝毫没有派上用场,因此 Java 的多态是 single dispatch 的。</p><p>想要依据参数的运行期类型来打印不同内容也不难,简单粗暴的办法可以选择<code>instanceOf</code></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">abstract</span> <span class="keyword">class</span> <span class="title class_">AbstractResizer</span> </span><br><span class="line">{</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="keyword">void</span> <span class="title function_">resize</span><span class="params">(Shape s)</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Resizer</span> <span class="keyword">extends</span> <span class="title class_">AbstractResizer</span></span><br><span class="line">{</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">resize</span><span class="params">(Shape s)</span> { </span><br><span class="line"> <span class="keyword">if</span> (s <span class="keyword">instanceof</span> Circle) {</span><br><span class="line"> System.out.println(<span class="string">"缩放圆形"</span>);</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (s <span class="keyword">instanceof</span> Rectangle) {</span><br><span class="line"> System.out.println(<span class="string">"缩放矩形"</span>);</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (s <span class="keyword">instanceof</span> Triangle) {</span><br><span class="line"> System.out.println(<span class="string">"缩放三角形"</span>);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> System.out.println(<span class="string">"缩放任意图形"</span>);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>或者动用 Visitor 模式。</p><h1 id="什么是-multiple-dispatch?"><a href="#什么是-multiple-dispatch?" class="headerlink" title="什么是 multiple dispatch?"></a>什么是 multiple dispatch?</h1><p>我第一次知道 multiple dispatch 这个词语,其实就是在偶然间查找 CLOS 的相关资料时看到的。在 Common Lisp 中,定义类和方法的语法与常见的语言画风不太一样。例如,下列代码跟 Java 一样定义了四个类</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defclass</span> shape ()</span><br><span class="line"> ())</span><br><span class="line"></span><br><span class="line">(<span class="name">defclass</span> circle (<span class="name">shape</span>)</span><br><span class="line"> ())</span><br><span class="line"></span><br><span class="line">(<span class="name">defclass</span> rectangle (<span class="name">shape</span>)</span><br><span class="line"> ())</span><br><span class="line"></span><br><span class="line">(<span class="name">defclass</span> triangle (<span class="name">shape</span>)</span><br><span class="line"> ())</span><br><span class="line"></span><br><span class="line">(<span class="name">defclass</span> abstract-resizer ()</span><br><span class="line"> ())</span><br><span class="line"></span><br><span class="line">(<span class="name">defclass</span> resizer (<span class="name">abstract-resizer</span>)</span><br><span class="line"> ())</span><br><span class="line"></span><br><span class="line">(<span class="name">defgeneric</span> resize (<span class="name">resizer</span> shape))</span><br><span class="line"></span><br><span class="line">(<span class="name">defmethod</span> resize ((<span class="name">resizer</span> resizer) (<span class="name">shape</span> circle))</span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"缩放圆形~%"</span>))</span><br><span class="line"></span><br><span class="line">(<span class="name">defmethod</span> resize ((<span class="name">resizer</span> resizer) (<span class="name">shape</span> rectangle))</span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"缩放矩形~%"</span>))</span><br><span class="line"></span><br><span class="line">(<span class="name">defmethod</span> resize ((<span class="name">resizer</span> resizer) (<span class="name">shape</span> shape))</span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"缩放任意图形~%"</span>))</span><br><span class="line"></span><br><span class="line">(<span class="name">defmethod</span> resize ((<span class="name">resizer</span> resizer) (<span class="name">shape</span> triangle))</span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"缩放三角形~%"</span>))</span><br><span class="line"></span><br><span class="line">(<span class="name">let</span> ((<span class="name">resizer</span> (<span class="name">make-instance</span> 'resizer))</span><br><span class="line"> (<span class="name">shapes</span> (<span class="name">list</span></span><br><span class="line"> (<span class="name">make-instance</span> 'circle)</span><br><span class="line"> (<span class="name">make-instance</span> 'rectangle)</span><br><span class="line"> (<span class="name">make-instance</span> 'triangle))))</span><br><span class="line"> (<span class="name">dolist</span> (<span class="name">shape</span> shapes)</span><br><span class="line"> (<span class="name">resize</span> resizer shape)))</span><br></pre></td></tr></table></figure><p>执行上述代码会调用不同版本的<code>resize</code>方法来打印内容</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20211015/multipledispatch%E7%9A%84%E4%BE%8B%E5%AD%90.jpg"></p><p>由于<code>defmethod</code>支持给每一个参数都声明对应的类这一做法是在太符合直觉了,以至于我丝毫没有意识到它有一个专门的名字叫做 multiple dispatch,并且在大多数语言中是不支持的。</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20211015/CL%E5%B0%B1%E6%98%AF%E5%8E%89%E5%AE%B3.png"></p><h1 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h1><p>聪明的你应该已经发现了,在上面的 Common Lisp 代码中,其实与 Java 中的抽象类<code>AbstractResizer</code>对应的类<code>abstract-resizer</code>是完全没有必要的,<code>defgeneric</code>本身就是一种用来定义抽象<em>接口</em>的手段。</p><p>此外,在第三个版本的<code>resize</code>方法中,可以看到标识符<code>shape</code>同时作为了参数的名字和该参数所属的类的名字——没错,在 Common Lisp 中,一个符号不仅仅可以同时代表一个变量和一个函数,同时还可以兼任一个类型,它不仅仅是一门通常所说的 Lisp-2 的语言。</p>]]></content>
<summary type="html"></summary>
</entry>
<entry>
<title>直观地了解自己每天在吃上面的开销</title>
<link href="https://liutos.github.io/2021/10/06/%E7%9B%B4%E8%A7%82%E5%9C%B0%E4%BA%86%E8%A7%A3%E8%87%AA%E5%B7%B1%E6%AF%8F%E5%A4%A9%E5%9C%A8%E5%90%83%E4%B8%8A%E9%9D%A2%E7%9A%84%E5%BC%80%E9%94%80/"/>
<id>https://liutos.github.io/2021/10/06/%E7%9B%B4%E8%A7%82%E5%9C%B0%E4%BA%86%E8%A7%A3%E8%87%AA%E5%B7%B1%E6%AF%8F%E5%A4%A9%E5%9C%A8%E5%90%83%E4%B8%8A%E9%9D%A2%E7%9A%84%E5%BC%80%E9%94%80/</id>
<published>2021-10-06T14:45:06.000Z</published>
<updated>2021-10-06T14:45:12.000Z</updated>
<content type="html"><![CDATA[<p><del>众所周知,</del>我用<code>Emacs</code>的<code>ledger-mode</code>来记账(参见以前的文章<a href="https://liutos.github.io/2020/03/19/%E7%A8%8B%E5%BA%8F%E5%91%98%E7%9A%84%E8%AE%B0%E8%B4%A6%E5%B7%A5%E5%85%B7%E2%80%94%E2%80%94ledger%E4%B8%8Eledger-mode/">《程序员的记账工具——ledger与ledger-mode》</a>)。作为一个出色的命令行报表工具,<code>ledger</code>的命令<code>balance</code>和<code>register</code>足以涵盖大部分的使用场景:</p><ul><li><code>balance</code>可以生成所有帐号的余额的报表,用于每天与各个账户中的真实余额进行比较;</li><li><code>register</code>可以生成给定帐号的交易明细,用于在余额不一致时与真实账户的流水一条条核对;</li></ul><p>美中不足的是,<code>ledger</code>的报表不够直观,因为它们是冷冰冰的文字信息,而不是振奋人心的统计图形。好在,正如<code>ledger</code>不存储数据,而只是一份份<code>.ledger</code>文件中的交易记录的搬运工一样,<code>gnuplot</code>也是这样的工具——它不存储数据,它只负责将存储在文本文件的数据以图形的形态呈现出来。</p><h1 id="如何运用gnuplot"><a href="#如何运用gnuplot" class="headerlink" title="如何运用gnuplot"></a>如何运用<code>gnuplot</code></h1><p><code>gnuplot</code>是很容易使用的。以最简单的情况为例,首先将如下内容保存到文件<code>/tmp/data.csv</code>中</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">-1 -1</span><br><span class="line">0 0</span><br><span class="line">1 1</span><br></pre></td></tr></table></figure><p>然后在命令行中启动<code>gnuplot</code>,进入它的 REPL 中,并执行如下命令</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">plot "/tmp/data.csv"</span><br></pre></td></tr></table></figure><p>即可得到这三组数据的展示</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20211006/gnuplot%E7%BB%98%E5%9B%BE%E6%95%88%E6%9E%9C%E5%88%9D%E8%AF%86.jpg"></p><p>三组数据分别是坐标为<code>(-1, -1)</code>、<code>(0, 0)</code>,以及<code>(1, 1)</code>的点。</p><p>因此要让<code>gnuplot</code>绘制开销的图形,首先就是从账本中提取出要绘制的数据,再决定如何用<code>gnuplot</code>绘制即可。</p><h2 id="用ledger提取开销记录"><a href="#用ledger提取开销记录" class="headerlink" title="用ledger提取开销记录"></a>用<code>ledger</code>提取开销记录</h2><p>尽管<code>ledger</code>的子命令<code>register</code>可以打印出给定帐号的交易明细,但此处更适合使用<code>csv</code>子命令。例如,下列的命令可以将最早的10条、吃的方面的支出记录,都以 CSV 格式打印出来</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">➜ Accounting ledger --anon --head 10 -f 2021.ledger csv 'Expense:Food'</span><br><span class="line">"2019/09/10","","32034acc","efe2a5b9:c720f278:58a3cd91:0dc07b7b","A","20","",""</span><br><span class="line">"2019/09/11","","a61b6164","5d45e249:fe84ca06:778d1855:daf61ede","A","5","",""</span><br><span class="line">"2019/09/11","","674ec19f","5d018df1:ebf020db:29d43aba:d0c84127","A","15","",""</span><br><span class="line">"2019/09/11","","e55ff018","370ca545:7d3aa2d0:86f5f330:1379261b","A","20","",""</span><br><span class="line">"2019/09/12","","f6aa675c","08315491:4c8f1ee7:5eeaddf3:f879914e","A","10.5","",""</span><br><span class="line">"2019/09/12","","139b790f","a137e4ee:9bc8ee49:7d7ccd8b:472d6007","A","23.9","",""</span><br><span class="line">"2019/09/12","","b24b716d","de348971:5364622c:b2144d94:01e74ff3","A","148","",""</span><br><span class="line">"2019/09/13","","e7c066fa","b418a3b2:a3e21e87:a32ee8ac:8716a847","A","3","",""</span><br><span class="line">"2019/09/13","","9eb044fe","702a13e9:3de7f1bd:9b20a278:1d20668d","A","24","",""</span><br><span class="line">"2019/09/13","","ba301270","d2b7eeb3:381f9473:54f86a33:391a8662","A","36","",""</span><br></pre></td></tr></table></figure><p><code>--anon</code>选项可以将交易明细中的敏感信息(如收款方、帐号)等匿名处理。</p><p>尽管<code>ledger</code>打印出的内容有很多列,但只有第一列的日期,以及第六列的金额是我所需要的。同时,由于一天中可能会有多次吃的方面的开销,因此同一天的交易也会有多笔,在绘图之前,需要将同一天之中的开销累加起来,只留下一个数字。这两个需求,都可以用<code>csvsql</code>来满足。</p><h2 id="用csvsql聚合数据"><a href="#用csvsql聚合数据" class="headerlink" title="用csvsql聚合数据"></a>用<code>csvsql</code>聚合数据</h2><p>以前文中的10条记录为例,用如下的命令可以将它们按天聚合在一起</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ledger --anon --head 10 -f 2021.ledger csv 'Expense:Food' | csvsql -H --query 'SELECT `a`, SUM(`f`) FROM `expense` GROUP BY `a` ORDER BY `a` ASC' --tables 'expense'</span><br></pre></td></tr></table></figure><p>其中:</p><ul><li>选项<code>-H</code>让<code>csvsql</code>知道从管道中输入的数据没有标题行。后续处理时,<code>csvsql</code>会默认使用<code>a</code>、<code>b</code>、<code>c</code>等作为列名;</li><li>选项<code>--query</code>用于提交要执行的 SQL 语句;</li><li>选项<code>--tables</code>用于指定表的名字,这样在<code>--query</code>中才能用 SQL 对其进行处理;</li></ul><p>结果如下</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">➜ Accounting ledger --anon --head 10 -f 2021.ledger csv 'Expense:Food' | csvsql -H --query 'SELECT `a`, SUM(`f`) FROM `expense` GROUP BY `a` ORDER BY `a` ASC' --tables 'expense'</span><br><span class="line">a,SUM(`f`)</span><br><span class="line">2019-09-10,20</span><br><span class="line">2019-09-11,40</span><br><span class="line">2019-09-12,182.4</span><br><span class="line">2019-09-13,63</span><br></pre></td></tr></table></figure><h2 id="用gnuplot读取数据并绘图"><a href="#用gnuplot读取数据并绘图" class="headerlink" title="用gnuplot读取数据并绘图"></a>用<code>gnuplot</code>读取数据并绘图</h2><p>用重定向将<code>csvsql</code>的输出结果保存到文件<code>/tmp/data.csv</code>中,然后就可以用<code>gnuplot</code>将它们画出来</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">➜ Accounting ledger --anon --head 10 -f 2021.ledger csv 'Expense:Food' | csvsql -H --query 'SELECT `a`, SUM(`f`) FROM `expense` GROUP BY `a` ORDER BY `a` ASC' --tables 'expense' | tail -n '+2' > /tmp/data.csv</span><br><span class="line">➜ Accounting cat /tmp/plot_expense.gplot</span><br><span class="line">set format x '%y-%m-%d'</span><br><span class="line">set style data boxes</span><br><span class="line">set terminal png font '/System/Library/Fonts/Hiragino Sans GB.ttc'</span><br><span class="line">set title '吃的开销'</span><br><span class="line">set output '/tmp/xyz.png'</span><br><span class="line">set timefmt '%Y-%m-%d'</span><br><span class="line">set xdata time</span><br><span class="line">set xlabel '日期'</span><br><span class="line">set xrange ['2019-09-10':'2019-09-13']</span><br><span class="line">set ylabel '金额(¥)'</span><br><span class="line">set yrange [0:200]</span><br><span class="line">set datafile separator comma</span><br><span class="line">plot '/tmp/data.csv' using 1:2</span><br><span class="line">➜ Accounting gnuplot /tmp/plot_expense.gplot</span><br></pre></td></tr></table></figure><p>生成的图片文件<code>/tmp/xyz.png</code>如下</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20211006/xyz_bak.png"></p><p>在脚本文件<code>/tmp/plot_expense.gplot</code>中用到的命令都可以通过<code>gnuplot</code>的<a href="http://www.gnuplot.info/docs_5.4/Gnuplot_5_4.pdf">在线手册</a>查阅到:</p><ul><li><code>set format</code>命令用于设置坐标轴的刻度的格式。<code>set format x "%y-%m-%d"</code>意味着设置 X 轴的刻度为形如<code>19-09-10</code>的格式;</li><li><code>set style data</code>命令设置数据的绘制风格。<code>set style data box</code>表示采用空心柱状图;</li><li><code>set terminal</code>命令用于告诉<code>gnuplot</code>该生成什么样的输出。<code>set terminal png font '/System/Library/Fonts/Hiragino Sans GB.ttc'</code>表示输出结果为 PNG 格式的图片,并且采用给定的字体;</li><li><code>set title</code>命令控制输出结果顶部中间位置的标题文案;</li><li><code>set output</code>命令用于将原本输出到屏幕上的内容重定向到文件中;</li><li><code>set timefmt</code>命令用于指定输入的日期时间数据的格式。<code>set timefmt '%Y-%m-%d'</code>意味着输入的日期时间数据的为形如<code>2019-09-10</code>的格式;</li><li><code>set xdata</code>命令控制<code>gnuplot</code>如何理解属于 X 轴的数据。<code>set xdata time</code>表示 X 轴上的均为时间型数据;</li><li><code>set xlabel</code>命令控制 X 轴的含义的文案。<code>set ylabel</code>与其类似,只是作用在 Y 轴上;</li><li><code>set xrange</code>命令控制<code>gnuplot</code>所绘制的图形中 X 轴上的展示范围;</li><li><code>set datafile separator</code>命令控制<code>gnuplot</code>读取数据文件时各列间的分隔符,<code>comma</code>表示分隔符为逗号。</li></ul><h2 id="想要按周统计怎么办"><a href="#想要按周统计怎么办" class="headerlink" title="想要按周统计怎么办"></a>想要按周统计怎么办</h2><p>假设我要查看的是2021年每一周在吃的方面的总开支,那么需要在<code>csvsql</code>中将数据按所处的是第几周进行聚合</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">➜ Accounting ledger -b '2021-01-01' -f 2021.ledger csv 'Expense:Food' | csvsql -H --query 'SELECT strftime("%W", `a`) AS `week`, SUM(`f`) FROM `expense` GROUP BY `week` ORDER BY `a` ASC' --tables 'expense' | tail -n '+2' > /tmp/expense_dow.csv</span><br><span class="line">➜ Accounting head /tmp/expense_dow.csv</span><br><span class="line">00,633.6</span><br><span class="line">01,437.3</span><br><span class="line">02,337.5</span><br><span class="line">03,428.4</span><br><span class="line">04,191.5</span><br><span class="line">05,330.4</span><br><span class="line">06,154.6</span><br><span class="line">07,621.4</span><br><span class="line">08,485.6</span><br><span class="line">09,375.73</span><br></pre></td></tr></table></figure><p>同时也需要调整<code>gnuplot</code>的脚本</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">set terminal png font '/System/Library/Fonts/Hiragino Sans GB.ttc'</span><br><span class="line">set title '吃的开销'</span><br><span class="line">set output '/tmp/xyz2.png'</span><br><span class="line">set xlabel '第几周'</span><br><span class="line">set xrange [0:54]</span><br><span class="line">set ylabel '金额(¥)'</span><br><span class="line">set yrange [0:1000]</span><br><span class="line">set datafile separator comma</span><br><span class="line">plot '/tmp/expense_dow.csv' using 1:2 with lines</span><br></pre></td></tr></table></figure><p>结果如下</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20211006/xyz2.png"></p><h2 id="想要同时查看两年的图形怎么办"><a href="#想要同时查看两年的图形怎么办" class="headerlink" title="想要同时查看两年的图形怎么办"></a>想要同时查看两年的图形怎么办</h2><p><code>gnuplot</code>支持同时绘制多条曲线,只要使用数据文件中不同的列作为纵坐标即可。假设我要对比的是2020年和2021年,那么先分别统计两年的开支到不同的文件中</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">➜ Accounting ledger -b '2020-01-01' -e '2021-01-01' -f 2021.ledger csv 'Expense:Food' | csvsql -H --query 'SELECT strftime("%W", `a`) AS `week`, SUM(`f`) FROM `expense` GROUP BY `week` ORDER BY `a` ASC' --tables 'expense' | tail -n '+2' > /tmp/expense_2020.csv</span><br><span class="line">➜ Accounting ledger -b '2021-01-01' -f 2021.ledger csv 'Expense:Food' | csvsql -H --query 'SELECT strftime("%W", `a`) AS `week`, SUM(`f`) FROM `expense` GROUP BY `week` ORDER BY `a` ASC' --tables 'expense' | tail -n '+2' > /tmp/expense_2021.csv</span><br></pre></td></tr></table></figure><p>再将处于同一周的数据合并在一起</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">➜ Accounting csvjoin -H -c a /tmp/expense_2020.csv /tmp/expense_2021.csv | tail -n '+2' > /tmp/expense_2years.csv</span><br></pre></td></tr></table></figure><p>最后,再让<code>gnuplot</code>一次性绘制两条折线</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">set terminal png font '/System/Library/Fonts/Hiragino Sans GB.ttc'</span><br><span class="line">set title '吃的开销'</span><br><span class="line">set output '/tmp/xyz2years.png'</span><br><span class="line">set xlabel '第几周'</span><br><span class="line">set xrange [0:54]</span><br><span class="line">set ylabel '金额(¥)'</span><br><span class="line">set yrange [0:1000]</span><br><span class="line">set datafile separator comma</span><br><span class="line">plot '/tmp/expense_2years.csv' using 1:2 with lines title "2020", '/tmp/expense_2years.csv' using 1:3 with lines title "2021"</span><br></pre></td></tr></table></figure><p>结果如下</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20211006/xyz2years.png"></p><h1 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h1><p>其实仍然是非常不直观的,因为最终生成的是一张静态的图片,并不能做到将鼠标挪到曲线上时就给出所在位置的纵坐标的效果。</p>]]></content>
<summary type="html"></summary>
</entry>
<entry>
<title>git-log很好,你也可以写一个</title>
<link href="https://liutos.github.io/2021/09/04/git-log%E5%BE%88%E5%A5%BD%EF%BC%8C%E4%BD%A0%E4%B9%9F%E5%8F%AF%E4%BB%A5%E5%86%99%E4%B8%80%E4%B8%AA/"/>
<id>https://liutos.github.io/2021/09/04/git-log%E5%BE%88%E5%A5%BD%EF%BC%8C%E4%BD%A0%E4%B9%9F%E5%8F%AF%E4%BB%A5%E5%86%99%E4%B8%80%E4%B8%AA/</id>
<published>2021-09-04T09:29:17.000Z</published>
<updated>2021-09-04T09:29:20.000Z</updated>
<content type="html"><![CDATA[<h1 id="序言"><a href="#序言" class="headerlink" title="序言"></a>序言</h1><p>作为一个天天都在用的工具,各位同行想必都非常熟悉 Git 的基本用法,例如:</p><ul><li>用<code>git-blame</code>找出某一行 bug 是哪一位同事引入的,由他背锅;</li><li>用<code>git-merge</code>把别人的代码合进自己完美无瑕的分支中,然后发现单元测试无法跑通;</li><li>用<code>git-push -f</code>把团队里其他人的提交通通覆盖掉。</li></ul><p>除此之外,Git 其实还是一个带版本功能的键值数据库:</p><ul><li>所有提交的内容都存储在目录<code>.git/objects/</code>下;</li><li>有存储文件内容的<code>blob</code>对象、存储文件元数据的<code>tree</code>对象,还有存储提交记录的<code>commit</code>对象等;</li><li>Git 提供了键值风格的读写命令<code>git-cat-file</code>和<code>git-hash-object</code>。</li></ul><p>读过我以前的文章<a href="https://liutos.github.io/2019/12/21/%E5%BD%93%E6%88%91%E4%BB%ACgit-merge%E7%9A%84%E6%97%B6%E5%80%99%E5%88%B0%E5%BA%95%E5%9C%A8merge%E4%BB%80%E4%B9%88/">《当我们git merge的时候到底在merge什么》</a>的朋友们应该都知道,如果一次合并不是<code>fast-forward</code>的,那么会产生一个新的<code>commit</code>类型的对象,并且它有两个父级<code>commit</code>对象。以知名的 Go 语言 Web 框架<code>gin</code>的仓库为例,它的哈希值为<code>e38955615a14e567811e390c87afe705df957f3a</code>的提交是一次合并产生的,这个提交的内容中有两行<code>parent</code></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">➜ gin git:(master) git cat-file -p 'e38955615a14e567811e390c87afe705df957f3a'</span><br><span class="line">tree 93e5046e502847a6355ed26223a902b4de2de7c7</span><br><span class="line">parent ad087650e9881c93a19fd8db75a86968aa998cac</span><br><span class="line">parent ce26751a5a3ed13e9a6aa010d9a7fa767de91b8c</span><br><span class="line">author Javier Provecho Fernandez <[email protected]> 1499534953 +0200</span><br><span class="line">committer Javier Provecho Fernandez <[email protected]> 1499535020 +0200</span><br><span class="line"></span><br><span class="line">Merge pull request #520 from 178inaba/travis-import_path</span><br></pre></td></tr></table></figure><p>通过一个提交的<code>parent</code>属性,所有的提交对象组成了一个有向无环图。但聪明的你应该发现了,<code>git-log</code>的输出结果是线性的,所以 Git 用到了某种图的遍历算法。</p><p>查阅<code>man git-log</code>,可以在<code>Commit Ordering</code>一节中看到</p><blockquote><p>By default, the commits are shown in reverse chronological order.</p></blockquote><p>聪明的你想必已经知道该如何实现这个图的遍历算法了。</p><h1 id="自己动手写一个git-log"><a href="#自己动手写一个git-log" class="headerlink" title="自己动手写一个git-log"></a>自己动手写一个<code>git-log</code></h1><h2 id="解析commit对象"><a href="#解析commit对象" class="headerlink" title="解析commit对象"></a>解析<code>commit</code>对象</h2><p>要想以正确的顺序打印<code>commit</code>对象的信息,得先解析它。我们不需要从零开始自己打开文件、读取字节流,以及解压文件内容,只需要像上文那样调用<code>git-cat-file</code>即可。<code>git-cat-file</code>打印的内容中,有一些是需要提取备用的:</p><ul><li>以<code>parent</code>开头的行。这一行的哈希值要用于定位到有向无环图中的一个节点;</li><li>以<code>committer</code>开头的行。这一行的 UNIX 时间戳将会作为决定谁是“下一个节点”的排序依据。</li></ul><p>可以随手写一个 Python 中的类来解析一个<code>commit</code>对象</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">CommitObject</span>:</span><br><span class="line"> <span class="string">"""一个Git中的commit类型的对象解析后的结果。"""</span></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, *, commit_id: <span class="built_in">str</span></span>) -> <span class="literal">None</span>:</span><br><span class="line"> <span class="variable language_">self</span>.commit_id = commit_id</span><br><span class="line"></span><br><span class="line"> file_content = <span class="variable language_">self</span>._cat_file(commit_id)</span><br><span class="line"> <span class="variable language_">self</span>.parents = <span class="variable language_">self</span>._parse_parents(file_content)</span><br><span class="line"> <span class="variable language_">self</span>.timestamp = <span class="variable language_">self</span>._parse_commit_timestamp(file_content)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">_cat_file</span>(<span class="params">self, commit_id: <span class="built_in">str</span></span>) -> <span class="built_in">str</span>:</span><br><span class="line"> cmd = [<span class="string">'git'</span>, <span class="string">'cat-file'</span>, <span class="string">'-p'</span>, commit_id]</span><br><span class="line"> <span class="keyword">return</span> subprocess.check_output(cmd).decode(<span class="string">'utf-8'</span>)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">_parse_commit_timestamp</span>(<span class="params">self, file_content: <span class="built_in">str</span></span>) -> <span class="built_in">int</span>:</span><br><span class="line"> <span class="string">"""解析出提交的UNIX时间戳。"""</span></span><br><span class="line"> lines = file_content.split(<span class="string">'\n'</span>)</span><br><span class="line"> <span class="keyword">for</span> line <span class="keyword">in</span> lines:</span><br><span class="line"> <span class="keyword">if</span> line.startswith(<span class="string">'committer '</span>):</span><br><span class="line"> m = re.search(<span class="string">'committer .+ <[^ ]+> ([0-9]+)'</span>, line.strip())</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">int</span>(m.group(<span class="number">1</span>))</span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">_parse_parents</span>(<span class="params">self, file_content: <span class="built_in">str</span></span>) -> <span class="type">List</span>[<span class="built_in">str</span>]:</span><br><span class="line"> lines = file_content.split(<span class="string">'\n'</span>)</span><br><span class="line"> parents: <span class="type">List</span>[<span class="built_in">str</span>] = []</span><br><span class="line"> <span class="keyword">for</span> line <span class="keyword">in</span> lines:</span><br><span class="line"> <span class="keyword">if</span> line.startswith(<span class="string">'parent '</span>):</span><br><span class="line"> m = re.search(<span class="string">'parent (.*)'</span>, line.strip())</span><br><span class="line"> parent_id = m.group(<span class="number">1</span>)</span><br><span class="line"> parents.append(parent_id)</span><br><span class="line"> <span class="keyword">return</span> parents</span><br></pre></td></tr></table></figure><h2 id="遍历commit组成的有向无环图——大根堆"><a href="#遍历commit组成的有向无环图——大根堆" class="headerlink" title="遍历commit组成的有向无环图——大根堆"></a>遍历<code>commit</code>组成的有向无环图——大根堆</h2><p>恭喜你,你学过的数据结构可以派上用场了。</p><p>假设用上面的类<code>CommitObject</code>解析了<code>gin</code>中哈希值为<code>e38955615a14e567811e390c87afe705df957f3a</code>的提交,那么它的<code>parents</code>属性中会有两个字符串:</p><ul><li><code>ad087650e9881c93a19fd8db75a86968aa998cac</code>;</li><li><code>ce26751a5a3ed13e9a6aa010d9a7fa767de91b8c</code>。</li></ul><p>其中:</p><ul><li>哈希值为<code>ad087650e9881c93a19fd8db75a86968aa998cac</code>的提交的时间为<code>Sat Jul 8 12:31:44</code>;</li><li>哈希值为<code>ce26751a5a3ed13e9a6aa010d9a7fa767de91b8c</code>的提交时间为<code>Jan 28 02:32:44</code>。</li></ul><p>显然,按照反转的时间先后顺序(<code>reverse chronological</code>)打印日志的话,下一个打印的节点应当是是<code>ad087650e9881c93a19fd8db75a86968aa998cac</code>——用<code>git-log</code>命令可以确认这一点。</p><p>打印完<code>ad087650e9881c93a19fd8db75a86968aa998cac</code>之后,又要从它的父级提交和<code>ce26751a5a3ed13e9a6aa010d9a7fa767de91b8c</code>中,挑选出下一个要打印的提交对象。显然,这是一个循环往复的过程:</p><ol><li>从待打印的<code>commit</code>对象中,找出提交时间戳最大的一个;</li><li>打印它的消息;</li><li>将<code>commit</code>的所有父级提交加入到待打印的对象池中,回到第1个步骤;</li></ol><p>这个过程一直持续到没有待打印的<code>commit</code>对象为止,而所有待打印的<code>commit</code>对象组成了一个优先级队列——可以用一个大根堆来实现。</p><p>然而,我并不打算在这短短的演示当中真的去实现一个堆数据结构——我用插入排序来代替它。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MyGitLogPrinter</span>():</span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, *, commit_id: <span class="built_in">str</span>, n: <span class="built_in">int</span></span>) -> <span class="literal">None</span>:</span><br><span class="line"> <span class="variable language_">self</span>.commits: <span class="type">List</span>[CommitObject] = []</span><br><span class="line"> <span class="variable language_">self</span>.times = n</span><br><span class="line"></span><br><span class="line"> commit = CommitObject(commit_id=commit_id)</span><br><span class="line"> <span class="variable language_">self</span>._enqueue(commit)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">run</span>(<span class="params">self</span>):</span><br><span class="line"> i = <span class="number">0</span></span><br><span class="line"> <span class="keyword">while</span> <span class="built_in">len</span>(<span class="variable language_">self</span>.commits) > <span class="number">0</span> <span class="keyword">and</span> i < <span class="variable language_">self</span>.times:</span><br><span class="line"> commit = <span class="variable language_">self</span>.commits.pop(<span class="number">0</span>)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> parent_id <span class="keyword">in</span> commit.parents:</span><br><span class="line"> parent = CommitObject(commit_id=parent_id)</span><br><span class="line"> <span class="variable language_">self</span>._enqueue(parent)</span><br><span class="line"></span><br><span class="line"> <span class="built_in">print</span>(<span class="string">'{} {}'</span>.<span class="built_in">format</span>(commit.commit_id, commit.timestamp))</span><br><span class="line"> i += <span class="number">1</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">_enqueue</span>(<span class="params">self, commit: CommitObject</span>):</span><br><span class="line"> <span class="keyword">for</span> comm <span class="keyword">in</span> <span class="variable language_">self</span>.commits:</span><br><span class="line"> <span class="keyword">if</span> commit.commit_id == comm.commit_id:</span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line"> <span class="comment"># 插入排序,先找到一个待插入的下标,然后将从i到最后一个元素都往尾部移动,再将新节点插入下标i的位置。</span></span><br><span class="line"> i = <span class="number">0</span></span><br><span class="line"> <span class="keyword">while</span> i < <span class="built_in">len</span>(<span class="variable language_">self</span>.commits):</span><br><span class="line"> <span class="keyword">if</span> commit.timestamp > <span class="variable language_">self</span>.commits[i].timestamp:</span><br><span class="line"> <span class="keyword">break</span></span><br><span class="line"> i += <span class="number">1</span></span><br><span class="line"> <span class="variable language_">self</span>.commits = <span class="variable language_">self</span>.commits[<span class="number">0</span>:i] + [commit] + <span class="variable language_">self</span>.commits[i:]</span><br></pre></td></tr></table></figure><p>最后再提供一个启动函数就可以体验一番了</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@click.command()</span></span><br><span class="line"><span class="meta">@click.option(<span class="params"><span class="string">'--commit-id'</span>, required=<span class="literal">True</span></span>)</span></span><br><span class="line"><span class="meta">@click.option(<span class="params"><span class="string">'-n'</span>, default=<span class="number">20</span></span>)</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">cli</span>(<span class="params">commit_id: <span class="built_in">str</span>, n: <span class="built_in">int</span></span>):</span><br><span class="line"> MyGitLogPrinter(commit_id=commit_id, n=n).run()</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">'__main__'</span>:</span><br><span class="line"> cli()</span><br></pre></td></tr></table></figure><h2 id="真假美猴王对比"><a href="#真假美猴王对比" class="headerlink" title="真假美猴王对比"></a>真假美猴王对比</h2><p>为了看看上面的代码所打印出来的<code>commit</code>对象的顺序是否正确,我先将它的输出内容重定向到一个文件中</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">➜ gin git:(master) python3 ~/SourceCode/python/my_git_log/my_git_log.py --commit-id 'e38955615a14e567811e390c87afe705df957f3a' -n 20 > /tmp/my_git_log.txt</span><br></pre></td></tr></table></figure><p>再用<code>git-log</code>以同样的格式打印出来</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">➜ gin git:(master) git log --pretty='format:%H %ct' 'e38955615a14e567811e390c87afe705df957f3a' -n 20 > /tmp/git_log.txt</span><br></pre></td></tr></table></figure><p>最后让<code>diff</code>命令告诉我们这两个文件是否有差异</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">➜ gin git:(master) diff /tmp/git_log.txt /tmp/my_git_log.txt</span><br><span class="line">20c20</span><br><span class="line">< 2521d8246d9813d65700650b29e278a08823e3ae 1499266911</span><br><span class="line">\ No newline at end of file</span><br><span class="line">---</span><br><span class="line"><span class="meta prompt_">> </span><span class="language-bash">2521d8246d9813d65700650b29e278a08823e3ae 1499266911</span></span><br></pre></td></tr></table></figure><p>可以说是一模一样了。</p>]]></content>
<summary type="html"></summary>
</entry>
<entry>
<title>这方面Python还是比Lisp略逊一筹</title>
<link href="https://liutos.github.io/2021/08/12/%E8%BF%99%E6%96%B9%E9%9D%A2Python%E8%BF%98%E6%98%AF%E6%AF%94Lisp%E7%95%A5%E9%80%8A%E4%B8%80%E7%AD%B9/"/>
<id>https://liutos.github.io/2021/08/12/%E8%BF%99%E6%96%B9%E9%9D%A2Python%E8%BF%98%E6%98%AF%E6%AF%94Lisp%E7%95%A5%E9%80%8A%E4%B8%80%E7%AD%B9/</id>
<published>2021-08-12T15:08:37.000Z</published>
<updated>2021-08-12T15:08:47.000Z</updated>
<content type="html"><![CDATA[<h1 id="序言"><a href="#序言" class="headerlink" title="序言"></a>序言</h1><p>众所周知,Python 支持向函数传递关键字参数。比如 Python 的内置函数<code>max</code>就接受名为<code>key</code>的关键字参数,以决定如何获取比较两个参数时的依据</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">max</span>({<span class="string">'v'</span>: <span class="number">1</span>}, {<span class="string">'v'</span>: <span class="number">3</span>}, {<span class="string">'v'</span>: <span class="number">2</span>}, key=<span class="keyword">lambda</span> o: o[<span class="string">'v'</span>]) <span class="comment"># 返回值为{'v': 3}</span></span><br></pre></td></tr></table></figure><p>自定义一个运用了关键字参数特性的函数当然也不在话下。例如模仿一下 Common Lisp 中的函数<code>string-equal</code></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">string_equal</span>(<span class="params">string1, string2, *, start1=<span class="literal">None</span>, end1=<span class="literal">None</span>, start2=<span class="literal">None</span>, end2=<span class="literal">None</span></span>):</span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> start1:</span><br><span class="line"> start1 = <span class="number">0</span></span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> end1:</span><br><span class="line"> end1 = <span class="built_in">len</span>(string1) - <span class="number">1</span></span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> start2:</span><br><span class="line"> start2 = <span class="number">0</span></span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> end2:</span><br><span class="line"> end2 = <span class="built_in">len</span>(string2) - <span class="number">1</span></span><br><span class="line"> <span class="keyword">return</span> string1[start1:end1 + <span class="number">1</span>] == string2[start2:end2 + <span class="number">1</span>]</span><br></pre></td></tr></table></figure><p>再以关键字参数的形式向它传参</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">string_equal(<span class="string">"Hello, world!"</span>, <span class="string">"ello"</span>, start1=<span class="number">1</span>, end1=<span class="number">4</span>) <span class="comment"># 返回值为True</span></span><br></pre></td></tr></table></figure><p><del>秉承 Python 之禅中的<code>There should be one-- and preferably only one --obvious way to do it.</code>理念,</del> 我甚至可以花里胡哨地、用关键字参数的语法向<code>string1</code>和<code>string2</code>传参</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">string_equal(string1=<span class="string">'Goodbye, world!'</span>, string2=<span class="string">'ello'</span>) <span class="comment"># 返回值为False</span></span><br></pre></td></tr></table></figure><p>但瑜不掩瑕,Python 的关键字参数也有其不足。</p><h1 id="Python-的不足"><a href="#Python-的不足" class="headerlink" title="Python 的不足"></a>Python 的不足</h1><p>Python 的关键字参数特性的缺点在于,同一个参数无法同时以:</p><ol><li>具有自身的参数名,以及;</li><li>可以从<code>**kwargs</code>中取得,</li></ol><p>两种形态存在于参数列表中。</p><p>举个例子,我们都知道 Python 有一个知名的第三方库叫做 requests,提供了<del>用于开发爬虫牢底坐穿的</del>发起 HTTP 请求的功能。它的类<code>requests.Session</code>的实例方法<code>request</code>有着让人忍不住运用 Long Parameter List 对其重构的、长达 16 个参数的参数列表。(你可以移步<a href="https://docs.python-requests.org/en/latest/api/#requests.Session.request"><code>request</code>方法的文档</a>观摩)</p><p>为了便于使用,requests 的作者贴心地提供了<code>requests.request</code>,这样只需要一次简单的函数调用即可</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">requests.request(<span class="string">'GET'</span>, <span class="string">'http://example.com'</span>)</span><br></pre></td></tr></table></figure><p><code>requests.request</code>函数支持与<code>requests.Session#request</code>(请允许我借用 Ruby 对于实例方法的写法)相同的参数列表,这一切都是通过在参数列表中声明<code>**kwargs</code>变量,并在函数体中用相同的语法向后者传参来实现的。(你可以移步<a href="https://docs.python-requests.org/en/latest/_modules/requests/api/#request">request 函数的源代码</a>观摩)</p><p>这样的缺陷在于,<code>requests.request</code>函数的参数列表丢失了大量的信息。要想知道使用者能往<code>kwargs</code>中传入什么参数,必须:</p><ol><li>先知道<code>requests.request</code>是如何往<code>requests.Session#request</code>中传参的——将<code>kwargs</code>完全展开传入是最简单的情况;</li><li>再查看<code>requests.Session#request</code>的参数列表中排除掉<code>method</code>和<code>url</code>的部分剩下哪些参数。</li></ol><p>如果想在<code>requests.request</code>的参数列表中使用参数自身的名字(例如<code>params</code>、<code>data</code>、<code>json</code>等),那么调用<code>requests.Session#request</code>则变得繁琐起来,不得不写成</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">with</span> sessions.Session() <span class="keyword">as</span> session:</span><br><span class="line"> <span class="keyword">return</span> session.request(method=method, url=url, params=params, data=data, json=data, **kwargs)</span><br></pre></td></tr></table></figure><p>的形式——果然人类的本质是复读机。</p><p>一个优雅的解决方案,可以参考隔壁的 Common Lisp。</p><h1 id="Common-Lisp-的优越性"><a href="#Common-Lisp-的优越性" class="headerlink" title="Common Lisp 的优越性"></a>Common Lisp 的优越性</h1><p>Common Lisp 第一次面世是在1984年,比 Python 的1991年要足足早了7年。但据悉,Python 的关键字参数特性借鉴自 Modula-3,而不是<del>万物起源的</del> Lisp。Common Lisp 中的关键字参数特性与 Python 有诸多不同。例如,根据 Python 官方手册中的说法,<code>**kwargs</code>中只有多出来的关键字参数</p><blockquote><p>If the form “**identifier” is present, it is initialized to a new ordered mapping receiving any excess keyword arguments</p></blockquote><p>而在 Common Lisp 中,与<code>**kwargs</code>对应的是<code>&rest args</code>,它必须放置在关键字参数之前(即左边),并且根据 CLHS 中<a href="http://www.lispworks.com/documentation/lw51/CLHS/Body/03_dac.htm">《A specifier for a rest parameter》</a>的说法,<code>args</code>中含有所有未经处理的参数——也包含了位于其后的关键字参数</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defun</span> foobar (<span class="name">&rest</span> args <span class="symbol">&key</span> k1 k2)</span><br><span class="line"> (<span class="name">list</span> args k1 k2))</span><br><span class="line"></span><br><span class="line">(<span class="name">foobar</span> <span class="symbol">:k1</span> <span class="number">1</span> <span class="symbol">:k2</span> <span class="number">3</span>) <span class="comment">;; 返回值为((:K1 1 :K2 3) 1 3)</span></span><br></pre></td></tr></table></figure><p>如果我还有另一个函数与<code>foobar</code>有着相似的参数列表,那么也可以轻松将所有参数传递给它</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defun</span> foobaz (<span class="name">a</span> <span class="symbol">&rest</span> args <span class="symbol">&key</span> k1 k2)</span><br><span class="line"> (<span class="name">declare</span> (<span class="name">ignorable</span> k1 k2))</span><br><span class="line"> (<span class="name">cons</span> a</span><br><span class="line"> (<span class="name">apply</span> #'foobar args)))</span><br><span class="line"></span><br><span class="line">(<span class="name">foobaz</span> <span class="number">1</span> <span class="symbol">:k1</span> <span class="number">2</span> <span class="symbol">:k2</span> <span class="number">3</span>) <span class="comment">;; 返回值为(1 (:K1 2 :K2 3) 2 3)</span></span><br></pre></td></tr></table></figure><p>甚至于,即使在<code>foobaz</code>中支持的关键字参数比<code>foobar</code>要多,也能轻松地处理,因为 Common Lisp 支持向被调用的函数传入一个特殊的关键字参数<code>:allow-other-keys</code>即可</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defun</span> foobaz (<span class="name">a</span> <span class="symbol">&rest</span> args <span class="symbol">&key</span> k1 k2 my-key)</span><br><span class="line"> (<span class="name">declare</span> (<span class="name">ignorable</span> k1 k2))</span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"my-key is ~S~%"</span> my-key)</span><br><span class="line"> (<span class="name">cons</span> a</span><br><span class="line"> (<span class="name">apply</span> #'foobar <span class="symbol">:allow-other-keys</span> <span class="literal">t</span> args)))</span><br><span class="line"></span><br><span class="line">(<span class="name">foobaz</span> <span class="number">1</span> <span class="symbol">:k1</span> <span class="number">2</span> <span class="symbol">:k2</span> <span class="number">3</span> <span class="symbol">:my-key</span> <span class="number">4</span>) <span class="comment">;; 打印my-key is 4,并返回(1 (:ALLOW-OTHER-KEYS T :K1 2 :K2 3 :MY-KEY 4) 2 3)</span></span><br></pre></td></tr></table></figure><p>回到 HTTP 客户端的例子。在 Common Lisp 中我一般用<a href="https://edicl.github.io/drakma/">drakma</a>这个第三方库来发起 HTTP 请求,它导出了一个<code>http-request</code>函数,用法与<code>requests.request</code>差不多</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">drakma</span><span class="symbol">:http-request</span> <span class="string">"http://example.com"</span> <span class="symbol">:method</span> <span class="symbol">:get</span>)</span><br></pre></td></tr></table></figure><p>如果我想要基于它来封装一个便捷地发出 GET 请求的函数<code>http-get</code>的话,可以这样写</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defun</span> http-get (<span class="name">uri</span> <span class="symbol">&rest</span> args)</span><br><span class="line"> (<span class="name">apply</span> #'drakma<span class="symbol">:http-request</span> uri <span class="symbol">:method</span> <span class="symbol">:get</span> args))</span><br></pre></td></tr></table></figure><p>如果我希望在<code>http-get</code>的参数列表中直接暴露出一部分<code>http-request</code>支持的关键字参数的话,可以这样写</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defun</span> http-get (<span class="name">uri</span> <span class="symbol">&rest</span> args <span class="symbol">&key</span> content)</span><br><span class="line"> (<span class="name">declare</span> (<span class="name">ignorable</span> content))</span><br><span class="line"> (<span class="name">apply</span> #'drakma<span class="symbol">:http-request</span> uri <span class="symbol">:method</span> <span class="symbol">:get</span> args))</span><br></pre></td></tr></table></figure><p>更进一步,如果我想在<code>http-get</code>中支持解析<code>Content-Type</code>为<code>application/json</code>的响应结果的话,还可以这样写</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">ql</span><span class="symbol">:quickload</span> 'jonathan)</span><br><span class="line">(<span class="name">ql</span><span class="symbol">:quickload</span> 'str)</span><br><span class="line">(<span class="name">defun</span> http-get (<span class="name">uri</span> <span class="symbol">&rest</span> args <span class="symbol">&key</span> content (<span class="name">decode-json</span> <span class="literal">t</span>))</span><br><span class="line"> <span class="comment">;; http-request并不支持decode-json这个参数,但依然可以将整个args传给它。</span></span><br><span class="line"> (<span class="name">declare</span> (<span class="name">ignorable</span> content))</span><br><span class="line"> (<span class="name">multiple-value-bind</span> (<span class="name">bytes</span> code headers)</span><br><span class="line"> (<span class="name">apply</span> #'drakma<span class="symbol">:http-request</span> uri</span><br><span class="line"> <span class="symbol">:allow-other-keys</span> <span class="literal">t</span></span><br><span class="line"> <span class="symbol">:method</span> <span class="symbol">:get</span></span><br><span class="line"> args)</span><br><span class="line"> (<span class="name">declare</span> (<span class="name">ignorable</span> code))</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">content-type</span> (<span class="name">cdr</span> (<span class="name">assoc</span> <span class="symbol">:content-type</span> headers)))</span><br><span class="line"> (<span class="name">text</span> (<span class="name">flexi-streams</span><span class="symbol">:octets-to-string</span> bytes)))</span><br><span class="line"> (<span class="name">if</span> (<span class="name">and</span> decode-json</span><br><span class="line"> (<span class="name">str</span><span class="symbol">:starts-with-p</span> <span class="string">"application/json"</span> content-type))</span><br><span class="line"> (<span class="name">jonathan</span><span class="symbol">:parse</span> text)</span><br><span class="line"> text))))</span><br></pre></td></tr></table></figure><p>不愧是<del>Dio</del> Common Lisp,轻易就做到了我们做不到的事情。</p><h2 id="题外话"><a href="#题外话" class="headerlink" title="题外话"></a>题外话</h2><p>曾几何时,Python 程序员还会津津乐道于 Python 之禅中的<code>There should be one-- and preferably only one --obvious way to do it.</code>,但其实 Python 光是在定义一个函数的参数方面就有五花八门的写法了。甚至在写这篇文章的过程中,我才知道原来 Python 的参数列表中可以通过写上<code>/</code>来使其左侧的参数都成为 positional-only 的参数。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">foo1</span>(<span class="params">a, b</span>): <span class="keyword">pass</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">foo2</span>(<span class="params">a, /, b</span>): <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">foo1(a=<span class="number">1</span>, b=<span class="number">2</span>)</span><br><span class="line">foo2(a=<span class="number">1</span>, b=<span class="number">2</span>) <span class="comment"># 会抛出异常,因为a只能按位置来传参。</span></span><br></pre></td></tr></table></figure>]]></content>
<summary type="html"></summary>
</entry>
<entry>
<title>屠龙术——如何运用整洁架构</title>
<link href="https://liutos.github.io/2021/08/02/%E5%B1%A0%E9%BE%99%E6%9C%AF%E2%80%94%E2%80%94%E5%A6%82%E4%BD%95%E8%BF%90%E7%94%A8%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84/"/>
<id>https://liutos.github.io/2021/08/02/%E5%B1%A0%E9%BE%99%E6%9C%AF%E2%80%94%E2%80%94%E5%A6%82%E4%BD%95%E8%BF%90%E7%94%A8%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84/</id>
<published>2021-08-02T15:13:45.000Z</published>
<updated>2021-08-02T15:13:48.000Z</updated>
<content type="html"><![CDATA[<h1 id="序言"><a href="#序言" class="headerlink" title="序言"></a>序言</h1><p>或许是为了显摆,也或许是虚心学习,总之我在去年年初花了大约两个月读完了<a href="https://book.douban.com/subject/30333919/">《架构整洁之道》</a>。但读过后也仅仅就是读了而已,尽管书中描绘了一个名为整洁架构的软件架构,但我并没有理解并应用到实际的开发中去。书中的诸多理念最终都蛰伏在了我的脑海深处。</p><p>今年年初的时候我换了工作。新的单位给每人都配备了办公用的电脑,从此我也不用背着2公斤重的MacBook Pro通勤了。美中不足的地方是,我和<a href="https://github.com/Liutos/cuckoo">cuckoo</a>之间的联系被斩断了,因为<code>cuckoo</code>是个单机程序,要在私人电脑和办公电脑上各装一份太不方便了。于是乎,我决定开两个新的项目,将<code>cuckoo</code>拆分为客户端和服务端两部分。</p><p>正好,这给了我在实际的项目中践行整洁架构的机会。</p><h2 id="什么是整洁架构"><a href="#什么是整洁架构" class="headerlink" title="什么是整洁架构"></a>什么是整洁架构</h2><p>不像数学领域的概念往往有一个精确的定义,书中甚至没有道出整洁架构是什么。相对的,只有一副引人入胜的架构示意图(图片摘自作者博客的<a href="https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html">这篇文章</a>)</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210802/%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84.jpg"></p><p>在作者的<a href="https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html">文章</a>中,对图中的四个层次给出了响应的解释:</p><!-- 对Entities的解释:Entities封装了企业范围内的业务规则。如果你没有经营一个企业,仅仅是开发一款应用,那么Entities就是应用的业务对象,它们封装了应用内最通用、上层的规则。 --><!-- TODO: 将几个层的名字都用一对反引号包裹起来。 --><ul><li>Entities封装了企业范围内的业务规则。如果你没有经营一个企业,仅仅是开发一款应用,那么Entities就是应用的业务对象,它们封装了应用内最通用、上层的规则。</li></ul><!-- 对Use Cases的解释:Use Cases包含了与应用相关的业务规则。它封装并实现了系统的所有用例。 --><ul><li>Use Cases包含了与应用相关的业务规则。它封装并实现了系统的所有用例。</li></ul><!-- 衍生出来的问题:何谓用例(Use Cases)? --><!-- 对Interface Adapters的解释:这一层负责将最方便entities和use cases的数据转换为最方便外部系统使用的格式。在这一层以内都是抽象的,对外界诸如MVC、GUI、数据库等均是无感知的。此外,这一层也负责与外部服务通信。(可以举fledgling的repository目录下的例子) --><ul><li>这一层负责将最方便entities和use cases的数据转换为最方便外部系统使用的格式。在这一层以内都是抽象的,对外界诸如MVC、GUI、数据库等均是无感知的。此外,这一层也负责与外部服务通信。</li><li><code>Frameworks & Drivers</code>,顾名思义,这一层包含了与框架相关的代码,或者像C语言中的<code>main</code>函数这样的入口函数代码;</li></ul><!-- 有多少层不要紧,关键在于必须遵循依赖规则:在源代码层面,总是外层的依赖于内层的。例如,nest中use_case目录下总是依赖于entity目录、infra和repository依赖于entity目录、cli和web依赖于app、infra,以及repository等“内层”目录。 --><h1 id="如何应用整洁架构"><a href="#如何应用整洁架构" class="headerlink" title="如何应用整洁架构"></a>如何应用整洁架构</h1><!-- 这里给出我的实践心得:实践整洁架构的项目的简介、目录结构的划分、语言特性的运用、设计模式的运用、SOLID原则的体现、与MVC相比更为清晰的边界。除了这些通用的,还有一些具体的编码细节:参数的传递方式、抽象方法对比NotImplementedError、返回值对比传入Presenter、更新资源的用例的输入定义、为什么不要防御性编程、无法隐藏的I/O、类方法对比实例方法、避免循环依赖 --><h2 id="实际项目的例子"><a href="#实际项目的例子" class="headerlink" title="实际项目的例子"></a>实际项目的例子</h2><p>前文提到,为了满足新需求,我需要将<a href="https://github.com/Liutos/cuckoo">cuckoo</a>改造为C/S模型。但比起缓缓地将cuckoo拆解为两部分,我更乐于大刀阔斧地从头开发开发这两个程序,于是便诞生了:</p><ul><li>服务端程序为<a href="https://github.com/Liutos/nest">nest</a>,负责管理任务、计划等实体对象,并提供基于HTTP协议的API;</li><li>客户端程序为<a href="https://github.com/Liutos/fledgling">fledgling</a>,负责与<code>nest</code>通信,并在客户机上触发通知(如macOS的右上角弹出通知)。</li></ul><p>它们都是我依照自己对整洁架构的理解来编写的。</p><h2 id="从架构理念到具体决策"><a href="#从架构理念到具体决策" class="headerlink" title="从架构理念到具体决策"></a>从架构理念到具体决策</h2><p>正如<a href="https://en.wikipedia.org/wiki/Representational_state_transfer">REST</a>仅仅是一种软件结构风格而不是具体的设计指南一样,整洁架构也并没有规定示意图中的分层结构该如何运用一门语言的特性来实现,这需要开发者自己去摸索。下文我给出自己在<code>nest</code>和<code>fledgling</code>项目中的做法。</p><h3 id="如何安排代码目录结构"><a href="#如何安排代码目录结构" class="headerlink" title="如何安排代码目录结构"></a>如何安排代码目录结构</h3><p>在程序的代码结构中,最接近于架构示意图的分层架构的,当属代码仓库的目录结构了。模仿整洁架构中的四层结构,我在<code>nest</code>中也安排了相似的目录结构</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">(venv) ➜ nest git:(master) tree -I '__pycache__' -d ./nest</span><br><span class="line">./nest</span><br><span class="line">├── app</span><br><span class="line">│ ├── entity</span><br><span class="line">│ └── use_case</span><br><span class="line">├── cli</span><br><span class="line">│ ├── command</span><br><span class="line">│ └── config</span><br><span class="line">├── infra</span><br><span class="line">├── repository</span><br><span class="line">│ └── DDL</span><br><span class="line">└── web</span><br><span class="line"> ├── config</span><br><span class="line"> ├── controller</span><br><span class="line"> └── presenter</span><br><span class="line"></span><br><span class="line">13 directories</span><br></pre></td></tr></table></figure><h4 id="nest-app-entity-目录"><a href="#nest-app-entity-目录" class="headerlink" title="nest/app/entity/目录"></a><code>nest/app/entity/</code>目录</h4><p><code>nest/app/entity/</code>目录下的各个文件分别定义了系统中的各个实体类型</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">(venv) ➜ nest git:(master) ls nest/app/entity</span><br><span class="line">__pycache__ certificate.py location.py plan.py task.py user.py</span><br></pre></td></tr></table></figure><p>例如:</p><ul><li><code>task.py</code>中定义了类<code>Task</code>,表示一个任务;</li><li><code>plan.py</code>中定义了类<code>Plan</code>,表示任务的一次触发计划,等等。</li></ul><p><code>entity/</code>目录下的各个文件中,还定义了管理各种实体对象生命期的仓库对象,例如:</p><ul><li><code>task.py</code>中定义了类<code>ITaskRepository</code>,它负责增(<code>add</code>方法)删(<code>clear</code>、<code>remove</code>方法)查(<code>find</code>、<code>find_by_id</code>方法)改(同样是<code>add</code>方法)任务对象;</li><li><code>plan.py</code>中定义了类<code>IPlanRepository</code>,同样能够增(<code>add</code>方法)删(<code>clear</code>、<code>remove</code>方法)查(<code>find_as_queue</code>、<code>find_by_id</code>、<code>find_by_task_id</code>方法)改(同样是<code>add</code>方法)计划对象,等等。</li></ul><p>实体类型都是充血模型,它们实现了系统核心的业务规则,例如:</p><ul><li>类<code>Plan</code>有方法<code>is_repeated</code>用于检查是否为重复性任务;</li><li>有方法<code>is_visible</code>用于检查该计划在当前时间是否可见;</li><li>有方法<code>rebirth</code>用于生成一个新的、下一次触发的计划,等等。</li></ul><p>这个目录下的内容相当于整洁架构中的<code>Entities</code>层。</p><h4 id="nest-app-use-case-目录"><a href="#nest-app-use-case-目录" class="headerlink" title="nest/app/use_case/目录"></a><code>nest/app/use_case/</code>目录</h4><p><code>nest/app/use_case/</code>目录下的各个文件分别定义了系统所提供的功能</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">(venv) ➜ nest git:(master) ls nest/app/use_case</span><br><span class="line">__init__.py authenticate.py change_task.py create_plan.py delete_plan.py get_location.py get_task.py list_plan.py login.py registration.py</span><br><span class="line">__pycache__ change_plan.py create_location.py create_task.py delete_task.py get_plan.py list_location.py list_task.py pop_plan.py</span><br></pre></td></tr></table></figure><p>例如:</p><ul><li><code>authenticate.py</code>定义了系统如何认证发送当前请求的用户;</li><li><code>change_task.py</code>定义了系统如何修改一个任务对象,等等。</li></ul><p>每一个处于该目录下的文件,只会依赖<code>nest/app/entity/</code>中的代码,并且它们都是抽象的。例如,<code>authenticate.py</code>中的类<code>AuthenticateUseCase</code>的构造方法中,要求其:</p><ul><li>参数<code>certificate_repository</code>必须是类<code>ICertificateRepository</code>或其子类的实例;</li><li>参数<code>params</code>必须是类<code>IParams</code>或其子类的实例。</li></ul><p>然而<code>ICertificateRepository</code>和<code>IParams</code>其实都是抽象基类<code>ABC</code>的子类,并且它们都有被装饰器<code>abstractmethod</code>装饰的抽象方法,因此并不能直接实例化。</p><p>该目录相当于整洁架构中的<code>Use Cases</code>层。</p><h4 id="其它目录"><a href="#其它目录" class="headerlink" title="其它目录"></a>其它目录</h4><p>顾名思义,<code>cli</code>和<code>web</code>目录分别是与命令行程序、基于HTTP的API相关的代码,它们实现了处理来自命令行和HTTP协议的输入,以及打印到终端和返回HTTP响应的功能。<code>repository</code>目录下的各个文件实现了<code>entity</code>目录中各个抽象的仓库类的具体子类</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">(venv) ➜ nest git:(master) ls nest/repository</span><br><span class="line">DDL __init__.py __pycache__ certificate.py db_operation.py location.py plan.py task.py user.py</span><br></pre></td></tr></table></figure><p>例如:</p><ul><li><code>certificate.py</code>中实现了<code>entity/</code>目录下的同名文件中的抽象类<code>ICertificateRepository</code>——一个基于内存的子类<code>MemoryCertificateRepository</code>,以及一个基于Redis的子类<code>RedisCertificateRepository</code>;</li><li><code>location.py</code>中实现了<code>entity/</code>目录下的同名文件中的抽象类<code>ILocationRepository</code>——基于MySQL的子类<code>DatabaseLocationRepository</code>,等等。</li></ul><p>需要注意的是,除了<code>app</code>外的这些目录,并不能与整洁架构示意图中的外面两层严格对应起来。例如,尽管<code>cli</code>和<code>web</code>的名字一下子就让人认为它们处于<code>Frameworks & Drivers</code>层,但<code>web/presenter/</code>目录下的内容其实与框架并无联系。反倒是从命名上看处于<code>Interface Adapters</code>层的<code>web/controller/</code>目录,其中的代码依赖于<code>Flask</code>框架。</p><h3 id="如何往Use-Cases层传入数据"><a href="#如何往Use-Cases层传入数据" class="headerlink" title="如何往Use Cases层传入数据"></a>如何往<code>Use Cases</code>层传入数据</h3><p>在鲍勃大叔的文章中,提到了关于如何在层之间传递数据的原则</p><blockquote><p>Typically the data that crosses the boundaries is simple data structures. You can use basic structs or simple Data Transfer objects if you like. Or the data can simply be arguments in function calls. Or you can pack it into a hashmap, or construct it into an object.</p></blockquote><p>在<code>nest/app/use_case/</code>目录下的所有用例采用的都是这里提到的<code>construct it into an object</code>的方式。以<code>create_task.py</code>为例:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">IParams</span>(<span class="title class_ inherited__">ABC</span>):</span><br><span class="line"><span class="meta"> @abstractmethod</span></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">get_brief</span>(<span class="params">self</span>) -> <span class="built_in">str</span>:</span><br><span class="line"> <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"><span class="meta"> @abstractmethod</span></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">get_keywords</span>(<span class="params">self</span>) -> <span class="type">List</span>[<span class="built_in">str</span>]:</span><br><span class="line"> <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"><span class="meta"> @abstractmethod</span></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">get_user_id</span>(<span class="params">self</span>) -> <span class="built_in">int</span>:</span><br><span class="line"> <span class="keyword">pass</span></span><br></pre></td></tr></table></figure><ul><li>用内置模块<code>abc</code>中的抽象基类<code>ABC</code>、装饰器<code>abstractmethod</code>,以及类<code>CreateTaskUseCase</code>中的<code>assert</code>一起模拟类似Java中的<code>interface</code>的效果;</li><li>用方法而不是成员变量来获取不同的输入参数:<ul><li><code>get_brief</code>获取任务的简述;</li><li><code>get_keywords</code>获取关键字列表;</li><li><code>get_user_id</code>获取创建该任务的用户的ID。</li></ul></li></ul><p>聪明的盲生已经发现了华点:明明只需要在类<code>CreateTaskUseCase</code>的构造方法中定义<code>brief</code>、<code>keywords</code>,以及<code>user_id</code>三个参数即可,为什么要用方法这么麻烦呢?答案是因为方法更灵活。</p><p>当你采用构造方法参数的方案时,本质上是立了一个假设:</p><ol><li><del>在所有惯性系中,物理定律有相同的表达形式</del>先完成所有参数的获取;</li><li>再执行用例中的业务逻辑。</li></ol><p>如果是一个基于HTTP协议的API,那么这个假设是成立的——用户在客户端发送的HTTP请求到达服务端后,便无法再补充参数了。但有一种场景,用户能够在用例执行业务逻辑的过程中,持续地与应用交互,那便是命令行程序。</p><p>我在<code>fledgling</code>项目中给了一个用户在用例执行过程中,交互式地输入的例子。在文件<code>fledgling/app/use_case/delete_task.py</code>中,实现了删除指定任务的用例。它要求输入两个参数</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">IParams</span>(<span class="title class_ inherited__">ABC</span>):</span><br><span class="line"><span class="meta"> @abstractmethod</span></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">get_confirmation</span>(<span class="params">self</span>) -> <span class="built_in">bool</span>:</span><br><span class="line"> <span class="string">"""获取用户是否要删除该任务的确认。"""</span></span><br><span class="line"> <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"><span class="meta"> @abstractmethod</span></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">get_task_id</span>(<span class="params">self</span>) -> <span class="built_in">int</span>:</span><br><span class="line"> <span class="keyword">pass</span></span><br></pre></td></tr></table></figure><p>在文件<code>fledgling/cli/command/delete_task.py</code>中实现了<code>IParams</code>类的命令行形态。当没有从命令行参数中获取到任务的ID时,便会使用第三方库<code>PyInquirer</code>询问用户输入任务ID,并进一步确认</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Params</span>(<span class="title class_ inherited__">IParams</span>):</span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, *, task_id: <span class="type">Optional</span>[<span class="built_in">int</span>]</span>):</span><br><span class="line"> <span class="variable language_">self</span>.task_id = task_id</span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">get_confirmation</span>(<span class="params">self</span>) -> <span class="built_in">bool</span>:</span><br><span class="line"> <span class="keyword">if</span> <span class="variable language_">self</span>.task_id:</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">True</span></span><br><span class="line"> questions = [</span><br><span class="line"> {</span><br><span class="line"> <span class="string">'message'</span>: <span class="string">'确定删除该任务'</span>,</span><br><span class="line"> <span class="string">'name'</span>: <span class="string">'confirmation'</span>,</span><br><span class="line"> <span class="string">'type'</span>: <span class="string">'confirm'</span>,</span><br><span class="line"> }</span><br><span class="line"> ]</span><br><span class="line"> answers = prompt(questions)</span><br><span class="line"> <span class="keyword">return</span> answers[<span class="string">'confirmation'</span>]</span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">get_task_id</span>(<span class="params">self</span>) -> <span class="built_in">int</span>:</span><br><span class="line"> <span class="keyword">if</span> <span class="variable language_">self</span>.task_id:</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">self</span>.task_id</span><br><span class="line"> questions = [</span><br><span class="line"> {</span><br><span class="line"> <span class="string">'message'</span>: <span class="string">'输入要删除的任务的ID'</span>,</span><br><span class="line"> <span class="string">'name'</span>: <span class="string">'task_id'</span>,</span><br><span class="line"> <span class="string">'type'</span>: <span class="string">'input'</span>,</span><br><span class="line"> }</span><br><span class="line"> ]</span><br><span class="line"> answers = prompt(questions)</span><br><span class="line"> <span class="keyword">return</span> answers[<span class="string">'task_id'</span>]</span><br></pre></td></tr></table></figure><p>而这一切<del>煮不在乎</del><code>DeleteTaskUseCase</code>并不会感知到,它独立于用户界面。</p><h3 id="在哪一层维护业务规则"><a href="#在哪一层维护业务规则" class="headerlink" title="在哪一层维护业务规则"></a>在哪一层维护业务规则</h3><p>在《架构整洁之道》第20章中,鲍勃大叔给出了业务规则的定义</p><blockquote><p>Strictly speaking, business rules are rules or procedures that make or save<br>the business money. Very strictly speaking, these rules would make or save the business money, irrespective of whether they were implemented on a computer. They would make or save money even if they were executed manually.</p></blockquote><p>业务规则往往不是独立存在的,它们需要作用在一些数据上</p><blockquote><p>Critical Business Rules usually require some data to work with. For example, our loan requires a loan balance, an interest rate, and a payment schedule.</p></blockquote><p>而整洁架构中的实体就是包含了一部分业务规则及其操作的数据的对象。以<code>nest</code>中的计划实体为例,在类<code>Plan</code>中包含了几种业务规则——尽管这些规则不能为我赚钱或者省钱:</p><ul><li>一个计划的持续时长(如果有的话)不会是负的秒数——由<code>duration</code>的setter保障;</li><li>周期性计划必须指定周期——由<code>new</code>方法维护;</li><li>一个计划是重复的,当且仅当它有指定重复类型——由<code>is_repeated</code>方法维护;</li><li>一个计划是可见的,当且仅当它:<ul><li>要么没有指定可见的小时,要么当且时间处于指定的小时中,并且;</li><li>要么没有指定星期几可见,要么今天是指定的<code>weekday</code>——由<code>is_visible</code>方法维护。</li></ul></li></ul><p>但在整洁架构的示意图中,<code>Use Cases</code>层也是有维护规则的,它维护的是应用的业务规则(<code>Application Business Rules</code>)。与<code>Entities</code>层所维护的业务规则不同,<code>Use Cases</code>层的业务规则取决于应用提供的功能。例如,在<code>nest</code>项目修改一个计划的用例<code>ChangePlanUseCase</code>类的方法<code>run</code>中,会:</p><ol><li>检查指定的计划是否存在——显然,实体没法检查自己是否存在;</li><li>检查计划是否能被修改;</li><li>检查新的地点的ID是否指向真实存在的地点对象——显然,<code>Plan</code>对象不会去检查<code>Location</code>存在与否;</li></ol><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 文件nest/app/use_case/change_plan.py</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">ChangePlanUseCase</span>:</span><br><span class="line"> <span class="comment"># 省略__init__的定义</span></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">run</span>(<span class="params">self</span>):</span><br><span class="line"> <span class="comment"># 省略一些无关要紧的代码</span></span><br><span class="line"> params = <span class="variable language_">self</span>.params</span><br><span class="line"> plan_id = params.get_plan_id()</span><br><span class="line"> plan = <span class="variable language_">self</span>.plan_repository.find_by_id(plan_id)</span><br><span class="line"> <span class="keyword">if</span> plan <span class="keyword">is</span> <span class="literal">None</span>: <span class="comment"># 上面第1点</span></span><br><span class="line"> <span class="keyword">raise</span> PlanNotFoundError(plan_id=plan_id)</span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> plan.is_changeable(): <span class="comment"># 上面第2点</span></span><br><span class="line"> <span class="keyword">raise</span> UnchangeableError()</span><br><span class="line"></span><br><span class="line"> found, location_id = params.get_location_id()</span><br><span class="line"> <span class="keyword">if</span> found:</span><br><span class="line"> <span class="keyword">if</span> location_id:</span><br><span class="line"> location = <span class="variable language_">self</span>.location_repository.get(id_=location_id)</span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> location: <span class="comment"># 上面第3点</span></span><br><span class="line"> <span class="keyword">raise</span> LocationNotFoundError(location_id=location_id)</span><br><span class="line"> plan.location_id = location_id</span><br></pre></td></tr></table></figure><p>聪明的你一定发现了:<code>is_changeable</code>为什么不作为<code>Enterpries Business Rules</code>,在<code>Plan</code>对象内自行检查呢?答案是因为这样写更简单。</p><p>试想一下,如果要让<code>Plan</code>自己禁止在<code>is_changeable</code>为<code>False</code>时被修改,那么必须:</p><ul><li>先为所有可修改的属性设置setter;</li><li>在每一个setter中都调用<code>is_changeable</code>进行检查。</li></ul><p>之所以要这么做,是因为一个实体对象(在这里是指<code>Plan</code>的实例对象)是外部的时间流动是无感知的。它不知道外层(此处是<code>Use Cases</code>层)会<strong>先</strong>调用哪一个方法,<strong>后</strong>调用哪一个方法。因此,要想保持“终止状态的计划不能修改”,就必须在每一处setter都检查。</p><p>与之相反,在用例中有编排,因此它可以感知时间的流动。用例可以让<code>Plan</code>的<code>is_changeable</code>方法在其它任何方法之前被调用,因此免除了繁琐地在每一个setter中检查<code>is_changeable</code>的必要。</p><h3 id="如何获取Use-Cases层的处理结果"><a href="#如何获取Use-Cases层的处理结果" class="headerlink" title="如何获取Use Cases层的处理结果"></a>如何获取<code>Use Cases</code>层的处理结果</h3><p>正如往<code>Use Cases</code>层中输入参数可以采用:</p><ol><li>直接在<code>__init__</code>中传入对应类型的参数,或;</li><li>在<code>__init__</code>中传入一个能根据方法提取参数的对象。</li></ol><p>两种方案一样,获取<code>Use Cases</code>层的计算结果同样有两种方案:</p><ol><li>获取<code>run</code>方法的返回值,捕捉它的异常,或;</li><li>在<code>__init__</code>中传入一个能够接受不同结果并处理的对象。</li></ol><p>在<code>nest</code>这样的仅仅提供HTTP API的应用中,第1种方案便已经足够了。例如,在文件<code>nest/web/controller/create_plan.py</code>中,类<code>CreatePlanUseCase</code>的<code>run</code>方法的返回值为创建的计划对象,如果<code>run</code>调用成功,这个controller会借助于<code>PlanPresenter</code>,将计划对象转换为JSON对象格式的字符串,返回给调用方;如果调用失败,那么controller中也会捕捉异常(如<code>InvalidRepeatTypeError</code>)并以另一种格式返回给调用方。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">create_plan</span>(<span class="params">certificate_repository, repository_factory</span>):</span><br><span class="line"> <span class="comment"># 省略了不必要的代码</span></span><br><span class="line"> params = HTTPParams()</span><br><span class="line"> use_case = CreatePlanUseCase(</span><br><span class="line"> location_repository=repository_factory.location(),</span><br><span class="line"> params=params,</span><br><span class="line"> plan_repository=repository_factory.plan(),</span><br><span class="line"> task_repository=repository_factory.task(),</span><br><span class="line"> )</span><br><span class="line"> <span class="keyword">try</span>:</span><br><span class="line"> plan = use_case.run()</span><br><span class="line"> presenter = PlanPresenter(plan=plan)</span><br><span class="line"> <span class="keyword">return</span> { <span class="comment"># 成功的情形</span></span><br><span class="line"> <span class="string">'error'</span>: <span class="literal">None</span>,</span><br><span class="line"> <span class="string">'result'</span>: presenter.<span class="built_in">format</span>(),</span><br><span class="line"> <span class="string">'status'</span>: <span class="string">'success'</span>,</span><br><span class="line"> }, <span class="number">201</span></span><br><span class="line"> <span class="keyword">except</span> InvalidRepeatTypeError <span class="keyword">as</span> e: <span class="comment"># 失败的情形</span></span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> <span class="string">'error'</span>: {</span><br><span class="line"> <span class="string">'message'</span>: <span class="string">'不支持的重复类型:{}'</span>.<span class="built_in">format</span>(e.repeat_type),</span><br><span class="line"> },</span><br><span class="line"> <span class="string">'result'</span>: <span class="literal">None</span>,</span><br><span class="line"> <span class="string">'status'</span>: <span class="string">'failure'</span>,</span><br><span class="line"> }, <span class="number">422</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>如果想要更高的灵活性并且也有施展的空间,那么可以考虑第2种方案。例如<code>fledgling</code>项目中文件<code>fledgling/app/use_case/list_plan.py</code>中,就定义了一个接口<code>IPresenter</code></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">IPresenter</span>(<span class="title class_ inherited__">ABC</span>):</span><br><span class="line"><span class="meta"> @abstractmethod</span></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">on_find_location</span>(<span class="params">self</span>):</span><br><span class="line"> <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"><span class="meta"> @abstractmethod</span></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">on_find_task</span>(<span class="params">self</span>):</span><br><span class="line"> <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"><span class="meta"> @abstractmethod</span></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">on_invalid_location</span>(<span class="params">self, *, error: InvalidLocationError</span>):</span><br><span class="line"> <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"><span class="meta"> @abstractmethod</span></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">show_plans</span>(<span class="params">self, *, count: <span class="built_in">int</span>, plans: <span class="type">List</span>[Plan]</span>):</span><br><span class="line"> <span class="keyword">pass</span></span><br></pre></td></tr></table></figure><p>并且在用例的执行过程中,会多次向<code>self.presenter</code>传递数据</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">ListPlanUseCase</span>:</span><br><span class="line"> <span class="comment"># 省略__init__方法</span></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">run</span>(<span class="params">self</span>):</span><br><span class="line"> location_id = <span class="literal">None</span></span><br><span class="line"> location_name = <span class="variable language_">self</span>.params.get_location_name()</span><br><span class="line"> no_location = <span class="variable language_">self</span>.params.get_no_location()</span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> no_location <span class="keyword">and</span> location_name <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span>:</span><br><span class="line"> locations = <span class="variable language_">self</span>.location_repository.find(name=location_name)</span><br><span class="line"> <span class="keyword">if</span> <span class="built_in">len</span>(locations) == <span class="number">0</span>:</span><br><span class="line"> <span class="variable language_">self</span>.presenter.on_invalid_location(error=InvalidLocationError(name=location_name)) <span class="comment"># 第1次,触发无效地点的错误</span></span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line"></span><br><span class="line"> location_id = locations[<span class="number">0</span>].<span class="built_in">id</span></span><br><span class="line"></span><br><span class="line"> page = <span class="variable language_">self</span>.params.get_page()</span><br><span class="line"> per_page = <span class="variable language_">self</span>.params.get_per_page()</span><br><span class="line"> criteria = {</span><br><span class="line"> <span class="string">'page'</span>: page,</span><br><span class="line"> <span class="string">'per_page'</span>: per_page,</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> location_id <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span>:</span><br><span class="line"> criteria[<span class="string">'location_id'</span>] = location_id</span><br><span class="line"> plans, count = <span class="variable language_">self</span>.plan_repository.<span class="built_in">list</span>(**criteria)</span><br><span class="line"> location_ids = [plan.location_id <span class="keyword">for</span> plan <span class="keyword">in</span> plans]</span><br><span class="line"> <span class="variable language_">self</span>.presenter.on_find_location() <span class="comment"># 第2次交互,通知presenter开始查找地点的事件</span></span><br><span class="line"> locations = <span class="variable language_">self</span>.location_repository.find(</span><br><span class="line"> ids=location_ids,</span><br><span class="line"> page=<span class="number">1</span>,</span><br><span class="line"> per_page=<span class="built_in">len</span>(location_ids),</span><br><span class="line"> )</span><br><span class="line"> task_ids = [plan.task_id <span class="keyword">for</span> plan <span class="keyword">in</span> plans]</span><br><span class="line"> <span class="variable language_">self</span>.presenter.on_find_task() <span class="comment"># 第3次交互,通知presenter开始查找任务的事件</span></span><br><span class="line"> tasks = <span class="variable language_">self</span>.task_repository.<span class="built_in">list</span>(</span><br><span class="line"> page=<span class="number">1</span>,</span><br><span class="line"> per_page=<span class="built_in">len</span>(task_ids),</span><br><span class="line"> task_ids=task_ids,</span><br><span class="line"> )</span><br><span class="line"> <span class="keyword">for</span> plan <span class="keyword">in</span> plans:</span><br><span class="line"> location_id = plan.location_id</span><br><span class="line"> location = [location <span class="keyword">for</span> location <span class="keyword">in</span> locations <span class="keyword">if</span> location.<span class="built_in">id</span> == location_id][<span class="number">0</span>]</span><br><span class="line"> plan.location = location</span><br><span class="line"> task_id = plan.task_id</span><br><span class="line"> task = [task <span class="keyword">for</span> task <span class="keyword">in</span> tasks <span class="keyword">if</span> task.<span class="built_in">id</span> == task_id][<span class="number">0</span>]</span><br><span class="line"> plan.task = task</span><br><span class="line"></span><br><span class="line"> <span class="comment"># 第4次,也是最后一次,传入用例的处理结果</span></span><br><span class="line"> <span class="variable language_">self</span>.presenter.show_plans(</span><br><span class="line"> count=count,</span><br><span class="line"> plans=plans,</span><br><span class="line"> )</span><br><span class="line"> <span class="keyword">return</span></span><br></pre></td></tr></table></figure><p>在构造方法中注入<code>presenter</code>的缺点在于用例的<code>run</code>方法中需要显式地<code>return</code>,否则用例会继续执行下去。</p><h3 id="Python语言特性的运用"><a href="#Python语言特性的运用" class="headerlink" title="Python语言特性的运用"></a>Python语言特性的运用</h3><h4 id="模拟接口——abstractmethodv-s-NotImplementedError"><a href="#模拟接口——abstractmethodv-s-NotImplementedError" class="headerlink" title="模拟接口——abstractmethodv.s.NotImplementedError"></a>模拟接口——<code>abstractmethod</code>v.s.<code>NotImplementedError</code></h4><p>整洁架构的每一层都只会依赖于内层,而内层又对外层一无所知,负责解耦两者的便是编程语言的接口特性。但Python并不像Java那般有<code>interface</code>关键字,因此我利用它的其它一系列特性来模拟出接口:</p><ul><li>用<code>class</code>代替<code>interface</code>,这些类继承自内置模块<code>abc</code>的抽象基类<code>ABC</code>;</li><li>除此之外,这些类中的方法还用同一模块中的<code>abstractmethod</code>装饰,使它们必须由该类的子类全部定义;</li><li>在使用这个接口的位置(例如<code>Use Cases</code>层)用断言<code>assert</code>约束输入参数的类型。</li></ul><p><code>nest</code>中的大部分需要接口的位置我都是用这种手法来做的,但这种方式会给编写单元测试用例带来一些不便:</p><ol><li>因为代码中用<code>assert</code>来检查参数类型,导致传入的参数只能是这个接口或其子类的实例;</li><li>因为接口类继承自<code>ABC</code>,所以必须定义所有被<code>abstractmethod</code>装饰的方法,否则在实例化时就会抛出异常。</li></ol><p>例如,在<code>nest</code>项目的文件<code>tests/use_case/task/test_list.py</code>中,作为白盒测试的人员,我确切地知道类<code>ListTaskUseCase</code>的<code>run</code>方法只会调用它的<code>task_repository</code>的<code>find</code>方法,但在类<code>MockTaskRepository</code>中依然不得不定义基类的每一个方法——尽管它们只有一行<code>pass</code>语句。</p><p>如果愿意放弃一点点的严谨性,那么可以弱化一下上面的接口方案:</p><ol><li>不使用<code>abstractmethod</code>,而是在本应为抽象方法的方法中只留下一句<code>raise NotImplementedError</code>;</li><li>不使用<code>assert</code>检查类型,而是在参数中写上type hint。</li></ol><p>有了第1点,那么在测试用例中就不需要为测试路径上不会调用的方法写多余的定义了。而有了第2点,也就不需要为测试路径上不会引用的属性创建对象了,大可直接传入一个<code>None</code>。选择哪一种都无妨,取决于开发者或团队的口味。</p><h2 id="金坷垃整洁架构的好处都有啥"><a href="#金坷垃整洁架构的好处都有啥" class="headerlink" title="金坷垃整洁架构的好处都有啥"></a><del>金坷垃</del>整洁架构的好处都有啥</h2><p>在《架构整洁之道》的第20章,作者给出了整洁架构的五种优秀特性:</p><ul><li>独立于框架。例如,我可以花不是很大的力气,将<code>nest</code>从<a href="https://flask.palletsprojects.com/en/2.0.x/">Flask</a>迁移到<a href="http://bottlepy.org/docs/dev/">Bottle</a>上,尽管并不会无缘无故或频繁地这么做;</li><li>容易测试。例如,在<code>nest</code>项目的目录<code>tests/use_case</code>下的测试用例不需要有任何外部系统的依赖就可以编写并运行;</li><li>独立于用户界面。例如,在<code>nest</code>项目中同一个用例<code>RegistrationUseCase</code>就有HTTP API和命令行两种用户界面:<ul><li>在文件<code>nest/web/controller/registration.py</code>中是HTTP API形态;</li><li>在文件<code>nest/cli/command/register.py</code>中则是命令行形态。</li></ul></li><li>独立于数据库。例如,就像更换Web框架一样,我也可以从MySQL迁移到PostgreSQL中,这对于<code>Entities</code>和<code>Use Cases</code>层的代码而言别无二致;</li><li>独立于外部系统。例如,在<code>fledgling</code>项目中,尽管也定义了一个接口<code>ITaskRepository</code>,但不同于<code>nest</code>中基于数据库的实现子类<code>DatabaseTaskRepository</code>,在<code>fledgling</code>中实现的是基于网络传输的类<code>TaskRepository</code>。但究竟是基于单机数据库,还是身处一个分布式系统(C/S模型)中,<code>Entities</code>和<code>Use Cases</code>层对此是无感知的。</li></ul><h2 id="甘瓜苦蒂——整洁架构的不足"><a href="#甘瓜苦蒂——整洁架构的不足" class="headerlink" title="甘瓜苦蒂——整洁架构的不足"></a>甘瓜苦蒂——整洁架构的不足</h2><h3 id="渗入内层的I-O"><a href="#渗入内层的I-O" class="headerlink" title="渗入内层的I/O"></a>渗入内层的I/O</h3><!-- 无法隐藏的I/O --><!-- 在整洁架构中,为了不在entity层面感知到数据库,但又为了可以最终利用到数据库的事务能力,发明了基于start_transaction/commit/rollback的抽象方法。其中,当需要多个repository联合使用事务时,可以将多个repository传入到start_transaction的参数中。 --><!-- 设计模式的运用 --><!-- Python的module v.s. 单例模式 --><!-- repository.py中的工厂方法模式 --><!-- SOLID原则的体现 --><!-- 书中是如何理解SOLID原则的,给出每一个对应章节的解释。 --><!-- 不同编程语言实践整洁架构的体会 --><!-- Python如何 --><!-- CL如何 --><!-- go如何 -->]]></content>
<summary type="html"></summary>
</entry>
<entry>
<title>模拟小于运算符的短路特性</title>
<link href="https://liutos.github.io/2021/06/26/%E6%A8%A1%E6%8B%9F%E5%B0%8F%E4%BA%8E%E8%BF%90%E7%AE%97%E7%AC%A6%E7%9A%84%E7%9F%AD%E8%B7%AF%E7%89%B9%E6%80%A7/"/>
<id>https://liutos.github.io/2021/06/26/%E6%A8%A1%E6%8B%9F%E5%B0%8F%E4%BA%8E%E8%BF%90%E7%AE%97%E7%AC%A6%E7%9A%84%E7%9F%AD%E8%B7%AF%E7%89%B9%E6%80%A7/</id>
<published>2021-06-26T03:02:53.000Z</published>
<updated>2022-05-02T03:18:50.000Z</updated>
<content type="html"><![CDATA[<p><del>忆往昔峥嵘岁月稠</del>在Python的语言标准的<a href="https://docs.python.org/3/reference/expressions.html#comparisons">Comparisions章节</a>中提到</p><blockquote><p>Also unlike C, expressions like a < b < c have the interpretation that is conventional in mathematics</p></blockquote><p>也就是说,在C语言中要写成<code>a < b && b < c</code>的表达式,在Python中可以写成<code>a < b < c</code>。并且,标准中还提到</p><blockquote><p>Comparisons can be chained arbitrarily, e.g., x < y <= z is equivalent to x < y and y <= z, except that y is evaluated only once (but in both cases z is not evaluated at all when x < y is found to be false).</p></blockquote><p>一般将这种性质成为短路。因此,像<code>2 < 1 < (1 / 0)</code>这样的表达式在Python中不会引发异常,而是返回<code>False</code>。</p><p>Python的小于号能拥有短路特性,是因为它并非一个普通函数,而是有语言层面加持的操作符。而在Common Lisp(下称CL)中,小于号仅仅是一个普通函数,就像Haskell中的小于号也是一个函数一般。不同的是,CL的小于号能接受多于两个的参数</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name"><</span> <span class="number">1</span> <span class="number">2</span> <span class="number">3</span> <span class="number">-1</span>) <span class="comment">; 结果为NIL</span></span><br></pre></td></tr></table></figure><p>但它并没有短路特性</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name"><</span> <span class="number">1</span> <span class="number">2</span> <span class="number">3</span> <span class="number">-1</span> (<span class="name">/</span> <span class="number">1</span> <span class="number">0</span>)) <span class="comment">; 引发名为DIVISION-BY-ZERO的错误</span></span><br></pre></td></tr></table></figure><p>要想模拟出具有短路特性的小于号,必须借助于宏的力量。</p><h1 id="想生成什么样的代码"><a href="#想生成什么样的代码" class="headerlink" title="想生成什么样的代码"></a>想生成什么样的代码</h1><p>要想写出一个宏,必须先设想出它的语法,以及它会展开成什么样的代码。姑且为这个宏起名为<code>less-than</code>,它的语法应当为</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defmacro</span> less-than (<span class="name">form</span> <span class="symbol">&rest</span> more-forms)</span><br><span class="line"> <span class="comment">; TBC</span></span><br><span class="line"> )</span><br></pre></td></tr></table></figure><p>至于它的展开结果可以有多种选择。例如,可以<code>(less-than 2 1 (/ 1 0))</code>展开为自身具有短路特性的<code>and</code>形式</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">and</span> (<span class="name"><</span> <span class="number">2</span> <span class="number">1</span>) (<span class="name"><</span> <span class="number">1</span> (<span class="name">/</span> <span class="number">1</span> <span class="number">0</span>)))</span><br></pre></td></tr></table></figure><p>但就像在C语言中用宏朴素地实现计算二者最大值的<code>MAX</code>宏一样,上面的展开方式在一些情况下会招致重复求值</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">less-than</span> <span class="number">1</span> (<span class="name">progn</span> (<span class="name">print</span> 'hello) <span class="number">2</span>) <span class="number">3</span>)</span><br></pre></td></tr></table></figure><p>因此,起码要展开为<code>and</code>和<code>let</code>的搭配</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">let</span> ((<span class="name">g917</span> <span class="number">1</span>)</span><br><span class="line"> (<span class="name">g918</span> (<span class="name">progn</span> (<span class="name">print</span> 'hello) <span class="number">2</span>)))</span><br><span class="line"> (<span class="name">and</span> (<span class="name"><</span> g917 g918)</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">g919</span> <span class="number">3</span>))</span><br><span class="line"> (<span class="name"><</span> g918 g919))))</span><br></pre></td></tr></table></figure><p>要想展开为这种结构,可以如这般实现<code>less-than</code></p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defmacro</span> less-than (<span class="name">form</span> <span class="symbol">&rest</span> more-forms)</span><br><span class="line"> (<span class="name">labels</span> ((<span class="name">aux</span> (<span class="name">lhs</span> forms)</span><br><span class="line"> <span class="string">"LHS表示紧接着下一次要比较的、小于号的左操作数。"</span></span><br><span class="line"> (<span class="name">unless</span> forms</span><br><span class="line"> (<span class="name">return-from</span> aux))</span><br><span class="line"> (<span class="name">let*</span> ((<span class="name">rhs</span> (<span class="name">gensym</span>))</span><br><span class="line"> (<span class="name">rv</span> (<span class="name">aux</span> rhs (<span class="name">rest</span> forms))))</span><br><span class="line"> (<span class="name">if</span> rv</span><br><span class="line"> `(let ((,rhs ,(first forms)))</span><br><span class="line"> (and (< ,lhs ,rhs)</span><br><span class="line"> ,rv))</span><br><span class="line"> `(< ,lhs ,(first forms))))))</span><br><span class="line"> (<span class="name">cond</span> ((<span class="name">null</span> more-forms)</span><br><span class="line"> `(< ,form))</span><br><span class="line"> (<span class="name">t</span></span><br><span class="line"> (<span class="name">let</span> ((<span class="name">lhs</span> (<span class="name">gensym</span>)))</span><br><span class="line"> `(let ((,lhs ,form))</span><br><span class="line"> ,(aux lhs more-forms)))))))</span><br></pre></td></tr></table></figure><p>用上面的输入验证一下是否会导致重复求值</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">CL-USER> (<span class="name">macroexpand-1</span> '(less-than <span class="number">1</span> (progn (print 'hello) <span class="number">2</span>) <span class="number">3</span>))</span><br><span class="line">(<span class="name">LET</span> ((<span class="name">#</span><span class="symbol">:G942</span> <span class="number">1</span>))</span><br><span class="line"> (<span class="name">LET</span> ((<span class="name">#</span><span class="symbol">:G943</span> (<span class="name">PROGN</span> (<span class="name">PRINT</span> 'HELLO) <span class="number">2</span>)))</span><br><span class="line"> (<span class="name">AND</span> (<span class="name"><</span> #<span class="symbol">:G942</span> #<span class="symbol">:G943</span>) (<span class="name"><</span> #<span class="symbol">:G943</span> <span class="number">3</span>))))</span><br><span class="line">T</span><br></pre></td></tr></table></figure><h1 id="优化一下"><a href="#优化一下" class="headerlink" title="优化一下"></a>优化一下</h1><p>显然<code>less-than</code>可以优化,只需要简单地运用递归的技巧即可</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defmacro</span> less-than (<span class="name">form</span> <span class="symbol">&rest</span> more-forms)</span><br><span class="line"> (<span class="name">cond</span> ((<span class="name"><=</span> (<span class="name">length</span> more-forms) <span class="number">1</span>)</span><br><span class="line"> `(< ,form ,@more-forms))</span><br><span class="line"> (<span class="name">t</span></span><br><span class="line"> (<span class="name">let</span> ((<span class="name">lhs</span> (<span class="name">gensym</span>))</span><br><span class="line"> (<span class="name">rhs</span> (<span class="name">gensym</span>)))</span><br><span class="line"> `(let ((,lhs ,form)</span><br><span class="line"> (,rhs ,(first more-forms)))</span><br><span class="line"> (and (< ,lhs ,rhs)</span><br><span class="line"> (less-than ,rhs ,@(rest more-forms))))))))</span><br></pre></td></tr></table></figure><p>展开后的代码简短得多</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">CL-USER> (<span class="name">macroexpand-1</span> '(less-than <span class="number">1</span> (progn (print 'hello) <span class="number">2</span>) <span class="number">3</span>))</span><br><span class="line">(<span class="name">LET</span> ((<span class="name">#</span><span class="symbol">:G955</span> <span class="number">1</span>) (<span class="name">#</span><span class="symbol">:G956</span> (<span class="name">PROGN</span> (<span class="name">PRINT</span> 'HELLO) <span class="number">2</span>)))</span><br><span class="line"> (<span class="name">AND</span> (<span class="name"><</span> #<span class="symbol">:G955</span> #<span class="symbol">:G956</span>) (<span class="name">LESS-THAN</span> #<span class="symbol">:G956</span> <span class="number">3</span>)))</span><br><span class="line">T</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html"></summary>
<category term="实战CommonLisp" scheme="https://liutos.github.io/categories/%E5%AE%9E%E6%88%98CommonLisp/"/>
<category term="common lisp" scheme="https://liutos.github.io/tags/common-lisp/"/>
<category term="lisp" scheme="https://liutos.github.io/tags/lisp/"/>
<category term="宏" scheme="https://liutos.github.io/tags/%E5%AE%8F/"/>
<category term="macro" scheme="https://liutos.github.io/tags/macro/"/>
</entry>
<entry>
<title>从AOP到method-combination</title>
<link href="https://liutos.github.io/2021/06/14/%E4%BB%8EAOP%E5%88%B0method-combination/"/>
<id>https://liutos.github.io/2021/06/14/%E4%BB%8EAOP%E5%88%B0method-combination/</id>
<published>2021-06-14T12:28:28.000Z</published>
<updated>2021-06-14T12:30:38.000Z</updated>
<content type="html"><![CDATA[<blockquote><p>“实战Elisp”系列旨在讲述我使用Elisp定制Emacs的经验,抛砖引玉,还请广大Emacs同好不吝赐教——如果真的有广大Emacs用户的话,哈哈哈。</p></blockquote><p>Emacs的org-mode用的是一门叫Org的标记语言,正如大部分的标记语言那样,它也支持无序列表和检查清单——前者以<code>- </code>(一个连字符、一个空格)为前缀,后者以<code>- [ ] </code>或<code>- [x] </code>为前缀(比无序列表多了一对方括号及中间的字母<code>x</code>)</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210614/org%E6%A8%A1%E5%BC%8F%E4%B8%ADcheckbox%E7%9A%84%E8%AF%AD%E6%B3%95.jpg"></p><p>此外,org-mode还为编辑这两种列表提供了快速插入新一行的快捷键<code>M-RET</code>(即按住<code>alt</code>键并按下回车键)。如果光标位于无序列表中,那么新的一行将会自动插入<code>- </code>前缀。遗憾的是,如果光标位于检查清单中,那么新一行并没有自动插入一对方括号</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210614/%E5%9C%A8Emacs%E4%B8%AD%E7%94%A8alt%E5%9B%9E%E8%BD%A6%E6%B7%BB%E5%8A%A0%E4%B8%80%E8%A1%8C.gif"></p><p>每次都要手动敲入<code>[ ] </code>还挺繁琐的。好在这是Emacs,它是可扩展的、可定制的。只需敲几行代码,就可以让Emacs代劳输入方括号了。</p><h1 id="Emacs的AOP特性——advice-add"><a href="#Emacs的AOP特性——advice-add" class="headerlink" title="Emacs的AOP特性——advice-add"></a>Emacs的AOP特性——<code>advice-add</code></h1><p>借助Emacs的<code>describe-key</code>功能,可以知道在一个<code>org-mode</code>的文件中按下<code>M-RET</code>时,Emacs会调用到函数<code>org-insert-item</code>上。要想让<code>M-RET</code>实现自动追加方括号的效果,马上可以想到简单粗暴的办法:</p><ul><li>定义一个新的函数,并将<code>M-RET</code>绑定到它身上;</li><li>重新定义<code>org-insert-item</code>函数,使其追加方括号;</li></ul><p>但不管是上述的哪一种,都需要连带着重新实现插入连字符、空格前缀的已有功能。有一种更温和的办法可以在现有的<code>org-insert-item</code>的基础上扩展它的行为,那就是Emacs的<code>advice</code>特性。</p><p><code>advice</code>是面向切面编程范式的一种,使用Emacs的<code>advice-add</code>函数,可以在一个普通的函数被调用前或被调用后捎带做一些事情——比如追加一对方括号。对于这两个时机,分别可以直接用<code>advice-add</code>的<code>:before</code>和<code>:after</code>来实现,但用在这里都不合适,因为:</p><ul><li>检测是否位于检查清单中,需要在调用<code>org-insert-item</code>前做;</li><li>追加一对方括号,则需要在<code>org-insert-item</code>之后做。</li></ul><p>因此,正确的做法是使用<code>:around</code>来修饰原始的<code>org-insert-item</code>函数</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">cl-defun</span> lt-around-org-insert-item (<span class="name">oldfunction</span> <span class="symbol">&rest</span> args)</span><br><span class="line"> <span class="string">"在调用了org-insert-item后识时务地追加 [ ]这样的内容。"</span></span><br><span class="line"> (<span class="name">let</span> ((<span class="name">is-checkbox</span> <span class="literal">nil</span>)</span><br><span class="line"> (<span class="name">line</span> (<span class="name">buffer-substring-no-properties</span> (<span class="name">line-beginning-position</span>) (<span class="name">line-end-position</span>))))</span><br><span class="line"> <span class="comment">;; 检查当前行是否为checkbox</span></span><br><span class="line"> (<span class="name">when</span> (<span class="name">string-match-p</span> <span class="string">"- \\[.\\]"</span> line)</span><br><span class="line"> (<span class="name">setf</span> is-checkbox <span class="literal">t</span>))</span><br><span class="line"> <span class="comment">;; 继续使用原来的org-insert-item插入文本</span></span><br><span class="line"> (<span class="name">apply</span> oldfunction args)</span><br><span class="line"> <span class="comment">;; 决定要不要追加“ [ ]”字符串</span></span><br><span class="line"> (<span class="name">when</span> is-checkbox</span><br><span class="line"> (<span class="name">insert</span> <span class="string">"[ ] "</span>))))</span><br><span class="line"></span><br><span class="line">(<span class="name">advice-add</span> 'org-insert-item <span class="symbol">:around</span> #'lt-around-org-insert-item)</span><br></pre></td></tr></table></figure><p>这下子,<code>M-RET</code>对检查清单也一视同仁了</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210614/advice%E5%90%8E%E7%9A%84%E6%95%88%E6%9E%9C.gif"></p><h1 id="Common-Lisp的method-combination"><a href="#Common-Lisp的method-combination" class="headerlink" title="Common Lisp的method combination"></a>Common Lisp的<code>method combination</code></h1><p><code>advice-add</code>的<code>:after</code>、<code>:around</code>,以及<code>:before</code>在Common Lisp中有着完全同名的等价物,只不过不是用一个叫<code>advice-add</code>的函数,而是喂给一个叫<code>defmethod</code>的宏。举个例子,用<code>defmethod</code>可以定义出一个多态的<code>len</code>函数,对不同类型的入参执行不同的逻辑</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defgeneric</span> len (<span class="name">x</span>))</span><br><span class="line"></span><br><span class="line">(<span class="name">defmethod</span> len ((<span class="name">x</span> string))</span><br><span class="line"> (<span class="name">length</span> x))</span><br><span class="line"></span><br><span class="line">(<span class="name">defmethod</span> len ((<span class="name">x</span> hash-table))</span><br><span class="line"> (<span class="name">hash-table-count</span> x))</span><br></pre></td></tr></table></figure><p>然后为其中参数类型为字符串的特化版本定义对应的<code>:after</code>、<code>:around</code>,以及<code>:before</code>修饰过的方法</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defmethod</span> len <span class="symbol">:after</span> ((<span class="name">x</span> string))</span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"after len~%"</span>))</span><br><span class="line"></span><br><span class="line">(<span class="name">defmethod</span> len <span class="symbol">:around</span> ((<span class="name">x</span> string))</span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"around中调用len前~%"</span>)</span><br><span class="line"> (<span class="name">prog1</span></span><br><span class="line"> (<span class="name">call-next-method</span>)</span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"around中调用len后~%"</span>)))</span><br><span class="line"></span><br><span class="line">(<span class="name">defmethod</span> len <span class="symbol">:before</span> ((<span class="name">x</span> string))</span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"before len~%"</span>))</span><br></pre></td></tr></table></figure><p>这一系列方法的调用规则为:</p><ol><li>先调用<code>:around</code>修饰的方法;</li><li>由于上述方法中调用了<code>call-next-method</code>,因此再调用<code>:before</code>修饰的方法;</li><li>调用不加修饰的方法(在CL中这称为<code>primary</code>方法);</li><li>再调用<code>:after</code>修饰的方法;</li><li>最后,又回到了<code>:around</code>中调用<code>call-next-method</code>的位置。</li></ol><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210614/%E5%AE%9E%E9%99%85%E6%95%88%E6%9E%9C.jpg"></p><p>咋看之下,Emacs的<code>advice-add</code>支持的修饰符要多得多,实则不然。在CL中,<code>:after</code>、<code>:around</code>,以及<code>:before</code>同属于一个名为<code>standard</code>的<code>method combination</code>,而CL还内置了其它的<code>method combination</code>。在<a href="https://lispcookbook.github.io/cl-cookbook/clos.html#method-qualifiers-before-after-around">《Other method combinations》</a>一节中,作者演示了<code>progn</code>和<code>list</code>的例子。</p><p>如果想要模拟Emacs的<code>advice-add</code>所支持的其它修饰符,那么就必须定义新的<code>method combination</code>了。</p><h1 id="可编程的编程语言——define-method-combination"><a href="#可编程的编程语言——define-method-combination" class="headerlink" title="可编程的编程语言——define-method-combination"></a>可编程的编程语言——<code>define-method-combination</code></h1><p>曾经我以为,<code>defmethod</code>只能接受<code>:after</code>、<code>:around</code>,以及<code>:before</code>,认为这三个修饰符是必须在语言一级支持的特性。直到有一天我闯入了LispWorks的<a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_defi_4.htm">define-method-combination</a>词条中,才发现它们也是三个平凡的修饰符而已。</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">define-method-combination</span> standard ()</span><br><span class="line"> ((<span class="name">around</span> (<span class="symbol">:around</span>))</span><br><span class="line"> (<span class="name">before</span> (<span class="symbol">:before</span>))</span><br><span class="line"> (<span class="name">primary</span> () <span class="symbol">:required</span> <span class="literal">t</span>)</span><br><span class="line"> (<span class="name">after</span> (<span class="symbol">:after</span>)))</span><br><span class="line"> (<span class="name">flet</span> ((<span class="name">call-methods</span> (<span class="name">methods</span>)</span><br><span class="line"> (<span class="name">mapcar</span> #'(lambda (method)</span><br><span class="line"> `(call-method ,method))</span><br><span class="line"> methods)))</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">form</span> (<span class="name">if</span> (<span class="name">or</span> before after (<span class="name">rest</span> primary))</span><br><span class="line"> `(multiple-value-prog1</span><br><span class="line"> (progn ,@(call-methods before)</span><br><span class="line"> (call-method ,(first primary)</span><br><span class="line"> ,(rest primary)))</span><br><span class="line"> ,@(call-methods (reverse after)))</span><br><span class="line"> `(call-method ,(first primary)))))</span><br><span class="line"> (<span class="name">if</span> around</span><br><span class="line"> `(call-method ,(first around)</span><br><span class="line"> (,@(rest around)</span><br><span class="line"> (make-method ,form)))</span><br><span class="line"> form))))</span><br></pre></td></tr></table></figure><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210614/DIO%E5%B0%B1%E6%98%AF%E5%8E%89%E5%AE%B3%E6%A2%97%E5%9B%BE.jpg"></p><p>秉持“柿子要挑软的捏”的原则,让我来尝试模拟出<code>advice-add</code>的<code>:after-while</code>和<code>:before-while</code>的效果吧。</p><p><code>:after-while</code>和<code>:before-while</code>的效果还是很容易理解的</p><blockquote><p>Call function after the old function and only if the old function returned non-<code>nil</code>.</p><p>Call function before the old function and don’t call the old function if function returns <code>nil</code>.</p></blockquote><p>因此,由<code>define-method-combination</code>生成的<code>form</code>中(犹记得伞哥在《PCL》中将它翻译为形式),势必要:</p><ul><li>检查是否有被<code>:before-while</code>修饰的方法;</li><li>如果有,检查调用了被<code>:before-while</code>修饰的方法后的返回值是否为<code>NIL</code>;</li><li>如果没有,或者被<code>:before-while</code>修饰的方法的返回值为非<code>NIL</code>,便调用<code>primary</code>方法;</li><li>如果有被<code>:after-while</code>修饰的方法,并且<code>primary</code>方法的返回值不为<code>NIL</code>,就调用这些方法;</li><li>返回<code>primary</code>方法的返回值。</li></ul><p>为了简单起见,尽管<code>after-while</code>和<code>before-while</code>变量指向的是多个“可调用”的方法,但这里只调用“最具体”的一个。</p><p>给这个新的<code>method combination</code>取名为<code>emacs-advice</code>,其具体实现已是水到渠成</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">define-method-combination</span> emacs-advice ()</span><br><span class="line"> ((<span class="name">after-while</span> (<span class="symbol">:after-while</span>))</span><br><span class="line"> (<span class="name">before-while</span> (<span class="symbol">:before-while</span>))</span><br><span class="line"> (<span class="name">primary</span> () <span class="symbol">:required</span> <span class="literal">t</span>))</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">after-while-fn</span> (<span class="name">first</span> after-while))</span><br><span class="line"> (<span class="name">before-while-fn</span> (<span class="name">first</span> before-while))</span><br><span class="line"> (<span class="name">result</span> (<span class="name">gensym</span>)))</span><br><span class="line"> `(let ((,result (when ,before-while-fn</span><br><span class="line"> (call-method ,before-while-fn))))</span><br><span class="line"> (when (or (null ,before-while-fn)</span><br><span class="line"> ,result)</span><br><span class="line"> (let ((,result (call-method ,(first primary))))</span><br><span class="line"> (when (and ,result ,after-while-fn)</span><br><span class="line"> (call-method ,after-while-fn))</span><br><span class="line"> ,result)))))</span><br></pre></td></tr></table></figure><p><code>call-method</code>(以及它的搭档<code>make-method</code>)是专门用于在<code>define-method-combination</code>中调用传入的方法的宏。</p><p>用一系列<code>foobar</code>方法来验证一番</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defgeneric</span> foobar (<span class="name">x</span>)</span><br><span class="line"> (<span class="symbol">:method-combination</span> emacs-advice))</span><br><span class="line"></span><br><span class="line">(<span class="name">defmethod</span> foobar (<span class="name">x</span>)</span><br><span class="line"> 'hello)</span><br><span class="line"></span><br><span class="line">(<span class="name">defmethod</span> foobar <span class="symbol">:after-while</span> (<span class="name">x</span>)</span><br><span class="line"> (<span class="name">declare</span> (<span class="name">ignorable</span> x))</span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"for side effect~%"</span>))</span><br><span class="line"></span><br><span class="line">(<span class="name">defmethod</span> foobar <span class="symbol">:before-while</span> (<span class="name">x</span>)</span><br><span class="line"> (<span class="name">evenp</span> x))</span><br><span class="line"></span><br><span class="line">(<span class="name">foobar</span> <span class="number">1</span>) <span class="comment">;; 返回NIL</span></span><br><span class="line">(<span class="name">foobar</span> <span class="number">2</span>) <span class="comment">;; 打印“fo side effect”,并返回HELLO</span></span><br></pre></td></tr></table></figure><h1 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h1><p>尽管我对CL赏识有加,但越是琢磨<code>define-method-combination</code>,就越会发现编程语言的能力是有极限的<del>,除非超越编程语言</del>。比如Emacs的<code>advice-add</code>所支持的<code>:filter-args</code>和<code>:filter-return</code>就无法用<code>define-method-combination</code>优雅地实现出来——并不是完全不行,只不过需要将它们合并在由<code>:around</code>修饰的方法之中。</p>]]></content>
<summary type="html"></summary>
<category term="实战Elisp" scheme="https://liutos.github.io/categories/%E5%AE%9E%E6%88%98Elisp/"/>
<category term="org-mode" scheme="https://liutos.github.io/tags/org-mode/"/>
<category term="common lisp" scheme="https://liutos.github.io/tags/common-lisp/"/>
<category term="elisp" scheme="https://liutos.github.io/tags/elisp/"/>
<category term="emacs" scheme="https://liutos.github.io/tags/emacs/"/>
<category term="lisp" scheme="https://liutos.github.io/tags/lisp/"/>
<category term="实战Elisp" scheme="https://liutos.github.io/tags/%E5%AE%9E%E6%88%98Elisp/"/>
<category term="oop" scheme="https://liutos.github.io/tags/oop/"/>
</entry>
<entry>
<title>非递归遍历二叉树到底有什么用</title>
<link href="https://liutos.github.io/2021/05/03/%E9%9D%9E%E9%80%92%E5%BD%92%E9%81%8D%E5%8E%86%E4%BA%8C%E5%8F%89%E6%A0%91%E5%88%B0%E5%BA%95%E6%9C%89%E4%BB%80%E4%B9%88%E7%94%A8/"/>
<id>https://liutos.github.io/2021/05/03/%E9%9D%9E%E9%80%92%E5%BD%92%E9%81%8D%E5%8E%86%E4%BA%8C%E5%8F%89%E6%A0%91%E5%88%B0%E5%BA%95%E6%9C%89%E4%BB%80%E4%B9%88%E7%94%A8/</id>
<published>2021-05-03T03:58:11.000Z</published>
<updated>2022-05-02T03:18:26.000Z</updated>
<content type="html"><![CDATA[<p>准备过互联网公司的服务端岗位面试的人,对于二叉树的三种遍历方式想必是如数家珍。假设以类<code>BinaryTree</code>定义一棵二叉树</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">BinaryTree</span>:</span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, left, right, value</span>):</span><br><span class="line"> <span class="variable language_">self</span>.left = left</span><br><span class="line"> <span class="variable language_">self</span>.right = right</span><br><span class="line"> <span class="variable language_">self</span>.value = value</span><br></pre></td></tr></table></figure><p>实现一个前序遍历的算法便是信手拈来的事情</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">preorder_traversal</span>(<span class="params">tree, func</span>):</span><br><span class="line"> <span class="string">"""前序遍历二叉树的每个节点。"""</span></span><br><span class="line"> <span class="keyword">if</span> tree <span class="keyword">is</span> <span class="literal">None</span>:</span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line"> func(tree.value)</span><br><span class="line"> preorder_traversal(tree.left, func)</span><br><span class="line"> preorder_traversal(tree.right, func)</span><br></pre></td></tr></table></figure><p>随着行业曲率的增大,要求写出不使用递归的版本也没什么过分的</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">iterative_preorder_traversal</span>(<span class="params">tree, func</span>):</span><br><span class="line"> nodes = [tree]</span><br><span class="line"> <span class="keyword">while</span> <span class="built_in">len</span>(nodes) > <span class="number">0</span>:</span><br><span class="line"> node = nodes.pop()</span><br><span class="line"> func(node)</span><br><span class="line"> <span class="keyword">if</span> node.left <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span>:</span><br><span class="line"> nodes.append(node.right)</span><br><span class="line"> <span class="keyword">if</span> node.left <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span>:</span><br><span class="line"> nodes.append(node.left)</span><br></pre></td></tr></table></figure><p>一直以来,我觉得这种用一个显式的栈来代替递归过程中隐式的栈的做法就是镜花水月。但最近却找到了它的一个用武之地——用于实现<code>iterator</code>。</p><h1 id="iterator是个啥?"><a href="#iterator是个啥?" class="headerlink" title="iterator是个啥?"></a><code>iterator</code>是个啥?</h1><p>这年头,<code>iterator</code>已经不是什么新鲜事物了,许多语言中都有支持,维基百科上有一份<a href="https://en.wikipedia.org/wiki/Iterator#In_different_programming_languages">清单</a>列出了比较知名的语言的<code>iterator</code>特性。按照Python官方的术语表中的<a href="https://docs.python.org/3/glossary.html#term-iterator">定义</a>,<code>iterator</code>表示一个数据流,反复调用其<code>__next__</code>方法可以一个接一个地返回流中的下一项数据。将内置函数<code>iter</code>作用于<code>list</code>、<code>str</code>、<code>tuple</code>类型的对象,可以获得相应的迭代器</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">cat</span> get_iter.py</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">-*- coding: utf8 -*-</span></span><br><span class="line">if __name__ == '__main__':</span><br><span class="line"> values = [</span><br><span class="line"> [1, 2, 3],</span><br><span class="line"> 'Hello, world!',</span><br><span class="line"> (True, None),</span><br><span class="line"> ]</span><br><span class="line"> for v in values:</span><br><span class="line"> print('type of iter({}) is {}'.format(v, type(iter(v))))</span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">python get_iter.py</span></span><br><span class="line">type of iter([1, 2, 3]) is <class 'list_iterator'></span><br><span class="line">type of iter(Hello, world!) is <class 'str_iterator'></span><br><span class="line">type of iter((True, None)) is <class 'tuple_iterator'></span><br></pre></td></tr></table></figure><h1 id="写一个前序遍历的iterator"><a href="#写一个前序遍历的iterator" class="headerlink" title="写一个前序遍历的iterator"></a>写一个前序遍历的<code>iterator</code></h1><p>一个<code>iterator</code>对象必须要实现<code>__iter__</code>和<code>__next__</code>方法:</p><ul><li><code>__iter__</code>只需要返回<code>iterator</code>对象自身即可;</li><li>顾名思义,<code>__next__</code>负责返回下一个元素。</li></ul><p>仔细观察一下前文中的<code>iterative_preorder_traversal</code>函数可以看出:</p><ul><li><code>nodes = [tree]</code>属于初始化逻辑;</li><li><code>len(nodes) > 0</code>用于判断是应当抛出<code>StopIteration</code>,还是应当继续返回下一个值(<code>nodes.pop()</code>);</li><li>最后四行就是负责填充<code>nodes</code>,好让它可以在下一次调用<code>__next__</code>的时候有值可以返回的。</li></ul><p>到这里,<code>iterator</code>的具体实现代码已经呼之欲出了</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">BinaryTreePreorderIterator</span>:</span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, root</span>):</span><br><span class="line"> nodes = []</span><br><span class="line"> <span class="keyword">if</span> root <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span>:</span><br><span class="line"> nodes.append(root)</span><br><span class="line"> <span class="variable language_">self</span>.nodes = nodes</span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">__iter__</span>(<span class="params">self</span>):</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">self</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">__next__</span>(<span class="params">self</span>):</span><br><span class="line"> <span class="keyword">if</span> <span class="built_in">len</span>(<span class="variable language_">self</span>.nodes) == <span class="number">0</span>:</span><br><span class="line"> <span class="keyword">raise</span> StopIteration</span><br><span class="line"> node = <span class="variable language_">self</span>.nodes.pop()</span><br><span class="line"> <span class="keyword">if</span> node.right <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span>:</span><br><span class="line"> <span class="variable language_">self</span>.nodes.append(node.right)</span><br><span class="line"> <span class="keyword">if</span> node.left <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span>:</span><br><span class="line"> <span class="variable language_">self</span>.nodes.append(node.left)</span><br><span class="line"> <span class="keyword">return</span> node.value</span><br></pre></td></tr></table></figure><p>构造一棵这样的满二叉树</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210503/%E6%BB%A1%E4%BA%8C%E5%8F%89%E6%A0%91.png"></p><p>用<code>BinaryTreePreorderIterator</code>可以正确地打印出每一个节点的值</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> __name__ == <span class="string">'__main__'</span>:</span><br><span class="line"> tree = BinaryTree(</span><br><span class="line"> BinaryTree(</span><br><span class="line"> BinaryTree(<span class="literal">None</span>, <span class="literal">None</span>, <span class="number">1</span>), </span><br><span class="line"> BinaryTree(<span class="literal">None</span>, <span class="literal">None</span>, <span class="number">3</span>), </span><br><span class="line"> <span class="number">2</span>,</span><br><span class="line"> ),</span><br><span class="line"> BinaryTree(</span><br><span class="line"> BinaryTree(<span class="literal">None</span>, <span class="literal">None</span>, <span class="number">5</span>), </span><br><span class="line"> BinaryTree(<span class="literal">None</span>, <span class="literal">None</span>, <span class="number">7</span>), </span><br><span class="line"> <span class="number">6</span>,</span><br><span class="line"> ),</span><br><span class="line"> <span class="number">4</span>,</span><br><span class="line"> )</span><br><span class="line"> <span class="keyword">for</span> n <span class="keyword">in</span> BinaryTreePreorderIterator(tree):</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">'{}\t'</span>.<span class="built_in">format</span>(n), end=<span class="string">''</span>)</span><br><span class="line"><span class="comment"># 打印内容为:4213657</span></span><br></pre></td></tr></table></figure><h1 id="iterator的优势"><a href="#iterator的优势" class="headerlink" title="iterator的优势"></a><code>iterator</code>的优势</h1><p>显然,<code>iterator</code>比起<code>preorder_traversal</code>更为灵活——很容易在<code>for-in</code>循环内添加各种各样的控制逻辑:用<code>continue</code>跳过一些值,或者用<code>break</code>提前结束遍历过程。这些在函数<code>preorder_traversal</code>中做起来会比较别扭。</p><p>聪明的你应该已经发现了,大可不必将<code>preorder_traversal</code>拆解到一个构造方法和一个<code>__next__</code>方法中。用<code>generator</code>写起来明明更加直观</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">preorder_generator</span>(<span class="params">tree</span>):</span><br><span class="line"> <span class="string">"""返回一个能够以前序遍历的次序遍历二叉树节点的generator。"""</span></span><br><span class="line"> nodes = [tree]</span><br><span class="line"> <span class="keyword">while</span> <span class="built_in">len</span>(nodes) > <span class="number">0</span>:</span><br><span class="line"> node = nodes.pop()</span><br><span class="line"> <span class="keyword">yield</span> node.value</span><br><span class="line"> <span class="keyword">if</span> node.left <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span>:</span><br><span class="line"> nodes.append(node.right)</span><br><span class="line"> <span class="keyword">if</span> node.left <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span>:</span><br><span class="line"> nodes.append(node.left)</span><br></pre></td></tr></table></figure><p>但是,很多语言并不支持<code>generator</code>。与之相比,<code>iterator</code>要亲民得多,更容易移植。例如,即使是Common Lisp这种一穷二白的语言,也可以实现和Python的<code>iterator</code>以及<code>for</code>类似的效果</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">in-package</span> #<span class="symbol">:cl-user</span>)</span><br><span class="line"></span><br><span class="line">(<span class="name">defpackage</span> #<span class="symbol">:com</span>.liutos.binary-tree</span><br><span class="line"> (<span class="symbol">:use</span> #<span class="symbol">:cl</span>))</span><br><span class="line"></span><br><span class="line">(<span class="name">in-package</span> #<span class="symbol">:com</span>.liutos.binary-tree)</span><br><span class="line"></span><br><span class="line">(<span class="name">defclass</span> preorder-iterator ()</span><br><span class="line"> ((<span class="name">nodes</span></span><br><span class="line"> <span class="symbol">:initform</span> <span class="literal">nil</span>)</span><br><span class="line"> (<span class="name">tree</span></span><br><span class="line"> <span class="symbol">:initarg</span> <span class="symbol">:tree</span>))</span><br><span class="line"> (<span class="symbol">:documentation</span> <span class="string">"前序遍历二叉树的迭代器"</span>))</span><br><span class="line"></span><br><span class="line">(<span class="name">defmethod</span> initialize-instance <span class="symbol">:after</span> ((<span class="name">instance</span> preorder-iterator) <span class="symbol">&key</span>)</span><br><span class="line"> (<span class="name">with-slots</span> (<span class="name">nodes</span> tree)</span><br><span class="line"> instance</span><br><span class="line"> (<span class="name">when</span> tree</span><br><span class="line"> (<span class="name">push</span> tree nodes))))</span><br><span class="line"></span><br><span class="line">(<span class="name">defgeneric</span> next (<span class="name">iterator</span>)</span><br><span class="line"> (<span class="symbol">:documentation</span> <span class="string">"返回迭代器的下一个值。"</span>))</span><br><span class="line"></span><br><span class="line">(<span class="name">define-condition</span> stop-iteration (<span class="name">error</span>)</span><br><span class="line"> ()</span><br><span class="line"> (<span class="symbol">:documentation</span> <span class="string">"Python中StopIteration异常的等价物。"</span>))</span><br><span class="line"></span><br><span class="line">(<span class="name">defmethod</span> next ((<span class="name">iterator</span> preorder-iterator))</span><br><span class="line"> (<span class="name">with-slots</span> (<span class="name">nodes</span>) iterator</span><br><span class="line"> (<span class="name">when</span> (<span class="name">null</span> nodes)</span><br><span class="line"> (<span class="name">error</span> 'stop-iteration))</span><br><span class="line"></span><br><span class="line"> (<span class="name">let</span> ((<span class="name">node</span> (<span class="name">pop</span> nodes)))</span><br><span class="line"> <span class="comment">;; 一个节点的结构为:(值 左子树 右子树)</span></span><br><span class="line"> (<span class="name">when</span> (<span class="name">third</span> node)</span><br><span class="line"> (<span class="name">push</span> (<span class="name">third</span> node) nodes))</span><br><span class="line"> (<span class="name">when</span> (<span class="name">second</span> node)</span><br><span class="line"> (<span class="name">push</span> (<span class="name">second</span> node) nodes))</span><br><span class="line"> (<span class="name">first</span> node))))</span><br><span class="line"></span><br><span class="line">(<span class="name">defmacro</span> for-in (<span class="name">var</span> iterator <span class="symbol">&body</span> forms)</span><br><span class="line"> <span class="string">"将iterator中的值逐个绑定到变量var上,并执行forms中的表达式。"</span></span><br><span class="line"> (<span class="name">let</span> ((<span class="name">iter</span> (<span class="name">gensym</span>)))</span><br><span class="line"> `(let ((,iter ,iterator))</span><br><span class="line"> (handler-case</span><br><span class="line"> (loop</span><br><span class="line"> (let ((,var (next ,iter)))</span><br><span class="line"> ,@forms))</span><br><span class="line"> (stop-iteration (c)</span><br><span class="line"> (declare (ignorable c)))))))</span><br><span class="line"></span><br><span class="line">(<span class="name">defparameter</span> *tree*</span><br><span class="line"> '(<span class="number">4</span> (<span class="number">2</span> (<span class="number">1</span> <span class="literal">nil</span> <span class="literal">nil</span>) (<span class="number">3</span> <span class="literal">nil</span> <span class="literal">nil</span>)) (<span class="number">6</span> (<span class="number">5</span> <span class="literal">nil</span> <span class="literal">nil</span>) (<span class="number">7</span> <span class="literal">nil</span> <span class="literal">nil</span>))))</span><br><span class="line"></span><br><span class="line">(<span class="name">defun</span> test-preorder-iterator ()</span><br><span class="line"> <span class="string">"测试前序遍历迭代器。"</span></span><br><span class="line"> (<span class="name">for-in</span> n (<span class="name">make-instance</span> 'preorder-iterator</span><br><span class="line"> <span class="symbol">:tree</span> *tree*)</span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"~D~C"</span> n #\Tab)))</span><br></pre></td></tr></table></figure><h1 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h1><p>中序遍历和后序遍历也可以写成迭代器,证明略。</p>]]></content>
<summary type="html"></summary>
<category term="common lisp" scheme="https://liutos.github.io/tags/common-lisp/"/>
<category term="算法" scheme="https://liutos.github.io/tags/%E7%AE%97%E6%B3%95/"/>
<category term="lisp" scheme="https://liutos.github.io/tags/lisp/"/>
<category term="二叉树" scheme="https://liutos.github.io/tags/%E4%BA%8C%E5%8F%89%E6%A0%91/"/>
<category term="递归" scheme="https://liutos.github.io/tags/%E9%80%92%E5%BD%92/"/>
<category term="python" scheme="https://liutos.github.io/tags/python/"/>
</entry>
<entry>
<title>当你SET的时候,Redis到底在SET些什么</title>
<link href="https://liutos.github.io/2021/04/17/%E5%BD%93%E4%BD%A0SET%E7%9A%84%E6%97%B6%E5%80%99%EF%BC%8CRedis%E5%88%B0%E5%BA%95%E5%9C%A8SET%E4%BA%9B%E4%BB%80%E4%B9%88/"/>
<id>https://liutos.github.io/2021/04/17/%E5%BD%93%E4%BD%A0SET%E7%9A%84%E6%97%B6%E5%80%99%EF%BC%8CRedis%E5%88%B0%E5%BA%95%E5%9C%A8SET%E4%BA%9B%E4%BB%80%E4%B9%88/</id>
<published>2021-04-17T07:09:24.000Z</published>
<updated>2021-04-17T07:14:22.000Z</updated>
<content type="html"><![CDATA[<p>准备过互联网公司的服务端岗位面试的人,对Redis中的5种数据类型想必是如数家珍。而网上很多面试题里也会出现这道题目</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210417/%E9%9D%A2%E8%AF%95%E9%A2%98%E4%BE%8B%E5%AD%901.jpg" alt="来自https://blog.csdn.net/ThinkWon/article/details/103522351"></p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210417/%E9%9D%A2%E8%AF%95%E9%A2%98%E4%BE%8B%E5%AD%902.jpg" alt="来自https://juejin.cn/post/6844903982066827277"></p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210417/%E9%9D%A2%E8%AF%95%E9%A2%98%E4%BE%8B%E5%AD%903.jpg" alt="来自https://mikechen.cc/3313.html"></p><p>随着行业曲率的增大,光是知道有这些数据类型已经不够了,还得知道同一个类型也有不同的底层数据结构。例如同样是<code>string</code>类型,不同内容或不同长度会采用不同的编码方式:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> SET key1 "1"</span><br><span class="line">OK</span><br><span class="line">127.0.0.1:6379> SET key2 "value"</span><br><span class="line">OK</span><br><span class="line">127.0.0.1:6379> SET key3 "Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp."</span><br><span class="line">OK</span><br><span class="line">127.0.0.1:6379> TYPE key1</span><br><span class="line">string</span><br><span class="line">127.0.0.1:6379> TYPE key2</span><br><span class="line">string</span><br><span class="line">127.0.0.1:6379> TYPE key3</span><br><span class="line">string</span><br><span class="line">127.0.0.1:6379> OBJECT ENCODING key1</span><br><span class="line">"int"</span><br><span class="line">127.0.0.1:6379> OBJECT ENCODING key2</span><br><span class="line">"embstr"</span><br><span class="line">127.0.0.1:6379> OBJECT ENCODING key3</span><br><span class="line">"raw"</span><br></pre></td></tr></table></figure><p>而<code>hash</code>类型也有两种底层实现</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> HSET myhash field1 "Hello"</span><br><span class="line">(integer) 1</span><br><span class="line">127.0.0.1:6379> HSET myhash2 field1 "Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp."</span><br><span class="line">(integer) 1</span><br><span class="line">127.0.0.1:6379> OBJECT ENCODING myhash</span><br><span class="line">"ziplist"</span><br><span class="line">127.0.0.1:6379> OBJECT ENCODING myhash2</span><br><span class="line">"hashtable"</span><br></pre></td></tr></table></figure><p>不知道你是否曾经好奇过,上文中的<code>key1</code>、<code>key2</code>、<code>key3</code>、<code>myhash</code>,以及<code>myhash2</code>这些键,与它们各自的值(前三个为<code>string</code>,后两个为<code>hash</code>)之间的关系又是存储在什么数据结构中的呢?</p><p>答案在意料之外,情理之中:键与值的关系,也是存储在一张哈希表中的,并且正是上文中的<code>hashtable</code>。</p><p>求证的办法当然是阅读Redis的源代码。</p><h1 id="Redis命令的派发逻辑"><a href="#Redis命令的派发逻辑" class="headerlink" title="Redis命令的派发逻辑"></a>Redis命令的派发逻辑</h1><p>阅读Redis的源码是比较轻松愉快的,一是因为其源码由简单易懂的C语言编写,二是因为源码仓库的<code>README.md</code>中对内部实现做了一番高屋建瓴的介绍。在<code>README.md</code>的<a href="https://github.com/redis/redis#serverc">server.c</a>一节中,道出了有关命令派发的两个关键点</p><blockquote><p><code>call()</code> is used in order to call a given command in the context of a given client.</p></blockquote><blockquote><p>The global variable <code>redisCommandTable</code> defines all the Redis commands, specifying the name of the command, the function implementing the command, the number of arguments required, and other properties of each command.</p></blockquote><p>位于文件<code>src/server.c</code>中的变量<code>redisCommandTable</code>定义了所有可以在Redis中使用的命令——为什么一个C语言项目里要用<code>camelCase</code>这种格格不入的命名风格呢——它的元素的类型为<code>struct redisCommand</code>,其中:</p><ul><li><code>name</code>存放命令的名字;</li><li><code>proc</code>存放实现命令的C函数的指针;</li></ul><p>比如高频使用的<code>GET</code>命令在<code>redisCommandTable</code>中就是这样定义的</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">{<span class="string">"get"</span>,getCommand,<span class="number">2</span>,</span><br><span class="line"> <span class="string">"read-only fast @string"</span>,</span><br><span class="line"> <span class="number">0</span>,<span class="literal">NULL</span>,<span class="number">1</span>,<span class="number">1</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>},</span><br></pre></td></tr></table></figure><p>身为一名老解释器爱好者,对这种套路的代码当然是不会陌生的。我也曾在写过的、跑不起来的玩具解释器上用过类似的手法</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210417/liutscm%E4%B8%AD%E7%9A%84%E7%B1%BB%E4%BC%BC%E4%BB%A3%E7%A0%81.jpg"></p><p>Redis收到一道需要执行的命令后,根据命令的名字用<code>lookupCommand</code>找到一个命令(是个<code>struct redisCommand</code>类型的结构体),然后<code>call</code>函数做的事情就是调用它的<code>proc</code>成员所指向的函数而已</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">c->cmd->proc(c);</span><br></pre></td></tr></table></figure><p>那么接下来,就要看看<code>SET</code>命令对应的C函数究竟做了些什么了。</p><h1 id="SET命令的实现"><a href="#SET命令的实现" class="headerlink" title="SET命令的实现"></a><code>SET</code>命令的实现</h1><p><code>redisCommonTable</code>中下标为2的元素正是<code>SET</code>命令的定义</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* Note that we can't flag set as fast, since it may perform an</span></span><br><span class="line"><span class="comment"> * implicit DEL of a large key. */</span></span><br><span class="line">{<span class="string">"set"</span>,setCommand,<span class="number">-3</span>,</span><br><span class="line"> <span class="string">"write use-memory @string"</span>,</span><br><span class="line"> <span class="number">0</span>,<span class="literal">NULL</span>,<span class="number">1</span>,<span class="number">1</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>},</span><br></pre></td></tr></table></figure><p>其中函数<code>setCommand</code>定义在文件<code>t_string.c</code>中,它根据参数中是否有传入<code>NX</code>、<code>XX</code>、<code>EX</code>等选项计算出一个<code>flags</code>后,便调用<code>setGenericCommand</code>——顾名思义,这是一个通用的<code>SET</code>命令,它同时被<code>SET</code>、<code>SETNX</code>、<code>SETEX</code>,以及<code>PSETEX</code>四个Redis命令的实现函数所共用。</p><p><code>setGenericCommand</code>调用了<code>genericSetKey</code>,后者定义在文件<code>db.c</code>中。尽管该函数上方的注释写着</p><blockquote><p>All the new keys in the database should be created via this interface.</p></blockquote><p>但<del>人生不如意事十之八九</del>事实并非如此。例如在命令<code>RPUSH</code>的实现函数<code>rpushCommand</code>中,调用了<code>pushGenericCommand</code>,后者直接调用了<code>dbAdd</code>往Redis中存入键和列表对象的关系。</p><p>言归正传。根据键存在与否,<code>genericSetKey</code>会调用<code>dbAdd</code>或<code>dbOverwrite</code>。而在<code>dbAdd</code>中,最终调用了<code>dictAdd</code>将键与值存入数据库中。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* Add an element to the target hash table */</span></span><br><span class="line"><span class="type">int</span> <span class="title function_">dictAdd</span><span class="params">(dict *d, <span class="type">void</span> *key, <span class="type">void</span> *val)</span></span><br><span class="line">{</span><br><span class="line"> dictEntry *entry = dictAddRaw(d,key,<span class="literal">NULL</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (!entry) <span class="keyword">return</span> DICT_ERR;</span><br><span class="line"> dictSetVal(d, entry, val);</span><br><span class="line"> <span class="keyword">return</span> DICT_OK;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>现在我们知道了,使用<code>SET</code>命令时传入的<code>key</code>和<code>value</code>,是存储在一个<code>dict</code>类型的数据结构中。</p><h1 id="HSET命令的实现"><a href="#HSET命令的实现" class="headerlink" title="HSET命令的实现"></a><code>HSET</code>命令的实现</h1><p>依葫芦画瓢,Redis的<code>HSET</code>命令由位于文件<code>t_hash.c</code>中的函数<code>hsetCommand</code>实现,它会尝试转换要操作的<code>hash</code>值的编码方式。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hashTypeTryConversion(o,c->argv,<span class="number">2</span>,c->argc<span class="number">-1</span>);</span><br></pre></td></tr></table></figure><p>如果<code>hashTypeTryConversion</code>发现要写入哈希表的任何一个键或者值的长度超过了<code>server.hash_max_ziplist_value</code>所规定的值,就会将<code>hash</code>类型的编码从<code>ziplist</code>转换为<code>hashtable</code>。<code>server.hash_max_ziplist_value</code>的值在文件<code>config.c</code>中通过宏设置,默认值为64——这正是上文中<code>myhash2</code>所对应的值的编码为<code>hashtable</code>的原因。</p><p>将思绪拉回到函数<code>hsetCommand</code>中。做完编码的转换后,它调用函数<code>hashTypeSet</code>,在编码为<code>hashtable</code>的世界线中,同样调用了<code>dictAdd</code>实现往哈希表中写入键值对。</p><p>殊途同归</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210417/path_to_dbAdd.png"></p><h1 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h1><p>因此,在Redis中用以维持每一个键与其对应的值——这些值也许是<code>string</code>,也许是<code>list</code>,也许是<code>hash</code>——的关系的数据结构,与Redis中的一系列操作哈希表的命令——也许是<code>HSET</code>、也许<code>HGET</code>,也许是<code>HDEL</code>——所用的数据结构,不能说是毫不相关,起码是一模一样。</p>]]></content>
<summary type="html"></summary>
</entry>
<entry>
<title>多重返回值的阵营九宫格</title>
<link href="https://liutos.github.io/2021/03/14/%E5%A4%9A%E9%87%8D%E8%BF%94%E5%9B%9E%E5%80%BC%E7%9A%84%E9%98%B5%E8%90%A5%E4%B9%9D%E5%AE%AB%E6%A0%BC/"/>
<id>https://liutos.github.io/2021/03/14/%E5%A4%9A%E9%87%8D%E8%BF%94%E5%9B%9E%E5%80%BC%E7%9A%84%E9%98%B5%E8%90%A5%E4%B9%9D%E5%AE%AB%E6%A0%BC/</id>
<published>2021-03-14T07:52:34.000Z</published>
<updated>2021-03-15T14:46:49.000Z</updated>
<content type="html"><![CDATA[<p>通常在糊业务代码的时候,不管是函数、方法,还是宏,都只会有一个返回值。比如在C语言用于检查一个字符是否为阿拉伯数字的<code>isdigit</code>函数就只会返回是(1)或否(0)</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><ctype.h></span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><stdio.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span></span><br><span class="line"><span class="title function_">main</span><span class="params">(<span class="type">int</span> argc, <span class="type">char</span> *argv[])</span></span><br><span class="line">{</span><br><span class="line"> <span class="type">char</span> c = <span class="string">'a'</span>;</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"isdigit('%c') is %d\n"</span>, c, <span class="built_in">isdigit</span>(c));</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>但有时候如果一个函数、方法,或宏可以返回多个值的话会更加方便。例如,在Python中<code>dict</code>类型有一个实例方法<code>get</code>,它可以取得<code>dict</code>实例中与给定的键对应的值。但如果有一个键在字典中的值为<code>None</code>,那么光凭<code>get</code>的返回值无法准确判断这个键是否存在——除非你给它一个非<code>None</code>的默认值</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># -*- coding: utf8 -*-</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">test</span>(<span class="params">d, key</span>):</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"d.get('{0}') is {1}\t'{0}' in d is {2}"</span>.<span class="built_in">format</span>(key, d.get(key), key <span class="keyword">in</span> d))</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">'__main__'</span>:</span><br><span class="line"> d = {</span><br><span class="line"> <span class="string">'foo'</span>: <span class="string">'bar'</span>,</span><br><span class="line"> <span class="string">'baz'</span>: <span class="literal">None</span>,</span><br><span class="line"> }</span><br><span class="line"> test(d, <span class="string">'foo'</span>)</span><br><span class="line"> test(d, <span class="string">'baz'</span>)</span><br></pre></td></tr></table></figure><p>发展了这么多年的编程语言,又怎么会连一次调用、多值返回这么简单的事情都做不到呢。事实上,有各种各样、各显神通的返回多个值的方法,我给其中的一些做了个分类</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210314/%E5%A4%9A%E9%87%8D%E8%BF%94%E5%9B%9E%E5%80%BC%E7%9A%84%E5%AE%9A%E4%B9%89%E9%98%B5%E8%90%A5%E4%B9%9D%E5%AE%AB%E6%A0%BC.png"></p><h1 id="Lisp的multiple-value-bind"><a href="#Lisp的multiple-value-bind" class="headerlink" title="Lisp的multiple-value-bind"></a>Lisp的<code>multiple-value-bind</code></h1><p>Common Lisp(简称为CL)的多重返回值当之无愧是其中最正统、最好用的实现方式。以它的内置函数<code>truncate</code>为例,它的第一个返回值为第一个参数除以第二个参数的商,第二个返回值为对应的余数</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">CL-USER> (<span class="name">truncate</span> <span class="number">10</span> <span class="number">3</span>)</span><br><span class="line"><span class="number">3</span></span><br><span class="line"><span class="number">1</span></span><br></pre></td></tr></table></figure><p>如果不加修饰地调用<code>truncate</code>,就像其它只返回一个值的函数一样,也只会拿到一个返回值</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">CL-USER> (<span class="name">let</span> ((<span class="name">q</span> (<span class="name">truncate</span> <span class="number">10</span> <span class="number">3</span>)))</span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"q = ~D~%"</span> q))</span><br><span class="line">q = <span class="number">3</span></span><br></pre></td></tr></table></figure><p>除非用<code>multiple-value-bind</code>来捕获一个函数产生的所有返回值</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">CL-USER> (<span class="name">multiple-value-bind</span> (<span class="name">q</span> r)</span><br><span class="line"> (<span class="name">truncate</span> <span class="number">10</span> <span class="number">3</span>)</span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"q = ~D~8Tr = ~D~%"</span> q r))</span><br><span class="line">q = <span class="number">3</span> r = <span class="number">1</span></span><br></pre></td></tr></table></figure><p>CL的方案的优点在于它十分灵活。即使将一个函数从返回单个值改为返回多个值,也不会导致原本调用该函数的位置要全部修改一遍——对修改封闭,对扩展开放(误)。</p><h1 id="Go的多重返回值"><a href="#Go的多重返回值" class="headerlink" title="Go的多重返回值"></a>Go的多重返回值</h1><p>踩在C语言肩膀上的Go也能够从函数中返回多个值。在<code>io/ioutil</code>包的<a href="https://golang.org/pkg/io/ioutil/">官方文档</a>中有大量的例子,比如用<code>ReadAll</code>方法从字符串衍生的流中读取全部内容,就会返回两个值</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">"fmt"</span></span><br><span class="line"><span class="string">"io/ioutil"</span></span><br><span class="line"><span class="string">"log"</span></span><br><span class="line"><span class="string">"strings"</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line">s := <span class="string">"Hello, world!"</span></span><br><span class="line">reader := strings.NewReader(s)</span><br><span class="line">bytes, err := ioutil.ReadAll(reader)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line">log.Fatal(err)</span><br><span class="line">}</span><br><span class="line">fmt.Printf(<span class="string">"bytes is %s"</span>, bytes)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>Go以这种方式取代了C语言中用返回值表达成功与否、再通过指针传出读到的数据的风格。由于这个模式在有用的Go程序中到处出现,因此Gopher们用的都是定制的键盘(误)</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210314/go%E7%9A%84%E9%94%99%E8%AF%AF%E6%A3%80%E6%9F%A5.jpg"></p><p>不同于前文的<code>multiple-value-bind</code>,如果一个函数或方法返回多个值,那么调用者必须捕获每一个值,否则编译无法通过</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">➜ try cat try_read_all_ignore_err.go</span><br><span class="line">package main</span><br><span class="line"></span><br><span class="line">import (</span><br><span class="line">"fmt"</span><br><span class="line">"io/ioutil"</span><br><span class="line">"strings"</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line">func main() {</span><br><span class="line">s := "Hello, world!"</span><br><span class="line">reader := strings.NewReader(s)</span><br><span class="line">bytes := ioutil.ReadAll(reader)</span><br><span class="line">fmt.Printf("bytes is %s", bytes)</span><br><span class="line">}</span><br><span class="line">➜ try go build try_read_all_ignore_err.go</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">command-line-arguments</span></span><br><span class="line">./try_read_all_ignore_err.go:12:8: assignment mismatch: 1 variable but ioutil.ReadAll returns 2 values</span><br></pre></td></tr></table></figure><p>这一要求也是合理的,毕竟多重返回值机制主要用于向调用者传递出错原因——既然可能出错,那么就必须要检查一番。</p><h1 id="Python和Rust的解构"><a href="#Python和Rust的解构" class="headerlink" title="Python和Rust的解构"></a>Python和Rust的解构</h1><p>就像CL的<code>truncate</code>函数一样,Python中的函数<code>divmod</code>也可以同时返回两个数相除的商和余数,并且咋看之下也是返回多个值的形式</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># -*- coding: utf8 -*-</span></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">'__main__'</span>:</span><br><span class="line"> q, r = <span class="built_in">divmod</span>(<span class="number">10</span>, <span class="number">3</span>)</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">'q = {}\tr = {}'</span>.<span class="built_in">format</span>(q, r))</span><br></pre></td></tr></table></figure><p>但本质上,这是因为Python支持<a href="https://docs.python.org/3/reference/simple_stmts.html#assignment-statements">解构</a>,同时<a href="https://docs.python.org/3/library/functions.html#divmod"><code>divmod</code></a>返回的是一个由商和余数组成的元组。这样的做法与CL的真·奥义·多重返回值的差异在于,如果只想要<code>divmod</code>的第一个值,那么等号左侧也要写成对应的结构</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># -*- coding: utf8 -*-</span></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">'__main__'</span>:</span><br><span class="line"> q, _ = <span class="built_in">divmod</span>(<span class="number">10</span>, <span class="number">3</span>)</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">'q = {}'</span>.<span class="built_in">format</span>(q))</span><br></pre></td></tr></table></figure><p>在支持解构的语言中都可以模仿出多重返回值,例如Rust</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">divmod</span>(a: <span class="type">u32</span>, b: <span class="type">u32</span>) <span class="punctuation">-></span> (<span class="type">u32</span>, <span class="type">u32</span>) {</span><br><span class="line"> (a / b, a % b)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> (q, r) = <span class="title function_ invoke__">divmod</span>(<span class="number">10</span>, <span class="number">3</span>);</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"q = {}\tr = {}"</span>, q, r);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="Prolog的归一"><a href="#Prolog的归一" class="headerlink" title="Prolog的归一"></a>Prolog的归一</h1><p>到了Prolog这里,画风就有点不一样了。首先Prolog既没有函数,也没有方法,更没有宏。在Prolog中,像<code>length/2</code>和<code>member/2</code>这样的东西叫做<code>functor</code>,它们之于Prolog中的列表,就犹如CL的<code>length</code>和<code>member</code>之于列表、Python的<code>len</code>函数和<code>in</code>操作符之于列表,JavaScript的<code>length</code>属性和<code>indexOf</code>方法之于数组……</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210314/prolog%E7%9A%84length%E5%92%8Cmember.jpg"></p><p>其次,Prolog并不“返回”一个<code>functor</code>的“调用结果”,它只是判断输入的查询是否成立,以及给出使查询成立的变量值。在第一个查询中,<code>length/2</code>的第二个参数为变量<code>L</code>,因此Prolog给出了使这个查询成立的<code>L</code>的值4;第二个查询中没有变量,Prolog只是简单地给出查询是否成立;第三个查询中,Prolog给出了四个能够使查询成立的变量<code>X</code>的值。</p><p>由于Prolog会给出查询中每一个变量的值,可以用这个特性来模拟多重返回值。例如,可以让Prolog一次性给出两个数字的和、差、积,和商</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210314/%E5%90%8C%E6%97%B6%E8%AE%A1%E7%AE%97%E5%87%BA%E5%92%8C%E6%9F%A5%E7%A7%AF%E5%95%86.jpg"></p><p>麻烦之处在于就算只想要得到两数之和,也必须用占位符填在后三个参数上:<code>jjcc(10, 3, S, _, _, _)</code>。</p><h1 id="作弊的指针与全局变量"><a href="#作弊的指针与全局变量" class="headerlink" title="作弊的指针与全局变量"></a>作弊的指针与全局变量</h1><p>尽管在开篇的时候提到了C语言中的函数无法返回多个值,但如果像上文的Prolog那般允许修改参数的话,C语言也是可以做到的,谁让它有指针这个强力特性呢。例如,<code>stat(2)</code>函数就会将关于一个文件的信息填充到参数中所指向的结构体的内存中</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><stdio.h></span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><sys/stat.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span></span><br><span class="line"><span class="title function_">main</span><span class="params">(<span class="type">int</span> argc, <span class="type">char</span> *argv[])</span></span><br><span class="line">{</span><br><span class="line"> <span class="type">char</span> *path = <span class="string">"./try_stat.c"</span>;</span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">stat</span> <span class="title">buf</span>;</span></span><br><span class="line"> stat(path, &buf);</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"inode's number of %s is %llu\n"</span>, path, buf.st_ino);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>查看<code>man 2 stat</code>可以知道<code>struct stat</code>类型中有非常多的内容,这显然也是一种多重返回值。同样的手法,在Go中也可以运用,例如用于把从数据库中读取出来的行的数据写入目标数据结构的<a href="https://golang.org/pkg/database/sql/#Rows.Scan"><code>Scan</code>方法</a>。</p><p>最后,如果只要能让调用者感知就行,那么全局变量未尝不是一种通用的多重返回值机制。例如在C语言中的<code>strtol</code>函数,就会在无法转换出任何数字的时候返回0并设置<code>errno</code>,因此检查<code>errno</code>是必须的步骤</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><stdio.h></span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><stdlib.h></span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><sys/errno.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">try_conversion</span><span class="params">(<span class="type">const</span> <span class="type">char</span> *str)</span></span><br><span class="line">{</span><br><span class="line"> <span class="type">long</span> num = strtol(str, <span class="literal">NULL</span>, <span class="number">10</span>);</span><br><span class="line"> <span class="keyword">if</span> (errno == EINVAL || errno == ERANGE)</span><br><span class="line"> {</span><br><span class="line"> <span class="type">char</span> message[<span class="number">64</span>];</span><br><span class="line"> <span class="built_in">snprintf</span>(message, <span class="keyword">sizeof</span>(message), <span class="string">"strtol(\"%s\")"</span>, str);</span><br><span class="line"> perror(message);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"strtol(\"%s\") is %ld\n"</span>, str, num);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="type">int</span></span><br><span class="line"><span class="title function_">main</span><span class="params">(<span class="type">int</span> argc, <span class="type">char</span> *argv[])</span></span><br><span class="line">{</span><br><span class="line"> try_conversion(<span class="string">"233"</span>);</span><br><span class="line"> try_conversion(<span class="string">"0"</span>);</span><br><span class="line"> try_conversion(<span class="string">"lisp"</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>鉴于<code>errno</code>是一个全局变量,<code>strtol</code>的使用者完全有可能忘记要检查。相比之下,Go的<a href="https://golang.org/pkg/strconv/">strconv</a>包的函数都将转换过程中的错误以第二个参数的形式返回给调用者,用起来更安全。</p><h1 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h1><p>按照<a href="https://mp.weixin.qq.com/s/8T0rrl5dkwtYB_XCbAhACQ">《代码写得不好,不要总觉得是自己抽象得不好》</a>这篇文章的说法,代码写成什么样子完全是由产品经理决定的。但产品经理又怎么会在意你用的技术是怎么实现多重返回值的呢。综上所述,这个特性没用(误)。</p><p>全文完。</p>]]></content>
<summary type="html"></summary>
<category term="lisp" scheme="https://liutos.github.io/tags/lisp/"/>
<category term="编程语言" scheme="https://liutos.github.io/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
</entry>
<entry>
<title>终端看片指日可待</title>
<link href="https://liutos.github.io/2021/02/27/%E7%BB%88%E7%AB%AF%E7%9C%8B%E7%89%87%E6%8C%87%E6%97%A5%E5%8F%AF%E5%BE%85/"/>
<id>https://liutos.github.io/2021/02/27/%E7%BB%88%E7%AB%AF%E7%9C%8B%E7%89%87%E6%8C%87%E6%97%A5%E5%8F%AF%E5%BE%85/</id>
<published>2021-02-27T06:54:55.000Z</published>
<updated>2021-02-27T07:02:55.000Z</updated>
<content type="html"><![CDATA[<h1 id="序言"><a href="#序言" class="headerlink" title="序言"></a>序言</h1><p>在旧文<a href="https://liutos.github.io/2020/07/03/%E5%A6%82%E4%BD%95%E5%86%99%E4%B8%80%E4%B8%AA%E5%91%BD%E4%BB%A4%E8%A1%8C%E7%9A%84%E7%A7%92%E8%A1%A8/">《如何写一个命令行的秒表》</a>中,借助命令<code>tput</code>,我实现了“原地更新”所输出的时分秒的效果</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210227/stopwatch%E7%9A%84%E6%BC%94%E7%A4%BA%E6%95%88%E6%9E%9C.gif"></p><p>其中用到的是ASCII转义序列<code>\x1b[8D</code>和<code>\x1b[0K</code>。除此之外,ASCII转义序列还有许多其它功能。例如,可以用来定制输出内容的前景色</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210227/%E6%89%93%E5%8D%B0%E7%BA%A2%E8%89%B2%E7%9A%84helloworld.jpg"></p><p>将转义序列中的参数<code>38</code>改为<code>48</code>,可以定制输出内容的背景色</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210227/%E6%89%93%E5%8D%B0%E7%BA%A2%E8%89%B2%E8%83%8C%E6%99%AF%E7%9A%84helloworld.jpg"></p><p>将打印内容改为两个空格,看起来就像是在一块黑色的画布上涂了一个红色的方块</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210227/%E6%89%93%E5%8D%B0%E7%BA%A2%E8%89%B2%E6%96%B9%E5%9D%97.jpg"></p><p>既然如此,只要尺寸合适,就可以在终端打印出一张图片,只需要将每一个像素的颜色作为背景色,在坐标对应的行列上输出两个空格即可。如果能抹掉输出的内容并在同样的位置上打印一张不同的图片,甚至可以实现动画的效果。</p><p>百闻不如一见,下面我用Python演示一番。</p><h1 id="把GIF装进终端"><a href="#把GIF装进终端" class="headerlink" title="把GIF装进终端"></a>把GIF装进终端</h1><p>要想用前文的思路在终端中显示一张GIF图片,必须先得到GIF图片每一帧的每个像素的颜色才行。在Python中使用名为<a href="https://github.com/python-pillow/Pillow">Pillow</a>的库可以轻松地解析GIF文件,先安装这个库</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">➜ /tmp rmdir show_gif</span><br><span class="line">➜ /tmp mkdir show_gif</span><br><span class="line">➜ /tmp cd show_gif</span><br><span class="line">➜ show_gif python3 -m venv ./venv</span><br><span class="line">➜ show_gif . ./venv/bin/activate</span><br><span class="line">(venv) ➜ show_gif pip install Pillow</span><br><span class="line">Collecting Pillow</span><br><span class="line"> Using cached Pillow-8.1.0-cp39-cp39-macosx_10_10_x86_64.whl (2.2 MB)</span><br><span class="line">Installing collected packages: Pillow</span><br><span class="line">Successfully installed Pillow-8.1.0</span><br><span class="line">WARNING: You are using pip version 20.2.3; however, version 21.0.1 is available.</span><br><span class="line">You should consider upgrading via the '/private/tmp/show_gif/venv/bin/python3 -m pip install --upgrade pip' command.</span><br></pre></td></tr></table></figure><p>接着便可以让它读入并解析一张GIF图片</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> sys</span><br><span class="line"></span><br><span class="line"><span class="keyword">from</span> PIL <span class="keyword">import</span> Image, ImageSequence</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">'__main__'</span>:</span><br><span class="line"> path = sys.argv[<span class="number">1</span>]</span><br><span class="line"> im = Image.<span class="built_in">open</span>(path)</span><br><span class="line"> <span class="keyword">for</span> frame <span class="keyword">in</span> ImageSequence.Iterator(im):</span><br><span class="line"> <span class="keyword">pass</span></span><br></pre></td></tr></table></figure><p>然后将每一帧都转换为<code>RGB</code>模式再遍历其每一个像素</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> sys</span><br><span class="line"></span><br><span class="line"><span class="keyword">from</span> PIL <span class="keyword">import</span> Image, ImageSequence</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">'__main__'</span>:</span><br><span class="line"> path = sys.argv[<span class="number">1</span>]</span><br><span class="line"> im = Image.<span class="built_in">open</span>(path)</span><br><span class="line"> <span class="keyword">for</span> frame <span class="keyword">in</span> ImageSequence.Iterator(im):</span><br><span class="line"> rgb_frame = frame.convert(<span class="string">'RGB'</span>)</span><br><span class="line"> pixels = rgb_frame.load()</span><br><span class="line"> <span class="keyword">for</span> y <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">0</span>, rgb_frame.height):</span><br><span class="line"> <span class="keyword">for</span> x <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">0</span>, rgb_frame.width):</span><br><span class="line"> <span class="keyword">pass</span></span><br></pre></td></tr></table></figure><p>调用<code>Image</code>类的实例方法<code>load</code>得到的是一个<a href="https://pillow.readthedocs.io/en/stable/reference/PixelAccess.html"><code>PixelAccess</code></a>类的实例,它可以像二维数组一般用坐标获取每一个像素的颜色值,颜色值则是一个长度为3的<code>tuple</code>类型的值,其中依次是像素的三原色的分量。</p><p>从<a href="https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit">ANSI escape code词条的24-bit小节</a>中得知,使用参数为<code>48;2;</code>的转义序列,再接上以分号分隔的三原色分量即可设置24位的背景色</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> sys</span><br><span class="line"></span><br><span class="line"><span class="keyword">from</span> PIL <span class="keyword">import</span> Image, ImageSequence</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">'__main__'</span>:</span><br><span class="line"> path = sys.argv[<span class="number">1</span>]</span><br><span class="line"> im = Image.<span class="built_in">open</span>(path)</span><br><span class="line"> <span class="keyword">for</span> frame <span class="keyword">in</span> ImageSequence.Iterator(im):</span><br><span class="line"> rgb_frame = frame.convert(<span class="string">'RGB'</span>)</span><br><span class="line"> pixels = rgb_frame.load()</span><br><span class="line"> <span class="keyword">for</span> y <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">0</span>, rgb_frame.height):</span><br><span class="line"> <span class="keyword">for</span> x <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">0</span>, rgb_frame.width):</span><br><span class="line"> colors = pixels[x, y]</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">'\x1b[48;2;{};{};{}m \x1b[0m'</span>.<span class="built_in">format</span>(*colors), end=<span class="string">''</span>)</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">''</span>)</span><br></pre></td></tr></table></figure><p>在每次二重循环遍历了所有像素后,还必须清除输出的内容,并将光标重置到左上角才能再次打印,这可以用ASCII转义序列来实现。查阅<a href="https://vt100.net/docs/vt100-ug/chapter3.html">VT100 User Guide</a>可以知道,用<a href="https://vt100.net/docs/vt100-ug/chapter3.html#ED">ED命令</a>可以擦除显示的字符,对应的转义序列为<code>\x1b[2J</code>;用<a href="https://vt100.net/docs/vt100-ug/chapter3.html#CUP">CUP命令</a>可以移动光标的位置到左上角,对应的转义序列为<code>\x1b[0;0H</code>。在每次开始打印一帧图像前输出这两个转义序列即可</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> sys</span><br><span class="line"></span><br><span class="line"><span class="keyword">from</span> PIL <span class="keyword">import</span> Image, ImageSequence</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">'__main__'</span>:</span><br><span class="line"> path = sys.argv[<span class="number">1</span>]</span><br><span class="line"> im = Image.<span class="built_in">open</span>(path)</span><br><span class="line"> <span class="keyword">for</span> frame <span class="keyword">in</span> ImageSequence.Iterator(im):</span><br><span class="line"> rgb_frame = frame.convert(<span class="string">'RGB'</span>)</span><br><span class="line"> pixels = rgb_frame.load()</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">'\x1b[2J\x1b[0;0H'</span>, end=<span class="string">''</span>)</span><br><span class="line"> <span class="keyword">for</span> y <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">0</span>, rgb_frame.height):</span><br><span class="line"> <span class="keyword">for</span> x <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">0</span>, rgb_frame.width):</span><br><span class="line"> colors = pixels[x, y]</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">'\x1b[48;2;{};{};{}m \x1b[0m'</span>.<span class="built_in">format</span>(*colors), end=<span class="string">''</span>)</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">''</span>)</span><br></pre></td></tr></table></figure><p>最后,只需要在每次打印完一帧后,按GIF文件的要求睡眠一段时间即可。每一帧的展示时长可以从<code>info</code>属性的键<code>duration</code>中得到,单位是毫秒</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> sys</span><br><span class="line"><span class="keyword">import</span> time</span><br><span class="line"></span><br><span class="line"><span class="keyword">from</span> PIL <span class="keyword">import</span> Image, ImageSequence</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">'__main__'</span>:</span><br><span class="line"> path = sys.argv[<span class="number">1</span>]</span><br><span class="line"> im = Image.<span class="built_in">open</span>(path)</span><br><span class="line"> <span class="keyword">for</span> frame <span class="keyword">in</span> ImageSequence.Iterator(im):</span><br><span class="line"> rgb_frame = frame.convert(<span class="string">'RGB'</span>)</span><br><span class="line"> pixels = rgb_frame.load()</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">'\x1b[2J\x1b[0;0H'</span>, end=<span class="string">''</span>)</span><br><span class="line"> <span class="keyword">for</span> y <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">0</span>, rgb_frame.height):</span><br><span class="line"> <span class="keyword">for</span> x <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">0</span>, rgb_frame.width):</span><br><span class="line"> colors = pixels[x, y]</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">'\x1b[48;2;{};{};{}m \x1b[0m'</span>.<span class="built_in">format</span>(*colors), end=<span class="string">''</span>)</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">''</span>)</span><br><span class="line"> time.sleep(rgb_frame.info[<span class="string">'duration'</span>] / <span class="number">1000</span>)</span><br></pre></td></tr></table></figure><p>现在可以看看效果了。我准备了一张测试用的GIF图片,宽度和高度均为47像素,共34帧</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210227/%E4%B8%80%E9%A2%97%E8%8B%B9%E6%9E%9C%E7%9A%84%E5%8A%A8%E7%94%BB.gif"></p><p>让它在终端中显示出来吧</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210227/%E5%9C%A8%E7%BB%88%E7%AB%AF%E6%92%AD%E6%94%BE%E4%B8%80%E6%A3%B5%E8%8B%B9%E6%9E%9C%E7%9A%84%E5%8A%A8%E5%9B%BE.gif"></p><h1 id="一点微小的改进"><a href="#一点微小的改进" class="headerlink" title="一点微小的改进"></a>一点微小的改进</h1><p>你可能留意到了,前文的演示效果中有明显的闪烁,这是因为打印ASCII转义序列的速度不够快导致的。既然如此,可以将一整行的转义序列先生成出来,再一次性输出到终端。改动不复杂</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> sys</span><br><span class="line"><span class="keyword">import</span> time</span><br><span class="line"></span><br><span class="line"><span class="keyword">from</span> PIL <span class="keyword">import</span> Image, ImageSequence</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">'__main__'</span>:</span><br><span class="line"> path = sys.argv[<span class="number">1</span>]</span><br><span class="line"> im = Image.<span class="built_in">open</span>(path)</span><br><span class="line"> <span class="keyword">for</span> frame <span class="keyword">in</span> ImageSequence.Iterator(im):</span><br><span class="line"> rgb_frame = frame.convert(<span class="string">'RGB'</span>)</span><br><span class="line"> pixels = rgb_frame.load()</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">'\x1b[2J\x1b[0;0H'</span>, end=<span class="string">''</span>)</span><br><span class="line"> <span class="keyword">for</span> y <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">0</span>, rgb_frame.height):</span><br><span class="line"> last_colors = <span class="literal">None</span></span><br><span class="line"> line = <span class="string">''</span></span><br><span class="line"> <span class="keyword">for</span> x <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">0</span>, rgb_frame.width):</span><br><span class="line"> colors = pixels[x, y]</span><br><span class="line"> <span class="keyword">if</span> colors != last_colors:</span><br><span class="line"> line += <span class="string">'\x1b[0m\x1b[48;2;{};{};{}m '</span>.<span class="built_in">format</span>(*colors)</span><br><span class="line"> <span class="keyword">else</span>:</span><br><span class="line"> line += <span class="string">' '</span></span><br><span class="line"> last_colors = colors</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">'{}\x1b[0m'</span>.<span class="built_in">format</span>(line))</span><br><span class="line"> time.sleep(rgb_frame.info[<span class="string">'duration'</span>] / <span class="number">1000</span>)</span><br></pre></td></tr></table></figure><p>但效果却很显著</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210227/%E4%B8%8D%E6%80%8E%E4%B9%88%E9%97%AA%E7%83%81%E7%9A%84%E7%89%88%E6%9C%AC.gif"></p><p>全文完</p>]]></content>
<summary type="html"></summary>
<category term="命令行" scheme="https://liutos.github.io/tags/%E5%91%BD%E4%BB%A4%E8%A1%8C/"/>
<category term="绘图" scheme="https://liutos.github.io/tags/%E7%BB%98%E5%9B%BE/"/>
</entry>
<entry>
<title>此数绵绵无绝期——欧拉计划第66题</title>
<link href="https://liutos.github.io/2021/02/11/%E6%AD%A4%E6%95%B0%E7%BB%B5%E7%BB%B5%E6%97%A0%E7%BB%9D%E6%9C%9F%E2%80%94%E2%80%94%E6%AC%A7%E6%8B%89%E8%AE%A1%E5%88%92%E7%AC%AC66%E9%A2%98/"/>
<id>https://liutos.github.io/2021/02/11/%E6%AD%A4%E6%95%B0%E7%BB%B5%E7%BB%B5%E6%97%A0%E7%BB%9D%E6%9C%9F%E2%80%94%E2%80%94%E6%AC%A7%E6%8B%89%E8%AE%A1%E5%88%92%E7%AC%AC66%E9%A2%98/</id>
<published>2021-02-11T07:21:32.000Z</published>
<updated>2022-05-02T03:19:01.000Z</updated>
<content type="html"><![CDATA[<blockquote><p>仅以此文膜拜八年前的自己</p></blockquote><h1 id="序言"><a href="#序言" class="headerlink" title="序言"></a>序言</h1><p><a href="https://projecteuler.net/about">欧拉计划</a>(Project Euler)就像LeetCode,是一个编程答题的网站。不同于LeetCode的是,欧拉计划只要求用户提交最终答案即可(一般是一个数字),而不需要完整代码。因此,可以尽情地使用自己喜欢的编程语言——不少题目甚至光靠笔和纸便能解决。</p><p>欧拉计划的<a href="https://projecteuler.net/problem=66">第66题</a>非常有意思,它的题目很简单,就是要求找出在不大于1000的整数中,以哪一个数字为丢番图方程的系数,可以得到所有最小解中的最大值。</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210211/%E7%AC%AC66%E9%A2%98%E7%9A%84%E6%8F%8F%E8%BF%B0.jpeg"></p><p>可以很容易地看出方程有一个直观的暴力算法:让y从1开始递增,对于每一个y,计算公式<code>Dy^2+1</code>的值。如果该值为平方数,那么它的平方根就是最小的x解。再依照这个算法求解所有D不大于1000的方程,便可以求出题目的答案。很容易用Python写出这个算法</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># -*- coding: utf8 -*-</span></span><br><span class="line"><span class="keyword">import</span> math</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">is_square</span>(<span class="params">num: <span class="built_in">int</span></span>) -> <span class="built_in">bool</span>:</span><br><span class="line"> <span class="keyword">return</span> math.isqrt(num) ** <span class="number">2</span> == num</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">find_x</span>(<span class="params">D: <span class="built_in">int</span></span>) -> <span class="built_in">int</span>:</span><br><span class="line"> <span class="string">"""</span></span><br><span class="line"><span class="string"> 求出给定D时,满足题目所给的丢番图方程的最小的x。</span></span><br><span class="line"><span class="string"> """</span></span><br><span class="line"> <span class="keyword">assert</span> <span class="keyword">not</span> is_square(D)</span><br><span class="line"> y = <span class="number">1</span></span><br><span class="line"> <span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line"> candidate = D * y * y + <span class="number">1</span></span><br><span class="line"> <span class="keyword">if</span> is_square(candidate):</span><br><span class="line"> <span class="keyword">return</span> math.isqrt(candidate)</span><br><span class="line"> y += <span class="number">1</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">solve_66</span>(<span class="params">limit</span>):</span><br><span class="line"> <span class="string">"""</span></span><br><span class="line"><span class="string"> 找出不大于limi的D中,使find_x的返回值最大的那一个数字。</span></span><br><span class="line"><span class="string"> """</span></span><br><span class="line"> max_D = <span class="literal">None</span></span><br><span class="line"> max_x = <span class="literal">None</span></span><br><span class="line"> D = <span class="number">2</span></span><br><span class="line"> <span class="keyword">while</span> D <= limit:</span><br><span class="line"> <span class="keyword">if</span> is_square(D):</span><br><span class="line"> D += <span class="number">1</span></span><br><span class="line"> <span class="keyword">continue</span></span><br><span class="line"> x = find_x(D)</span><br><span class="line"> <span class="keyword">if</span> max_x <span class="keyword">is</span> <span class="literal">None</span> <span class="keyword">or</span> x > max_x:</span><br><span class="line"> max_D = D</span><br><span class="line"> max_x = x</span><br><span class="line"> D += <span class="number">1</span></span><br><span class="line"> <span class="keyword">return</span> max_D, max_x</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">'__main__'</span>:</span><br><span class="line"> D, x = solve_66(<span class="number">7</span>)</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">'D is {} and x is {}'</span>.<span class="built_in">format</span>(D, x))</span><br></pre></td></tr></table></figure><p>但如果将上限<code>limit</code>提升为1000,这个算法在有生之年是算不出结果的。</p><p>要想解决这一题,需要借助数学的力量。</p><h1 id="佩尔方程"><a href="#佩尔方程" class="headerlink" title="佩尔方程"></a>佩尔方程</h1><p>八年前第一次做这一题的时候,经过一番搜索,我从<a href="https://eli.thegreenplace.net/2009/06/19/project-euler-problem-66-and-continued-fractions/">这篇文章</a>中知道了题目中的方程叫做<a href="https://zh.wikipedia.org/wiki/%E4%BD%A9%E5%B0%94%E6%96%B9%E7%A8%8B">佩尔方程。</a>它有标准的解法,但需要用到连分数。那么什么是连分数呢?</p><p>连分数不是一种新的数系,只是小数的另一种写法。例如可以把分数45除以16写成下面的形式</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210211/%E4%B8%80%E4%B8%AA%E8%BF%9E%E5%88%86%E6%95%B0%E7%9A%84%E4%BE%8B%E5%AD%90.jpg"></p><p>就像定义递归的数据结构一样,可以给连分数一个递归的定义。连分数要么是一个整数,要么是一个整数加上另一个连分数的倒数。除了上面的形式,连分数也可以写成更节省篇幅的样子。比如把45除以16写成<code>[2;1,4,3]</code>,即把原本的式子中所有的整数部分按顺序写在一对方括号之间。这种记法,看起来就像是编程语言中的数组一般。</p><p>如果用数组<code>[2;1,4,3]</code>的不同前缀来构造分式,那么结果依次为<code>2/1</code>、<code>3/1</code>、<code>14/5</code>。它们是这个连分数的渐进连分数,而佩尔方程的一组解,就来自于渐进连分数的分子和分母。</p><p>以系数为7的佩尔方程为例,先计算出根号7的连分数,然后依次尝试它的渐进连分数。前三个分别为<code>2/1</code>、<code>3/1</code>、<code>5/2</code>,都不是方程的解。第四个渐进连分数<code>8/3</code>才是方程的解。如果继续提高连分数的精度,还会找到第二个解<code>127/48</code>。继续找,还有更多,而8则是其中最小的x。</p><p>所以,想要快速算出佩尔方程的解,最重要的是找到计算一个数的平方根的连分数的算法。</p><h1 id="计算平方根的连分数的错误方法"><a href="#计算平方根的连分数的错误方法" class="headerlink" title="计算平方根的连分数的错误方法"></a>计算平方根的连分数的错误方法</h1><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210211/%E8%BF%9E%E5%88%86%E6%95%B0%E7%9A%84%E9%80%9A%E7%94%A8%E5%BD%A2%E5%BC%8F.jpg"></p><p>要计算一个数字的连分数,最重要的便是要算出所有的整数部分(<code>a0</code>、<code>a2</code>、<code>a2</code>等)。它们都可以依据定义直接计算</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210211/%E6%95%B4%E6%95%B0%E9%83%A8%E5%88%86%E7%9A%84%E8%AE%A1%E7%AE%97%E6%96%B9%E6%B3%95.png"></p><p>推广到一半情况,如果用变量<code>n</code>存储开平方的数字,用<code>numbers</code>存储所有已知的整数,那么用Python可以写出下面的算法来计算出下一个整数</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 计算连分数数列的下一个数字</span></span><br><span class="line"><span class="keyword">import</span> math</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">compute_next_integer_part</span>(<span class="params">n, numbers</span>):</span><br><span class="line"> v = math.sqrt(n)</span><br><span class="line"> <span class="keyword">for</span> a <span class="keyword">in</span> numbers:</span><br><span class="line"> v = <span class="number">1</span> / (v - a)</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">int</span>(v)</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">'__main__'</span>:</span><br><span class="line"> n = <span class="number">14</span></span><br><span class="line"> numbers = [<span class="number">3</span>, <span class="number">1</span>, <span class="number">2</span>, <span class="number">1</span>]</span><br><span class="line"> v = compute_next_integer_part(n, numbers)</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">'下一个数字为{}'</span>.<span class="built_in">format</span>(v))</span><br></pre></td></tr></table></figure><p>遗憾的是,这个算法算出来的数字会因为计算上的精度误差而导致失之毫厘谬以千里。</p><h1 id="计算平方根的连分数的正确方法"><a href="#计算平方根的连分数的正确方法" class="headerlink" title="计算平方根的连分数的正确方法"></a>计算平方根的连分数的正确方法</h1><p>要想计算出正确的结果,就需要尽可能地消除在计算<code>1 / (v - a)</code>的时候引入的误差,因此必须把浮点数从分母中除去。</p><p>在<a href="http://www.maths.surrey.ac.uk/hosted-sites/R.Knott/Fibonacci/cfINTRO.html#section6.2.2">这个网站</a>中,作者以计算根号14的连分数为例,列出了一个表格</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210211/%E8%AE%A1%E7%AE%97%E6%A0%B9%E5%8F%B714%E8%BF%9E%E5%88%86%E6%95%B0%E7%9A%84%E8%BF%87%E7%A8%8B.jpeg"></p><p>可以看到<code>x1</code>、<code>x2</code>,以及<code>x3</code>都是形如<code>(sqrt(n)+a)/b</code>这样的格式,这样的式子更利于控制误差。那么是否每一个待计算的<code>x</code>都符合这种格式呢?答案是肯定的,可以用数学归纳法予以证明(为了方便写公式,用LaTeX写好后截了图)</p><p><img src="https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/20210211/%E6%95%B0%E5%AD%A6%E5%BD%92%E7%BA%B3%E6%B3%95%E8%BF%87%E7%A8%8B.jpg"></p><p>在这个证明过程中,还得到了分子中的<code>a</code>以及分母中的<code>b</code>的递推公式,现在可以写出正确的计算连分数整数部分的代码了。</p><h1 id="用Common-Lisp实现上述算法"><a href="#用Common-Lisp实现上述算法" class="headerlink" title="用Common Lisp实现上述算法"></a>用Common Lisp实现上述算法</h1><p>为了在实现这个算法的同时还要写出优雅的代码,我会用上Common Lisp的面向对象特性。首先是定义一个类来表示一个可以不断提高精度的连分数</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defpackage</span> #<span class="symbol">:com</span>.liutos.cf</span><br><span class="line"> (<span class="symbol">:use</span> #<span class="symbol">:cl</span>))</span><br><span class="line"></span><br><span class="line">(<span class="name">in-package</span> #<span class="symbol">:com</span>.liutos.cf)</span><br><span class="line"></span><br><span class="line">(<span class="name">defclass</span> <cf> ()</span><br><span class="line"> ((<span class="name">a</span></span><br><span class="line"> <span class="symbol">:documentation</span> <span class="string">"数学归纳法中、分子中与平方根相加的数"</span></span><br><span class="line"> <span class="symbol">:initform</span> <span class="number">0</span>)</span><br><span class="line"> (<span class="name">b</span></span><br><span class="line"> <span class="symbol">:documentation</span> <span class="string">"数学归纳法中的分母"</span></span><br><span class="line"> <span class="symbol">:initform</span> <span class="number">1</span>)</span><br><span class="line"> (<span class="name">numbers</span></span><br><span class="line"> <span class="symbol">:documentation</span> <span class="string">"连分数中的整数部分依次组成的数组。"</span></span><br><span class="line"> <span class="symbol">:initform</span> <span class="literal">nil</span>)</span><br><span class="line"> (<span class="name">origin</span></span><br><span class="line"> <span class="symbol">:documentation</span> <span class="string">"被开平方的数字"</span></span><br><span class="line"> <span class="symbol">:initarg</span> <span class="symbol">:origin</span>))</span><br><span class="line"> (<span class="symbol">:documentation</span> <span class="string">"表示整数ORIGIN的平方根的连分数。"</span>))</span><br></pre></td></tr></table></figure><p>接着再定义这个类需要实现的“接口”</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defgeneric</span> advance (<span class="name">cf</span>)</span><br><span class="line"> (<span class="symbol">:documentation</span> <span class="string">"让连分数CF提高到下一个精度。"</span>))</span><br><span class="line"></span><br><span class="line">(<span class="name">defgeneric</span> into-rational (<span class="name">cf</span>)</span><br><span class="line"> (<span class="symbol">:documentation</span> <span class="string">"将连分数CF转换为有理数类型的值。"</span>))</span><br></pre></td></tr></table></figure><p>最后来实现上述两个接口</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defmethod</span> advance ((<span class="name">cf</span> <cf>))</span><br><span class="line"> <span class="string">"根据递推公式计算出下一批a、b,以及连分数的整数部分。"</span></span><br><span class="line"> (<span class="name">let*</span> ((<span class="name">a</span> (<span class="name">slot-value</span> cf 'a))</span><br><span class="line"> (<span class="name">b</span> (<span class="name">slot-value</span> cf 'b))</span><br><span class="line"> (<span class="name">n</span> (<span class="name">slot-value</span> cf 'origin))</span><br><span class="line"> (<span class="name">m</span> (<span class="name">truncate</span> (<span class="name">+</span> (<span class="name">sqrt</span> n) a) b)))</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">a</span> (<span class="name">-</span> (<span class="name">*</span> b m) a))</span><br><span class="line"> (<span class="name">b</span> (<span class="name">/</span> (<span class="name">-</span> n (<span class="name">expt</span> (<span class="name">-</span> a (<span class="name">*</span> b m)) <span class="number">2</span>)) b)))</span><br><span class="line"> (<span class="name">setf</span> (<span class="name">slot-value</span> cf 'a) a</span><br><span class="line"> (<span class="name">slot-value</span> cf 'b) b</span><br><span class="line"> (<span class="name">slot-value</span> cf 'numbers) (<span class="name">append</span> (<span class="name">slot-value</span> cf 'numbers) (<span class="name">list</span> m))))</span><br><span class="line"> (<span class="name">values</span>)))</span><br><span class="line"></span><br><span class="line">(<span class="name">defmethod</span> into-rational ((<span class="name">cf</span> <cf>))</span><br><span class="line"> (<span class="name">let*</span> ((<span class="name">numbers</span> (<span class="name">reverse</span> (<span class="name">slot-value</span> cf 'numbers)))</span><br><span class="line"> (<span class="name">v</span> (<span class="name">first</span> numbers)))</span><br><span class="line"> (<span class="name">dolist</span> (<span class="name">n</span> (<span class="name">rest</span> numbers))</span><br><span class="line"> (<span class="name">setf</span> v</span><br><span class="line"> (<span class="name">+</span> n (<span class="name">/</span> <span class="number">1</span> v))))</span><br><span class="line"> v))</span><br></pre></td></tr></table></figure><p>在实现<code>into-rational</code>方法上,Common Lisp的有理数数值类型给我带来了极大的便利,它使我不必担心计算<code>(/ 1 v)</code>的时候会引入误差,代码写起来简单直白。</p><h1 id="解题"><a href="#解题" class="headerlink" title="解题"></a>解题</h1><p>乘胜追击,用Common Lisp解答第66题</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defun</span> find-min-x (<span class="name">D</span>)</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">cf</span> (<span class="name">make-instance</span> '<cf> <span class="symbol">:origin</span> D)))</span><br><span class="line"> (<span class="name">loop</span></span><br><span class="line"> (<span class="name">advance</span> cf)</span><br><span class="line"> (<span class="name">let*</span> ((<span class="name">ratio</span> (<span class="name">into-rational</span> cf))</span><br><span class="line"> (<span class="name">x</span> (<span class="name">numerator</span> ratio))</span><br><span class="line"> (<span class="name">y</span> (<span class="name">denominator</span> ratio)))</span><br><span class="line"> (<span class="name">when</span> (<span class="name">=</span> (<span class="name">-</span> (<span class="name">*</span> x x) (<span class="name">*</span> D y y)) <span class="number">1</span>)</span><br><span class="line"> (<span class="name">return-from</span> find-min-x x))))))</span><br><span class="line"></span><br><span class="line">(<span class="name">defun</span> square-p (<span class="name">n</span>)</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">rt</span> (<span class="name">sqrt</span> n)))</span><br><span class="line"> (<span class="name">=</span> rt (<span class="name">truncate</span> rt))))</span><br><span class="line"></span><br><span class="line">(<span class="name">defun</span> pro66 (<span class="name">&optional</span> (<span class="name">bnd</span> <span class="number">1000</span>))</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">target-d</span>)</span><br><span class="line">(<span class="name">max-x</span> <span class="number">0</span>))</span><br><span class="line"> (<span class="name">loop</span> <span class="symbol">:for</span> i <span class="symbol">:from</span> <span class="number">2</span> <span class="symbol">:to</span> bnd</span><br><span class="line"> <span class="symbol">:when</span> (<span class="name">not</span> (<span class="name">square-p</span> i))</span><br><span class="line"> <span class="symbol">:do</span> (<span class="name">let</span> ((<span class="name">x</span> (<span class="name">find-min-x</span> i)))</span><br><span class="line"> (<span class="name">if</span> (<span class="name">></span> x max-x)</span><br><span class="line"> (<span class="name">setf</span> target-d i</span><br><span class="line"> max-x x))))</span><br><span class="line"> (<span class="name">values</span> target-d max-x)))</span><br></pre></td></tr></table></figure><p>答案的D是多少就不说了,不过作为答案的x是16421658242965910275055840472270471049。有兴趣的读者可以试一下暴力解法要花多久才能算到这个数字。</p><p>全文完。</p>]]></content>
<summary type="html"></summary>
<category term="common lisp" scheme="https://liutos.github.io/tags/common-lisp/"/>
<category term="project euler" scheme="https://liutos.github.io/tags/project-euler/"/>
<category term="algorithm" scheme="https://liutos.github.io/tags/algorithm/"/>
<category term="算法" scheme="https://liutos.github.io/tags/%E7%AE%97%E6%B3%95/"/>
<category term="数学" scheme="https://liutos.github.io/tags/%E6%95%B0%E5%AD%A6/"/>
<category term="lisp" scheme="https://liutos.github.io/tags/lisp/"/>
</entry>
<entry>
<title>format,不只是格式化</title>
<link href="https://liutos.github.io/2021/01/29/format%EF%BC%8C%E4%B8%8D%E5%8F%AA%E6%98%AF%E6%A0%BC%E5%BC%8F%E5%8C%96/"/>
<id>https://liutos.github.io/2021/01/29/format%EF%BC%8C%E4%B8%8D%E5%8F%AA%E6%98%AF%E6%A0%BC%E5%BC%8F%E5%8C%96/</id>
<published>2021-01-29T15:05:51.000Z</published>
<updated>2022-05-02T03:18:46.000Z</updated>
<content type="html"><![CDATA[<blockquote><p>《实战Common Lisp》系列主要讲述在使用Common Lisp时能派上用场的小函数,希望能为Common Lisp的复兴做一些微小的贡献。MAKE COMMON LISP GREAT AGAIN。</p></blockquote><h1 id="序言"><a href="#序言" class="headerlink" title="序言"></a>序言</h1><p>写了一段时间的Python后,总觉得它跟Common Lisp(下文简称CL)有亿点点像。例如,Python和CL都支持可变数量的函数参数。在Python中写作</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">foo</span>(<span class="params">* args</span>):</span><br><span class="line"> <span class="built_in">print</span>(args)</span><br></pre></td></tr></table></figure><p>而在CL中则写成</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defun</span> foo (<span class="name">&rest</span> args)</span><br><span class="line"> (<span class="name">print</span> args))</span><br></pre></td></tr></table></figure><p>Python的语法更紧凑,而CL的语法表意更清晰。此外,它们也都支持关键字参数。在Python中写成</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">bar</span>(<span class="params">*, a=<span class="literal">None</span>, b=<span class="literal">None</span></span>):</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">'a={}\tb={}'</span>.<span class="built_in">format</span>(a, b))</span><br></pre></td></tr></table></figure><p>而在CL中则是</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defun</span> bar (<span class="name">&key</span> (<span class="name">a</span> <span class="literal">nil</span>) (<span class="name">b</span> <span class="literal">nil</span>))</span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"a=~A~8Tb=~A~%"</span> a b))</span><br></pre></td></tr></table></figure><p>尽管CL的<code>&key</code>仍然更清晰,但声明参数默认值的语法确实是Python更胜一筹。</p><p>细心的读者可能发现了,在Python中有一个叫做<code>format</code>的方法(属于字符串类),而在CL则有一个叫做<code>format</code>的函数。并且,从上面的例子来看,它们都负责生成格式化的字符串,那么它们有相似之处吗?</p><p>答案是否定的,CL的<code>format</code>简直就是格式化打印界的一股泥石流。</p><h1 id="format的基本用法"><a href="#format的基本用法" class="headerlink" title="format的基本用法"></a><code>format</code>的基本用法</h1><p>不妨从上面的示例代码入手介绍CL中的<code>format</code>(下文在不引起歧义的情况下,简称为<code>format</code>)的基本用法。首先,它需要至少两个参数:</p><ul><li>第一个参数控制了<code>format</code>将会把格式化后的字符串打印到什么地方。<code>t</code>表示打印到标准输出;</li><li>第二个参数则是本文的主角,名为控制字符串(control-string)。它指导<code>format</code>如何格式化。</li></ul><p>听起来很神秘,但其实跟C语言的<code>fprintf</code>也没什么差别。</p><p>在控制字符串中,一般会有许多像占位符一般的命令(directive)。正如Python的<code>format</code>方法中,有各式各样的<a href="https://docs.python.org/3/library/string.html#formatspec">format_spec</a>能够格式化对应类型的数据,控制字符串中的命令也有很多种,常见的有:</p><ul><li>打印二进制数字的<code>~B</code>,例如<code>(format t "~B" 5)</code>会打印出101;</li><li>打印八进制数字的<code>~O</code>,例如<code>(format t "~O" 8)</code>会打印出10;</li><li>打印十进制数字的<code>~D</code>;</li><li>打印十六进制数字的<code>~X</code>,例如<code>(format t "~X" 161)</code>会打印出A1;</li><li>打印任意一种类型的<code>~A</code>,一般打印字符串的时候会用到。</li></ul><p>另外,<code>format</code>的命令也支持参数。在Python中,可以用下列代码打印右对齐的、左侧填充字符0的、二进制形式的数字5</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">print</span>(<span class="string">'{:0>8b}'</span>.<span class="built_in">format</span>(<span class="number">5</span>))</span><br></pre></td></tr></table></figure><p><code>format</code>函数也可以做到同样的事情</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~8,'0B"</span> <span class="number">5</span>)</span><br></pre></td></tr></table></figure><p>到这里为止,你可能会觉得<code>format</code>的控制字符串,不过就是将花括号去掉、冒号换成波浪线,以及参数语法不一样的<code>format</code>方法的翻版罢了。</p><p>接下来,让我们进入<code>format</code>的黑科技领域。</p><h1 id="format的高级用法"><a href="#format的高级用法" class="headerlink" title="format的高级用法"></a><code>format</code>的高级用法</h1><h2 id="进制转换"><a href="#进制转换" class="headerlink" title="进制转换"></a>进制转换</h2><p>前面列举了打印二、八、十,以及十六进制的命令,但<code>format</code>还支持其它的进制。使用命令<code>~R</code>搭配参数,<code>format</code>可以打印数字从2到36进制的所有形态。</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~3R~%"</span> <span class="number">36</span>) <span class="comment">; 以 3进制打印数字36,结果为1100</span></span><br><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~5R~%"</span> <span class="number">36</span>) <span class="comment">; 以 5进制打印数字36,结果为 121</span></span><br><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~7R~%"</span> <span class="number">36</span>) <span class="comment">; 以 7进制打印数字36,结果为 51</span></span><br><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~11R~%"</span> <span class="number">36</span>) <span class="comment">; 以11进制打印数字36,结果为 33</span></span><br><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~13R~%"</span> <span class="number">36</span>) <span class="comment">; 以13进制打印数字36,结果为 2A</span></span><br><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~17R~%"</span> <span class="number">36</span>) <span class="comment">; 以17进制打印数字36,结果为 22</span></span><br><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~19R~%"</span> <span class="number">36</span>) <span class="comment">; 以19进制打印数字36,结果为 1H</span></span><br><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~23R~%"</span> <span class="number">36</span>) <span class="comment">; 以23进制打印数字36,结果为 1D</span></span><br><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~29R~%"</span> <span class="number">36</span>) <span class="comment">; 以29进制打印数字36,结果为 17</span></span><br><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~31R~%"</span> <span class="number">36</span>) <span class="comment">; 以31进制打印数字36,结果为 15</span></span><br></pre></td></tr></table></figure><p>之所以最大为36进制,是因为十个阿拉伯数字,加上二十六个英文字母正好是三十六个。那如果不给<code>~R</code>加任何参数,会使用0进制吗?非也,<code>format</code>会把数字打印成英文单词</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~R~%"</span> <span class="number">123</span>) <span class="comment">; 打印出one hundred twenty-three</span></span><br></pre></td></tr></table></figure><p>甚至可以让<code>format</code>打印罗马数字,只要加上<code>@</code>这个修饰符即可</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~@R~%"</span> <span class="number">123</span>) <span class="comment">; 打印出CXXIII</span></span><br></pre></td></tr></table></figure><p>天晓得为什么要内置这么冷门的功能。</p><h2 id="大小写转换"><a href="#大小写转换" class="headerlink" title="大小写转换"></a>大小写转换</h2><p>你,作为一名细心的读者,可能留意到了,<code>format</code>的<code>~X</code>只能打印出大写字母,而在Python的<code>format</code>方法中,<code>{:x}</code>可以输出小写字母的十六进制数字。即使你在<code>format</code>函数中使用<code>~x</code>也是无效的,因为命令是大小写不敏感的(case insensitive)。</p><p>那要怎么实现打印小写字母的十六进制数字呢?答案是使用新的命令<code>~(</code>,以及它配套的命令<code>~)</code></p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~(~X~)~%"</span> <span class="number">26</span>) <span class="comment">; 打印1a</span></span><br></pre></td></tr></table></figure><p>配合<code>:</code>和<code>@</code>修饰符,一共可以实现四种大小写风格</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~(hello world~)~%"</span>) <span class="comment">; 打印hello world</span></span><br><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~:(hello world~)~%"</span>) <span class="comment">; 打印Hello World</span></span><br><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~@(hello world~)~%"</span>) <span class="comment">; 打印Hello world</span></span><br><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~:@(hello world~)~%"</span>) <span class="comment">; 打印HELLO WORLD</span></span><br></pre></td></tr></table></figure><h2 id="对齐控制"><a href="#对齐控制" class="headerlink" title="对齐控制"></a>对齐控制</h2><p>在Python的<code>format</code>方法中,可以控制打印出的内容的宽度,这一点在“<code>format</code>的基本用法”中已经演示过了。如果设置的最小宽度(在上面的例子中,是8)超过了打印的内容所占据的宽度(在上面的例子中,是3),那么还可以控制其采用左对齐、右对齐,还是居中对齐。</p><p>在CL的<code>format</code>函数中,不管是<code>~B</code>、<code>~D</code>、<code>~O</code>,还是<code>~X</code>,都没有控制对齐方式的选项,数字总是右对齐。要控制对齐方式,需要用到<code>~<</code>和它配套的<code>~></code>。例如,下面的CL代码可以让数字在八个宽度中左对齐</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"|~8<~B~;~>|"</span> <span class="number">5</span>)</span><br></pre></td></tr></table></figure><p>打印内容为<code>|101 |</code>。<code>~<</code>跟前面提到的其它命令不一样,它不消耗控制字符串之后的参数,它只控制<code>~<</code>和<code>~></code>之间的字符串的布局。这意味着,即使<code>~<</code>和<code>~></code>之间是字符串常量,它也可以起作用。</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"|~8,,,'-<~;hello~>|"</span> <span class="number">5</span>)</span><br></pre></td></tr></table></figure><p>上面的代码运行后会打印出<code>|---hello|</code>:8表示用于打印的最小宽度;三个逗号(<code>,</code>)之间为空,表示忽略<code>~<</code>的第二和第三个参数;第四个参数控制着打印结果中用于填充的字符,由于<code>-</code>不是数字,因此需要加上单引号前缀;<code>~;</code>是内部的分隔符,由于它的存在,<code>hello</code>成了最右侧的字符串,因此会被右对齐。</p><p>如果<code>~<</code>和<code>~></code>之间的内容被<code>~;</code>分隔成了三部分,还可以实现左对齐、居中对齐,以及右对齐的效果</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"|~24<left~;middle~;right~>|"</span>) <span class="comment">; 打印出|left middle right|</span></span><br></pre></td></tr></table></figure><h2 id="跳转"><a href="#跳转" class="headerlink" title="跳转"></a>跳转</h2><p>通常情况下,控制字符串中的命令会消耗参数,比如<code>~B</code>和<code>~D</code>等命令。也有像<code>~<</code>这样不消耗参数的命令。但有的命令甚至可以做到“一参多用”,那就是<code>~*</code>。比如,给<code>~*</code>加上冒号修饰,就可以让上一个被消耗的参数重新被消耗一遍</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~8D~:*~8D~8D~%"</span> <span class="number">1</span> <span class="number">2</span>) <span class="comment">; 打印出 1 1 2</span></span><br></pre></td></tr></table></figure><p>在<code>~8D</code>消耗了参数1之后,<code>~:*</code>让下一个被消耗的参数重新指向了1,因此第二个<code>~8D</code>拿到的参数仍然是1,最后一个拿到了2。尽管控制字符串中看起来有三个<code>~D</code>命令而参数只有两个,却依然可以正常打印。</p><p>在<code>format</code>的文档中一个不错的例子,就是让<code>~*</code>和<code>~P</code>搭配使用。<code>~P</code>可以根据它对应的参数是否大于1,来打印出字母<code>s</code>或者什么都不打印。配合<code>~:*</code>就可以实现根据参数打印出单词的单数或复数形式的功能</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~D dog~:*~P~%"</span> <span class="number">1</span>) <span class="comment">; 打印出1 dog</span></span><br><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~D dog~:*~P~%"</span> <span class="number">2</span>) <span class="comment">; 打印出2 dogs</span></span><br></pre></td></tr></table></figure><p>甚至你可以组合一下前面的毕生所学</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~@(~R dog~:*~P~)~%"</span> <span class="number">2</span>) <span class="comment">; 打印出Two dogs</span></span><br></pre></td></tr></table></figure><h2 id="条件打印"><a href="#条件打印" class="headerlink" title="条件打印"></a>条件打印</h2><p>命令<code>~[</code>和<code>~]</code>也是成对出现的,它们的作用是选择性打印,不过比起编程语言中的<code>if</code>,更像是取数组某个下标的元素</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~[~;one~;two~;three~]~%"</span> <span class="number">1</span>) <span class="comment">; 打印one</span></span><br><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~[~;one~;two~;three~]~%"</span> <span class="number">2</span>) <span class="comment">; 打印two</span></span><br><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~[~;one~;two~;three~]~%"</span> <span class="number">3</span>) <span class="comment">; 打印three</span></span><br></pre></td></tr></table></figure><p>但这个特性还挺鸡肋的。想想,你肯定不会无缘无故传入一个数字来作为下标,而这个作为下标的数字很可能本身就是通过<code>position</code>之类的函数计算出来的,而<code>position</code>就要求传入待查找的<code>item</code>和整个列表<code>sequence</code>,而为了用上<code>~[</code>你还得把列表中的每个元素硬编码到控制字符串中,颇有南辕北辙的味道。</p><p>给它加上冒号修饰符之后倒是有点用处,比如可以将CL中的真(<code>NIL</code>以外的所有对象)和假(<code>NIL</code>)打印成单词<code>true</code>和<code>false</code></p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~:[false~;true~]"</span> <span class="literal">nil</span>) <span class="comment">; 打印false</span></span><br></pre></td></tr></table></figure><h2 id="循环打印"><a href="#循环打印" class="headerlink" title="循环打印"></a>循环打印</h2><p>圆括号和方括号都用了,又怎么能少了花括号呢。没错,<code>~{</code>也是一个命令,它的作用是遍历列表。例如,想要打印出一个列表中的每个元素,并且两两之间用逗号和空格分开的话,可以用下列代码</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~{~D~^, ~}"</span> '(<span class="number">1</span> <span class="number">2</span> <span class="number">3</span>)) <span class="comment">; 打印出1, 2, 3</span></span><br></pre></td></tr></table></figure><p><code>~{</code>和<code>~}</code>之间也可以有不止一个命令,例如下列代码中每次会消耗列表中的两个元素</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"{~{\"~A\": ~D~^, ~}}"</span> '(<span class="symbol">:a</span> <span class="number">3</span> <span class="symbol">:b</span> <span class="number">2</span> <span class="symbol">:c</span> <span class="number">1</span>))</span><br></pre></td></tr></table></figure><p>打印结果为<code>{"A": 3, "B": 2, "C": 1}</code>。如果把这两个<code>format</code>表达式拆成用循环写的、不使用<code>format</code>的等价形式,大约是下面这样子</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">; 与(format t "~{~D~^, ~}" '(1 2 3))等价</span></span><br><span class="line">(<span class="name">progn</span></span><br><span class="line"> (<span class="name">do</span> ((<span class="name">lst</span> '(<span class="number">1</span> <span class="number">2</span> <span class="number">3</span>) (<span class="name">cdr</span> lst)))</span><br><span class="line"> ((<span class="name">null</span> lst))</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">e</span> (<span class="name">car</span> lst)))</span><br><span class="line"> (<span class="name">princ</span> e)</span><br><span class="line"> (<span class="name">when</span> (<span class="name">cdr</span> lst)</span><br><span class="line"> (<span class="name">princ</span> <span class="string">", "</span>))))</span><br><span class="line"> (<span class="name">princ</span> #\Newline))</span><br><span class="line"></span><br><span class="line"><span class="comment">; 与(format t "{~{\"~A\": ~D~^, ~}}" '(:a 3 :b 2 :c 1))等价</span></span><br><span class="line">(<span class="name">progn</span></span><br><span class="line"> (<span class="name">princ</span> <span class="string">"{"</span>)</span><br><span class="line"> (<span class="name">do</span> ((<span class="name">lst</span> '(<span class="symbol">:c</span> <span class="number">3</span> <span class="symbol">:b</span> <span class="number">2</span> <span class="symbol">:a</span> <span class="number">1</span>) (<span class="name">cddr</span> lst)))</span><br><span class="line"> ((<span class="name">null</span> lst))</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">key</span> (<span class="name">car</span> lst))</span><br><span class="line"> (<span class="name">val</span> (<span class="name">cadr</span> lst)))</span><br><span class="line"> (<span class="name">princ</span> <span class="string">"\""</span>)</span><br><span class="line"> (<span class="name">princ</span> key)</span><br><span class="line"> (<span class="name">princ</span> <span class="string">"\": "</span>)</span><br><span class="line"> (<span class="name">princ</span> val)</span><br><span class="line"> (<span class="name">when</span> (<span class="name">cddr</span> lst)</span><br><span class="line"> (<span class="name">princ</span> <span class="string">", "</span>))))</span><br><span class="line"> (<span class="name">princ</span> <span class="string">"}"</span>)</span><br><span class="line"> (<span class="name">princ</span> #\Newline))</span><br></pre></td></tr></table></figure><p>这么看来,<code>~{</code>确实可以让使用者写出更紧凑的代码。</p><h2 id="参数化参数"><a href="#参数化参数" class="headerlink" title="参数化参数"></a>参数化参数</h2><p>在前面的例子中,尽管用<code>~R</code>搭配不同的参数可以将数字打印成不同进制的形式,但毕竟这个参数是固化在控制字符串中的,局限性很大。例如,如果我想要定义一个函数<code>print-x-in-base-y</code>,使得参数<code>x</code>可以打印为<code>y</code>进程的形式,那么也许会这么写</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defun</span> print-x-in-base-y (<span class="name">x</span> y)</span><br><span class="line"> (<span class="name">let</span> ((<span class="name">control-string</span> (<span class="name">format</span> <span class="literal">nil</span> <span class="string">"~~~DR"</span> y)))</span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> control-string x)))</span><br></pre></td></tr></table></figure><p>但<code>format</code>的灵活性,允许使用者将命令的前缀参数也放到控制字符串之后的列表中,因此可以写成如下更简练的实现</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defun</span> print-x-in-base-y (<span class="name">x</span> y)</span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"~VR"</span> y x))</span><br></pre></td></tr></table></figure><p>而且不只一个,你可以把所有参数都写成参数的形式</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defun</span> print-x-in-base-y (<span class="name">x</span></span><br><span class="line"> <span class="symbol">&optional</span> y</span><br><span class="line"> <span class="symbol">&rest</span> args</span><br><span class="line"> <span class="symbol">&key</span> mincol padchar commachar commainterval)</span><br><span class="line"> (<span class="name">declare</span> (<span class="name">ignorable</span> args))</span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"~V,V,V,V,VR"</span></span><br><span class="line"> y mincol padchar commachar commainterval x))</span><br></pre></td></tr></table></figure><p>恭喜你重新发明了<code>~R</code>,而且还不支持<code>:</code>和<code>@</code>修饰符。</p><h2 id="自定义命令"><a href="#自定义命令" class="headerlink" title="自定义命令"></a>自定义命令</h2><p>要在CL中打印形如<code>2021-01-29 22:43</code>这样的日期和时间字符串,是一件比较麻烦的事情</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">multiple-value-bind</span> (<span class="name">sec</span> min hour date mon year)</span><br><span class="line"> (<span class="name">decode-universal-time</span> (<span class="name">get-universal-time</span>))</span><br><span class="line"> (<span class="name">declare</span> (<span class="name">ignorable</span> sec))</span><br><span class="line"> (<span class="name">format</span> <span class="literal">t</span> <span class="string">"~4D-~2,'0D-~2,'0D ~2,'0D:~2,'0D~%"</span></span><br><span class="line"> year mon date hour min))</span><br></pre></td></tr></table></figure><p>谁让CL没有内置像Python的<code>datetime</code>模块这般完善的功能呢。不过,借助<code>format</code>的<code>~/</code>命令,我们可以在控制字符串中写上要调用的自定义函数,来深度定制打印出来的内容。以打印上述格式的日期和时间为例,首先定义一个后续要用的自定义函数</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">defun</span> yyyy-mm-dd-HH-MM (<span class="name">dest</span> arg is-colon-p is-at-p <span class="symbol">&rest</span> args)</span><br><span class="line"> (<span class="name">declare</span> (<span class="name">ignorable</span> args is-at-p is-colon-p))</span><br><span class="line"> (<span class="name">multiple-value-bind</span> (<span class="name">sec</span> min hour date mon year)</span><br><span class="line"> (<span class="name">decode-universal-time</span> arg)</span><br><span class="line"> (<span class="name">declare</span> (<span class="name">ignorable</span> sec))</span><br><span class="line"> (<span class="name">format</span> dest <span class="string">"~4D-~2,'0D-~2,'0D ~2,'0D:~2,'0D~%"</span></span><br><span class="line"> year mon date hour min)))</span><br></pre></td></tr></table></figure><p>然后便可以直接在控制字符串中使用它的名字</p><figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name">format</span> <span class="literal">t</span> <span class="string">"~/yyyy-mm-dd-HH-MM/"</span> (<span class="name">get-universal-time</span>))</span><br></pre></td></tr></table></figure><p>在我的机器上运行的时候,打印内容为<code>2021-01-29 22:51</code>。</p><h1 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h1><p><code>format</code>可以做的事情还有很多,CL的HyperSpec中有关于<code>format</code>函数的<a href="http://www.ai.mit.edu/projects/iiip/doc/CommonLISP/HyperSpec/Body/sec_22-3.html">详细介绍</a>,CL爱好者一定不容错过。</p><p>最后,其实Python跟CL并不怎么像。每每看到Python中的<code>__eq__</code>、<code>__ge__</code>,以及<code>__len__</code>等方法的巧妙运用时,身为一名Common Lisp爱好者,我都会流露出羡慕的神情。纵然CL被称为可扩展的编程语言,这些平凡的功能却依旧无法方便地做到呢。</p>]]></content>
<summary type="html"></summary>
<category term="实战CommonLisp" scheme="https://liutos.github.io/categories/%E5%AE%9E%E6%88%98CommonLisp/"/>
<category term="common lisp" scheme="https://liutos.github.io/tags/common-lisp/"/>
<category term="lisp" scheme="https://liutos.github.io/tags/lisp/"/>
<category term="format" scheme="https://liutos.github.io/tags/format/"/>
<category term="格式化打印" scheme="https://liutos.github.io/tags/%E6%A0%BC%E5%BC%8F%E5%8C%96%E6%89%93%E5%8D%B0/"/>
</entry>
</feed>