-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrss2.xml
572 lines (286 loc) · 597 KB
/
rss2.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
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>greyishsong</title>
<link>https://greyishsong.ink/</link>
<image>
<url>https://greyishsong.ink/images/icon.svg</url>
<title>greyishsong</title>
<link>https://greyishsong.ink/</link>
</image>
<atom:link href="https://greyishsong.ink/rss2.xml" rel="self" type="application/rss+xml"/>
<description></description>
<pubDate>Sun, 05 Jan 2025 12:51:18 GMT</pubDate>
<generator>http://hexo.io/</generator>
<item>
<title>图形学实验框架 Dandelion 始末(六):运动和碰撞</title>
<link>https://greyishsong.ink/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E5%85%AD%EF%BC%89%EF%BC%9A%E8%BF%90%E5%8A%A8%E5%92%8C%E7%A2%B0%E6%92%9E/</link>
<guid>https://greyishsong.ink/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E5%85%AD%EF%BC%89%EF%BC%9A%E8%BF%90%E5%8A%A8%E5%92%8C%E7%A2%B0%E6%92%9E/</guid>
<pubDate>Sun, 05 Jan 2025 05:07:46 GMT</pubDate>
<description><p>《始末》系列是关于 Dandelion 1.0 版本的个人回忆,而这是系列里关于实现过程和细节的最后一篇。完成前面的诸多工作之后,利用场景、物体数据和渲染机制,再配合独立性很强的求解器,物体就可以运动起来了。</p></description>
<content:encoded><![CDATA[<p>《始末》系列是关于 Dandelion 1.0 版本的个人回忆,而这是系列里关于实现过程和细节的最后一篇。完成前面的诸多工作之后,利用场景、物体数据和渲染机制,再配合独立性很强的求解器,物体就可以运动起来了。</p><span id="more"></span><h2 id="图形学中的动画">图形学中的动画</h2><p>我把实时的动画渲染粗略分成三种,实现的工作量(未必等于难度)依次增加:</p><ul><li>运动动画:把物体视为<strong>刚体</strong>,根据刚体运动学和动力学改变物体的位置和姿态。</li><li>骨骼动画:把 Mesh 作为蒙皮,绑定到骨骼(端点相互连接的一组刚性杆)上,让 Mesh 随骨骼的运动发生形变和运动。</li><li>物理动画:物体是流体或软体,使用划分微元的方法实时模拟物体各局部的运动,从而产生形变或运动。</li></ul><p>这种划分不怎么严谨,因为上述三种动画是可以相互交叉的。比如物理模拟也包括刚体物理模拟,而刚体模拟的反馈也可以影响骨骼的运动,并没有严格的界限。对 Dandelion 来说,第一种机制最容易实现,后两者则都需要大幅度地完善甚至重构框架。所以,Dandelion 最终只实现了刚体运动动画,甚至还不包括刚体的转动,只完成了平动。这也是我认为 Dandelion 在动画方面最为薄弱的原因。</p><p>不过既然作了这样的取舍,就是想要快速高效地实现刚体的运动和碰撞,而后续的发展也确实如我所愿。</p><h2 id="让画面动起来:解算和更新">让画面动起来:解算和更新</h2><p>发布<a href="https://greyishsong.ink/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%B8%89%EF%BC%89%EF%BC%9AOpenGL-API-%E6%8A%BD%E8%B1%A1%E4%B8%8E%E5%AE%9E%E6%97%B6%E6%B8%B2%E6%9F%93/">实时渲染实现</a>这篇文章到现在几乎过去了一年之久,所以我想先简单回顾一遍 Dandelion 逐帧渲染的流程:</p><blockquote><ol><li>在 <code>Controller::render</code> 函数中,根据 <code>main_camera</code> 的参数计算出 View / Projection 矩阵,并设置 shader 中的 uniform 变量。</li><li>进入 <code>Scene::render</code> 函数,遍历所有的 <code>Object</code> 并调用 <code>Object::render</code> 函数渲染物体。</li><li>进入 <code>Object::render</code> 函数,设置 shader uniform 变量传入 Model 矩阵和材质参数,遍历所有的 <code>GL::Mesh</code> 对象,调用 <code>GL::Mesh::render</code> 渲染 mesh 。</li><li>调用 <code>glDrawArrays</code> / <code>glDrawElements</code> 绘制图元。</li></ol></blockquote><p>在第三步里,<code>Object</code> 的 Model 矩阵是每帧都会发送到 GPU 的,所以我们只要稍加改动,让 Model 矩阵随时间变化,就可以制造平动和转动的效果。而刚体的平动方程非常简单:</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mo fence="true">{</mo><mtable rowspacing="0.36em" columnalign="left left" columnspacing="1em"><mtr><mtd><mstyle scriptlevel="0" displaystyle="false"><mrow><mfrac><mrow><mi mathvariant="normal">d</mi><mi mathvariant="bold">x</mi></mrow><mrow><mi mathvariant="normal">d</mi><mi>t</mi></mrow></mfrac><mo>=</mo><mi mathvariant="bold">v</mi><mo stretchy="false">(</mo><mi>t</mi><mo stretchy="false">)</mo></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="false"><mrow><mfrac><mrow><mi mathvariant="normal">d</mi><mi mathvariant="bold">v</mi></mrow><mrow><mi mathvariant="normal">d</mi><mi>t</mi></mrow></mfrac><mo>=</mo><mi mathvariant="bold">a</mi><mo stretchy="false">(</mo><mi>t</mi><mo stretchy="false">)</mo></mrow></mstyle></mtd></mtr></mtable></mrow><annotation encoding="application/x-tex">\begin{cases} \frac{\mathrm{d}\mathbf{x}}{\mathrm{d}t}=\mathbf{v}(t) \\ \frac{\mathrm{d}\mathbf{v}}{\mathrm{d}t}=\mathbf{a}(t)\end{cases}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:3em;vertical-align:-1.25em;"></span><span class="minner"><span class="mopen delimcenter" style="top:0em;"><span class="delimsizing size4">{</span></span><span class="mord"><span class="mtable"><span class="col-align-l"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.69em;"><span style="top:-3.69em;"><span class="pstrut" style="height:3.008em;"></span><span class="mord"><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.8801em;"><span style="top:-2.655em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathrm mtight">d</span><span class="mord mathnormal mtight">t</span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.394em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathrm mtight">d</span><span class="mord mathbf mtight">x</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.345em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathbf" style="margin-right:0.01597em;">v</span><span class="mopen">(</span><span class="mord mathnormal">t</span><span class="mclose">)</span></span></span><span style="top:-2.25em;"><span class="pstrut" style="height:3.008em;"></span><span class="mord"><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.8801em;"><span style="top:-2.655em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathrm mtight">d</span><span class="mord mathnormal mtight">t</span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.394em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathrm mtight">d</span><span class="mord mathbf mtight" style="margin-right:0.01597em;">v</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.345em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathbf">a</span><span class="mopen">(</span><span class="mord mathnormal">t</span><span class="mclose">)</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:1.19em;"><span></span></span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></p><p>只要给一个边界条件 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="bold">x</mi><mo stretchy="false">(</mo><mn>0</mn><mo stretchy="false">)</mo><mo>=</mo><msub><mi mathvariant="bold">x</mi><mn>0</mn></msub></mrow><annotation encoding="application/x-tex">\mathbf{x}(0)=\mathbf{x}_0</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathbf">x</span><span class="mopen">(</span><span class="mord">0</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.5944em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathbf">x</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">0</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span> 就能定解,再用这个解更新物体的位置 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="bold">x</mi><mo stretchy="false">(</mo><mi>t</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">\mathbf{x}(t)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathbf">x</span><span class="mopen">(</span><span class="mord mathnormal">t</span><span class="mclose">)</span></span></span></span> 就是平动了。求解微分方程的工具就是求解器,在 Dandelion 中,解算器也和渲染器一样是与场景数据解耦的,有统一的接口:输入 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>t</mi></mrow><annotation encoding="application/x-tex">t</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6151em;"></span><span class="mord mathnormal">t</span></span></span></span> 时刻的状态 <code>KineticState</code> 和步长 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">Δ</mi><mi>t</mi></mrow><annotation encoding="application/x-tex">\Delta t</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord">Δ</span><span class="mord mathnormal">t</span></span></span></span>,计算 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>t</mi><mo>+</mo><mi mathvariant="normal">Δ</mi><mi>t</mi></mrow><annotation encoding="application/x-tex">t+\Delta t</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6984em;vertical-align:-0.0833em;"></span><span class="mord mathnormal">t</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord">Δ</span><span class="mord mathnormal">t</span></span></span></span> 时刻的新状态。而这个过程会发生在渲染下一帧之前,也就是在上述第二步和第三步之间插入更新运动状态的操作。</p><blockquote><p>实际上 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">Δ</mi><mi>t</mi></mrow><annotation encoding="application/x-tex">\Delta t</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord">Δ</span><span class="mord mathnormal">t</span></span></span></span> 一般是定值,用于控制微分方程的数值求解精度。而渲染两帧的时间间隔并不是定值,这就要求我们将累加的时间以预设的 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">Δ</mi><mi>t</mi></mrow><annotation encoding="application/x-tex">\Delta t</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord">Δ</span><span class="mord mathnormal">t</span></span></span></span> 为单位重新划分。由于实验手册里已经讲了很多,这里就不再赘述。</p></blockquote><p>到这里,运动就算是完成了——是的,就是这么短,加入的修改也很少:</p><ul><li><code>Scene</code> 中增加了一个状态,区分动画是正在播放还是被暂停。</li><li><code>Object</code> 增加了一个运动状态 <code>KineticState</code>,还增加了一个调用求解器的 <code>Object::update</code> 方法。</li><li><code>Scene::render</code> 中额外做了一些检查,并负责调用 <code>Object::update</code>。</li></ul><p>求解器本身就是一个函数,<code>Object</code> 类有一个静态成员负责保存现在要用的求解器:</p><figure><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp"><span class="token keyword">static</span> std<span class="token double-colon punctuation">::</span>function<span class="token operator"><</span><span class="token function">KineticState</span><span class="token punctuation">(</span><span class="token keyword">const</span> KineticState<span class="token operator">&</span><span class="token punctuation">,</span> <span class="token keyword">const</span> KineticState<span class="token operator">&</span><span class="token punctuation">)</span><span class="token operator">></span> step<span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre></div></figure><h2 id="碰撞与响应">碰撞与响应</h2><p>其实加入碰撞响应的动机非常朴素:在 siyuanluo 学弟写好求解器、我加入状态更新机制以后,我们感觉就这么点儿动画功能是不是太乏味了?这时我恰好想到为渲染器而实现的 BVH 也能用来加速碰撞求解,那就加个碰撞吧!</p><p>我选择的碰撞判定机制是 Mesh 对 Mesh 的离散检测 (Discrete Collision Detection),要在每一帧两两检查物体是否存在穿插,存在则视为碰撞,并回退碰撞物体的位置、修改它们的速度。只要稍微修改 <code>Object::update</code> 轮询其他物体,然后分别试试 <code>naive_intersect</code> 和 <code>BVH::intersect</code> 能否正确检测到碰撞,这部分代码就算完成了。</p><h2 id="回顾">回顾</h2><p>相较于之前的每一段工作,动画部分的编程实在是太快太省,以至于为完善文档多留了一些时间出来。尽管如此,2023 年完成 Dandelion 1.0 版本也消耗了我几乎所有的课余时间——以及一部分工作时间。到研一的期末考试完全结束后,我甚至一度连续工作了 42 天(从 GitHub 提交记录上数出来的),那真是一段忘我投入的日子。1.0 版本从 4 月开发到 9 月,当我敲下 <code>git tag v1.0.0</code> 并在 GitHub 上创建第一个公开 Release 时,与小伙伴们和老师一起工作讨论的经历确实如走马灯一般在眼前闪过。</p><p>相较于许许多多杰出的开源项目,Dandelion 的开发者既不算多也不算强,所以从第一篇到第六篇,我在总是在谈“取舍”,更甚于谈“优化”。这些取舍最终让 1.0 版本得以同正好 100 页的实验手册(以及说不清多少的开发者文档)一起及时发布,但也确实给我留下了许多遐想。与我而言,Dandelion 既是作品也是产品。从作品的角度看待它,我总有新的想法、新的期待和追求;从产品的角度看待它,能够为本科的同学打开一扇初窥图形学的小小窗户,并且保证这窗户不因意外的风雨而破损,这是最重要的。所以,《始末》系列的下一篇也是最后一篇文章中,我想谈谈自己为什么要成为助教、对一门计算机系专业选修课的认识和期望是什么——换言之,在第一个 commit 的起始之前以及 1.0 版本结束之后,我作为 Dandelion 背后“第一作者”的所思所想。</p>]]></content:encoded>
<category domain="https://greyishsong.ink/categories/%E7%BC%96%E7%A8%8B/">编程</category>
<category domain="https://greyishsong.ink/tags/%E5%9B%BE%E5%BD%A2%E5%AD%A6/">图形学</category>
<category domain="https://greyishsong.ink/tags/%E8%AF%BE%E7%A8%8B/">课程</category>
<category domain="https://greyishsong.ink/tags/%E7%BC%96%E7%A8%8B/">编程</category>
<category domain="https://greyishsong.ink/tags/%E6%9C%AC%E7%A7%91%E6%95%99%E8%82%B2/">本科教育</category>
<comments>https://greyishsong.ink/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E5%85%AD%EF%BC%89%EF%BC%9A%E8%BF%90%E5%8A%A8%E5%92%8C%E7%A2%B0%E6%92%9E/#disqus_thread</comments>
</item>
<item>
<title>图形学实验框架 Dandelion 始末(五):交互式几何编辑</title>
<link>https://greyishsong.ink/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%BA%94%EF%BC%89%EF%BC%9A%E4%BA%A4%E4%BA%92%E5%BC%8F%E5%87%A0%E4%BD%95%E7%BC%96%E8%BE%91/</link>
<guid>https://greyishsong.ink/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%BA%94%EF%BC%89%EF%BC%9A%E4%BA%A4%E4%BA%92%E5%BC%8F%E5%87%A0%E4%BD%95%E7%BC%96%E8%BE%91/</guid>
<pubDate>Wed, 01 Jan 2025 03:35:54 GMT</pubDate>
<description><p>原计划一口气写完《始末》这个系列,却在去年年初中断。写下这篇文章的时候已经是 2025 年,实在始料不及。这篇文章着重于 Dandelion “建模模式”背后的交互实现与数据同步机制,而不是几何算法,因为我认为前者是回忆录中更值得回顾的部分。</p></description>
<content:encoded><![CDATA[<p>原计划一口气写完《始末》这个系列,却在去年年初中断。写下这篇文章的时候已经是 2025 年,实在始料不及。这篇文章着重于 Dandelion “建模模式”背后的交互实现与数据同步机制,而不是几何算法,因为我认为前者是回忆录中更值得回顾的部分。</p><span id="more"></span><h2 id="几何编辑的两个核心问题">几何编辑的两个核心问题</h2><p>到<a href="https://greyishsong.ink/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E5%9B%9B%EF%BC%89%EF%BC%9A%E6%B8%B2%E6%9F%93%E6%A8%A1%E5%BC%8F%E4%B8%8E%E8%BD%AF%E6%B8%B2%E6%9F%93%E6%B5%81%E7%A8%8B/">实现渲染模式和渲染器</a>为止,Dandelion 已经是一个能够加载、管理和显示模型数据的小工具了。然而此前的所有功能(以及此后要实现的简单动画)基本上是重计算而轻交互、多流程而少状态的功能。用软光栅渲染器举个例子:它以几何数据 (Mesh)、变换信息、光源为输入,计算出每个像素的渲染结果。整个渲染过程按顶点处理、光栅化、片元处理、写入缓冲的流程执行,每一步读取上一步的输出,计算完成后写入自己的输出变量。虽然为了支持渲染,GUI 和物体数据中包含了一些状态,但这些状态在渲染过程中不会发生改变,整体的渲染流程全部是“前向”的,很容易被拆分成若干个互不影响的函数——也就是耦合度很低的函数。</p><p>就算法本身而言,几何处理算法与渲染流程也是类似的。但为了引入交互式的几何编辑功能,必须实现三维的交互操作和对应的可视化反馈,而 GUI 库对这些功能没有任何支持,这是第一个问题。对于渲染而言,“有状态”的变量最多只是变换、材质和光源,而对于几何编辑而言,“有状态”的变量是所有的几何数据,是图元级别而非物体级别的数据。图元数据不仅数量上非常庞大,还在内存和显存两个位置各有一份副本,所以任何修改首先需要从 UI/UX 层面同步到内存数据,再从内存同步到显存数据,这是第二个问题。</p><p>从功能角度,我们的参考对象 Scotty3D 把这两个问题都解决得不错:</p><ul><li>允许操作者拾取(pick,三维交互方面常用这个词代替“选中”)物体、三角形、边、顶点等多种不同粒度的元素,选中后既有高亮的视觉反馈,也能对所选元素进行各种操作;</li><li>将几何编辑操作划分为“局部操作”与“全局操作”两类,分别对应只影响几个图元的操作与影响整个物体的操作,并分别实现了内存-显存同步机制,保证编辑后能正确重绘画面。</li></ul><p>所以 Dandelion 继承了 Scotty3D 的许多交互设计,以及区分对待几何编辑操作的思想。</p><p>然而,Scotty3D 也有些不尽如人意的地方,其中最成问题的就是性能不佳。在我本科尚未毕业时,我用的还是一台装有 i5 8250U 和 GeForce 940MX 的轻薄本(2018 年产,实际 GPU 性能已经远逊于当代的集成显卡),加载一个几千面的模型并进入编辑模式后,帧数从 60 骤降到 35 左右;而如果加载著名的 Stanford Dragon 模型(约 30 万面),进入编辑模式后甚至达不到 3 帧!2022 年秋季学期上课的同学中,有不少人的笔记本性能甚至更差,以至于验收都无法正常进行。这使我决定从头重新实现所有的交互操作、视觉反馈和数据同步。</p><h2 id="半边网格数据结构">半边网格数据结构</h2><p>在谈实现之前,还是要简单讲讲这个对几何编辑非常重要的数据结构。半边网格 (Halfedge Mesh),是高效查改 Mesh 的一种辅助数据结构。这里我不多谈它的设计和实现,以免长篇大论。不过有个问题是避不开的:为什么几何编辑需要这种数据结构?</p><p>因为常规的 Mesh 存储不能满足几何操作的要求。最常用的 Mesh 数据结构大体上是顶点数组+顶点索引的模式,顶点数组维护属性、顶点索引维护图元。这种存储方式不直接反映任何拓扑上的邻接关系,而各种邻接关系正是几何操作频繁使用的。大多数几何操作的思想继承自微分几何学,遍历各种“邻域”——比如顶点邻域、边邻域——是在 Mesh 上近似微分操作的主要手段,而顶点索引不能为这种操作提供任何便利,所以我们还需要<strong>加速结构</strong>。</p><p>Dandelion 的半边网格 <code>HalfedgeMesh</code> 大量参考了 Scotty3D 的代码,这里再次感谢 CMU 老师和助教们无私的开源贡献。但为了让编写和使用更简洁,我还是在底层更换了不少东西。半边网格是包含大量指针的数据结构,首先要用链表组织半边、顶点、边、面片四种基本图元,然后再用指针维护四种图元之间的邻接关系。Scotty3D 直接使用 <code>std::list</code> 实现双链表,省去了自己实现的麻烦。但 <code>std::list</code> 是非侵入式链表,数据类型不是节点类型,且并没有对外暴露节点类型。偏偏半边网格需要在数据类型内增加节点类型的指针(比如顶点 <code>Vertex</code> 类型内要有指向某个半边的指针),以此提供遍历邻域的能力。最终 Scotty3D 选择全部用迭代器实现指针,并大量定义了 <code>Ref</code> 和 <code>CRef</code> 为后缀的类型别名(比如 <code>using VertexRef = list<Vertex>::iterator</code>)。诚然,迭代器的性能开销不算大,但到处都是的 <code>Ref</code> 类型还是容易让初学者感到迷惑。所以最终我选择全部改用指针实现,包括自己实现侵入式链表(<em>utils/linked_list.hpp</em> 中的 <code>LinkedListNode<T></code> 和 <code>LinkedList<T></code>)、所有图元类型的引用关系全部表示为裸指针。在与指针战斗了不知道多少天之后,这一修改还给 Dandelion 带来了一个副产品:指针类型与引用类型的项目内规范。</p><blockquote><p>简单来说包括这么几条:</p><ul><li>裸指针 <code>T*</code> 表示引用关系,但不表示所有权。换言之,指针 <code>T* x</code> 的持有者不考虑 <code>x</code> 的分配和释放问题。</li><li>引用类型 <code>T&</code> 用于传递参数和返回结果。换言之,引用类型是临时的,表示可以临时访问或者修改某个对象。</li><li>智能指针 <code>unique_ptr</code> 和 <code>shared_ptr</code> 管理所有权,<code>unique_ptr<T> x</code> 的持有者负责分配和释放 <code>x</code>。如果需要传参,先转成引用;如果新构造的对象需要保存 <code>x</code> 的引用关系,先转成裸指针。</li></ul></blockquote><p>引入半边网格有些类似向数据库中加入索引(通常是 B+ 树),能让很多操作执行加快,但也在输入(操作)和底层数据之间增加了一层,需要时时小心确保加速结构和底层数据之间是一致的,增加了很多同步问题。除了接下来要聊的同步方案,Dandelion 还采用一条很激进的策略,那就是只允许编辑单个物体、只保留一个半边网格,一旦退出建模模式,附属于 <code>Scene</code> 对象的 <code>HalfedgeMesh</code> 马上销毁。</p><h2 id="交互:多粒度拾取与内存-显存同步">交互:多粒度拾取与内存-显存同步</h2><p>任何编辑操作的过程都可以简单概括为选中、修改、同步、重绘这几步,而在三维空间中,第一个问题就是如何“选中”。通过光标在屏幕上选择三维实体的操作通称“拾取” (picking),负责把二维的屏幕坐标(点击或者拖动的位置)转换到三维空间,并判断这个坐标在哪个可选中的对象上。形式化一下,拾取操作代表一个将 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mi mathvariant="double-struck">R</mi><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">\mathbb{R}^2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8141em;"></span><span class="mord"><span class="mord mathbb">R</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span> 映射到某个有限集合的函数</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>p</mi><mo>:</mo><msup><mi mathvariant="double-struck">R</mi><mn>2</mn></msup><mo>→</mo><mtext>set of selectable units</mtext></mrow><annotation encoding="application/x-tex">p:\mathbb{R}^2\rightarrow\text{set of selectable units}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.625em;vertical-align:-0.1944em;"></span><span class="mord mathnormal">p</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">:</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8641em;"></span><span class="mord"><span class="mord mathbb">R</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em;"><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">→</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord text"><span class="mord">set of selectable units</span></span></span></span></span></span></p><p>对渲染稍有印象的朋友很容易联想到光线追踪:如果把观察相机的位置 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="bold">e</mi></mrow><annotation encoding="application/x-tex">\mathbf{e}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4444em;"></span><span class="mord mathbf">e</span></span></span></span> 作为原点,窗口视作三维空间中的一个矩形,那么点击的位置可以在矩形上确定一点 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="bold">c</mi></mrow><annotation encoding="application/x-tex">\mathbf{c}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4444em;"></span><span class="mord mathbf">c</span></span></span></span>,这两个点就能确定一条射线。而这条射线类似我们的视线,它打到哪里,就代表我们看到(点击)了哪里,也就代表了我们的选择操作。相应的代码实现和光线追踪完全一样,可以直接用之前写好的射线求交功能。</p><p>不过对于光线追踪而言,射线求交要获取的是坐标、法线和材质;对编辑操作而言,求交要获取的则是图元和物体。如果要拖曳一个顶点,那就要选中顶点;如果要拉伸一条边,那就要选中边,等等。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%BA%94%EF%BC%89%EF%BC%9A%E4%BA%A4%E4%BA%92%E5%BC%8F%E5%87%A0%E4%BD%95%E7%BC%96%E8%BE%91/halfedge-selection.png" alt="选中半边"></div><div class="group-image-wrap"><img src="/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%BA%94%EF%BC%89%EF%BC%9A%E4%BA%A4%E4%BA%92%E5%BC%8F%E5%87%A0%E4%BD%95%E7%BC%96%E8%BE%91/vertex-selection.png" alt="选中顶点"></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%BA%94%EF%BC%89%EF%BC%9A%E4%BA%A4%E4%BA%92%E5%BC%8F%E5%87%A0%E4%BD%95%E7%BC%96%E8%BE%91/edge-selection.png" alt="选中边"></div><div class="group-image-wrap"><img src="/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%BA%94%EF%BC%89%EF%BC%9A%E4%BA%A4%E4%BA%92%E5%BC%8F%E5%87%A0%E4%BD%95%E7%BC%96%E8%BE%91/face-selection.png" alt="选中面片"></div></div></div><p>然而在三维场景中,真实存在的几何数据只有面片,射线也只能和面片求交。连求交都无法实现,如何才能选中其他类型的图元呢?Scotty3D 的做法是进入建模模式后临时增加大量的 Mesh:</p><ul><li>每个顶点处增加一个小球</li><li>每条边上增加一个圆柱</li><li>每条半边上增加一个箭头(也就是一个圆柱+一个圆锥)</li></ul><p>有了 Mesh,这些图元也就都可以用求交选中了。这个思路说起来很简洁,但制造了巨大的性能问题,堪称是一个力大砖飞的解决方案。例如一个立方体有 24 条半边、8 个顶点、18 条边和 12 个三角形面,假如每个小球、圆柱和箭头各有 20 个面,</p><ul><li>在布局模式下,只需要渲染 12 个三角形面;</li><li>在建模模式下,需要渲染足足 1012 个三角形面。</li></ul><p>大致估算 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>F</mi></mrow><annotation encoding="application/x-tex">F</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.13889em;">F</span></span></span></span> 个三角形面的 Mesh 有多少图元时,一般按顶点数 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>V</mi><mo>=</mo><mfrac><mn>1</mn><mn>2</mn></mfrac><mi>F</mi></mrow><annotation encoding="application/x-tex">V=\frac{1}{2}F</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.22222em;">V</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1.1901em;vertical-align:-0.345em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.8451em;"><span style="top:-2.655em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">2</span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.394em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.345em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mord mathnormal" style="margin-right:0.13889em;">F</span></span></span></span>、边数 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>E</mi><mo>=</mo><mfrac><mn>3</mn><mn>2</mn></mfrac><mi>F</mi></mrow><annotation encoding="application/x-tex">E=\frac{3}{2}F</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.05764em;">E</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1.1901em;vertical-align:-0.345em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.8451em;"><span style="top:-2.655em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">2</span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.394em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">3</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.345em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mord mathnormal" style="margin-right:0.13889em;">F</span></span></span></span> 计算,那么半边有 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>H</mi><mo>=</mo><mn>2</mn><mi>E</mi><mo>=</mo><mn>3</mn><mi>F</mi></mrow><annotation encoding="application/x-tex">H=2E=3F</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.08125em;">H</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord">2</span><span class="mord mathnormal" style="margin-right:0.05764em;">E</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord">3</span><span class="mord mathnormal" style="margin-right:0.13889em;">F</span></span></span></span> 条,那么建模模式下就需要额外渲染 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>20</mn><mo stretchy="false">(</mo><mi>V</mi><mo>+</mo><mi>E</mi><mo>+</mo><mi>H</mi><mo stretchy="false">)</mo><mo>=</mo><mn>100</mn><mi>F</mi></mrow><annotation encoding="application/x-tex">20(V+E+H)=100F</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord">20</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.22222em;">V</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.7667em;vertical-align:-0.0833em;"></span><span class="mord mathnormal" style="margin-right:0.05764em;">E</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.08125em;">H</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord">100</span><span class="mord mathnormal" style="margin-right:0.13889em;">F</span></span></span></span> 的面片😱无怪乎加载了五千面的模型后,Scotty3D 帧数会大幅下降——毕竟此时场景中已经有大约五十万面片(两万五千个辅助图元)了,对 940MX 这样的低端显卡来说负担绝对不轻。更加可怕的是,求交操作必须有真实的几何数据才能完成,也就是场景中真的存在两万多个小球、圆柱或箭头的副本,而不是利用 <em>实例化绘制 (Instantiated Draw)</em> 技术渲染出来的“假象”。如此巨大的 Draw Call 和光栅化开销,我是绝对无法接受的。</p><blockquote><p>上面的估算是基于一个理想均匀 Mesh 假设:所有顶点的度(相邻的面片数)都是 6,因此每个顶点被 6 个面共享、每条边被 2 个面共享。折算一下,每个面占有 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>3</mn><mo>×</mo><mfrac><mn>1</mn><mn>6</mn></mfrac><mo>=</mo><mfrac><mn>1</mn><mn>2</mn></mfrac></mrow><annotation encoding="application/x-tex">3\times\frac{1}{6}=\frac{1}{2}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.7278em;vertical-align:-0.0833em;"></span><span class="mord">3</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">×</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1.1901em;vertical-align:-0.345em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.8451em;"><span style="top:-2.655em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">6</span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.394em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.345em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1.1901em;vertical-align:-0.345em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.8451em;"><span style="top:-2.655em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">2</span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.394em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.345em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span> 个顶点、<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>3</mn><mo>×</mo><mfrac><mn>1</mn><mn>2</mn></mfrac><mo>=</mo><mfrac><mn>3</mn><mn>2</mn></mfrac></mrow><annotation encoding="application/x-tex">3\times\frac{1}{2}=\frac{3}{2}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.7278em;vertical-align:-0.0833em;"></span><span class="mord">3</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">×</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1.1901em;vertical-align:-0.345em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.8451em;"><span style="top:-2.655em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">2</span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.394em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.345em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1.1901em;vertical-align:-0.345em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.8451em;"><span style="top:-2.655em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">2</span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.394em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">3</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.345em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span> 条边。</p></blockquote><p>于是我放弃了给每个图元增加 Mesh 的做法,转向另一种比较取巧的思路:能否只和面片求交就判断出使用者想选中的是什么图元呢?顶点和边都是没有体积的,但从使用者的意图角度看,点击面片的边缘可以代表拾取边、点击角可以代表拾取顶点。由于射线求交的返回结果中包含重心坐标,判断相交的位置在不在边缘或顶角上相当容易。获取到重心坐标后,Dandelion 会按如下顺序判断被拾取的是哪种图元:</p><ol><li>射线打中靠近顶角的位置(蓝色区域)时代表顶点;</li><li>打中比较靠近边界的位置(深绿色区域)时代表半边;</li><li>打中靠近边界的位置(浅绿色区域)时代表边;</li><li>打中中间位置(白色区域)时代表面片。</li></ol><p><img src="/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%BA%94%EF%BC%89%EF%BC%9A%E4%BA%A4%E4%BA%92%E5%BC%8F%E5%87%A0%E4%BD%95%E7%BC%96%E8%BE%91/selection-range.svg" alt="不同图元对应的相交区域"></p><p>借助这种间接选择的方法,无论要拾取的是哪种图元,射线都只需要与面片求交一次,其他图元则不必增加实际存在的 Mesh,也就不必面对 Scotty3D 那样巨大的开销了。</p><p>现在我有了拾取操作的实现方案,不过距离真正实现还差一些技术细节。这些细节包括 UI 层面的拾取状态管理,以及 UI→Halfedge Mesh→Mesh→VRAM Buffer 的整个同步过程。</p><p>在不同的模式下,可以被拾取的对象包括物体和各种图元,而可以被选中的对象还包括光源。再加上“什么都没有选中”的情况,选择/拾取操作可能的状态会有很多种。加上建模模式下的拾取操作与其他模式不一样,完整的选择/拾取操作逻辑是比较复杂的。整理过后,我给出的实现方案是:</p><ul><li>用 <code>Controller::on_picking</code> 作为统一入口,响应鼠标点击、生成拾取射线,并根据当前的模式决定转到 <code>pick_object</code> 还是 <code>pick_element</code></li><li><code>Controller::pick_object</code> 与 <code>Controller::pick_element</code> 的逻辑很相似,都是先求交然后根据交点判断是否执行拾取,再调用 <code>select</code></li><li><code>Controller::select</code> 将直观的“拾取”转换为实际的“选择”,会根据要选择的对象类型调用 <code>select_object</code>/<code>select_halfedge</code>/<code>select_vertex</code> 等不同的函数,最终修改 <code>Controller::selected_element</code></li></ul><p>如果只考虑之前的求交、拾取逻辑,<code>select</code> 与那一大堆 <code>select_</code> 前缀的函数似乎都有些多余,让这个实现显得很复杂。它们的实际作用也的确不是处理空间上的拾取逻辑,而是更新拾取后的渲染数据,让被选的对象高亮显示。由于实时渲染要先设置数据和状态才能更新画面,我甚至还要再增加一个 <code>unselect</code> 函数,专门负责清理与之前被选对象关联的状态,以免多出一些不该有的高亮来。</p><p>完成这些部分之后,选择状态就保存在了 <code>Controller::selected_element</code> 这个属性中。它是一个 <code>SelectableType</code> 类型的变量,而 <code>SelectableType</code> 则是所有可选中类型的联合:</p><figure><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp"><span class="token keyword">using</span> SelectableType <span class="token operator">=</span> std<span class="token double-colon punctuation">::</span>variant<span class="token operator"><</span>std<span class="token double-colon punctuation">::</span>monostate<span class="token punctuation">,</span> Object<span class="token operator">*</span><span class="token punctuation">,</span> <span class="token keyword">const</span> Halfedge<span class="token operator">*</span><span class="token punctuation">,</span> Vertex<span class="token operator">*</span><span class="token punctuation">,</span> Edge<span class="token operator">*</span><span class="token punctuation">,</span> Face<span class="token operator">*</span><span class="token punctuation">,</span> Light<span class="token operator">*</span><span class="token operator">></span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre></div></figure><p>当使用者拾取了一个顶点后,<code>select</code> 方法就会更新 <code>selected_element</code> 属性,并附带更新 <code>HalfedgeMesh::inconsistent_element</code>——这代表半边网格上此顶点的信息需要逐帧同步到 <code>GL::Mesh::vertices</code> 中对应的顶点。当使用者通过 toolbar 上的滑动条改变顶点坐标时,<code>Scene::render</code> 方法就会调用 <code>HalfedgeMesh::sync</code> 方法,在渲染画面前完成 <code>HalfedgeMesh</code>→<code>GL::Mesh</code> 的同步(包括内存和显存)。</p><p><img src="/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%BA%94%EF%BC%89%EF%BC%9A%E4%BA%A4%E4%BA%92%E5%BC%8F%E5%87%A0%E4%BD%95%E7%BC%96%E8%BE%91/vertex-sync.png" alt="修改一个顶点坐标时的同步过程(橙色标出的部分属于 UI 层面,绿色部分是背后的逻辑和渲染)"></p><p>不得不说,我在写这部分代码之前,从来没有如此频繁地使用过 <code>std::variant</code>、<code>std::visit</code> 和各种各样的 lambda 表达式。甚至还因为与静态变量、静态 lambda 表达式相关的一些生命周期问题,度过了若干苦苦调试的白天——因为那段时间持续无休工作,夜晚早就完全没精神了。</p><blockquote><p>上面谈到的只是局部形变的同步方式,仅适用于单个顶点/边/面片的修改。对于各种改变拓扑的局部操作,以及 Mesh 细分、简化之类的全局操作,Dandelion 会执行全局同步。全局同步由 <code>HalfedgeMesh::global_inconsistent</code> 标记触发,会彻底重写 <code>GL::Mesh</code> 的所有数据。这种同步方式比较常规,这里就简单带过。</p></blockquote><h2 id="渲染:辅助图元与视觉反馈">渲染:辅助图元与视觉反馈</h2><p>刚才说图元拾取的时候,我提到</p><blockquote><p>无论要拾取的是哪种图元,射线都只需要与面片求交一次,其他图元则不必增加实际存在的 Mesh</p></blockquote><p>然而尽管无需使用大量的 Mesh,半边、顶点这些图元还是要渲染出来的。而且,如果某个图元被选中了,那么还有必要用另一种颜色高亮显示,提供最基本的视觉反馈。</p><p>既然不必用实际存在的 Mesh 来表示图元,那为了降低渲染开销,我就要搬出实时渲染第一要诀了:<strong>近似一下,能看就行</strong>。所以在 Dandelion 中,</p><ul><li>顶点是用 <code>GL_POINT</code> 渲染的,通过增大 point size 来避免太小看不见的问题;</li><li>同理,边是用 <code>GL_LINES</code> 渲染的,调大了 line width;</li><li>半边的箭头呢?其实它不是真的箭头,而是五条线段伪装出来的。用重心坐标控制作为轴的线段保持在离边很近但又不重叠的位置,再用相对于轴的坐标来添上四条短线段。所有的坐标(长度和位置)都是相对的,无论面片大小都不会出错。</li></ul><p>这样一来,绘制辅助图元所需的数据量比 Scotty3D 少了一个量级;而无需几何求交意味着同类的辅助图元可以被并入一个 Draw Call,每帧也只需要增加三个而不是成千上万个 Draw Call。从算法的角度去评估,这大概算不上什么优化,称为取舍——效果和性能之间的取舍——也许更合适。不过这个取舍带来了极明显的性能提升:在两年的教学中,实时渲染性能不再是完成实验的瓶颈,我们再也没有遇到过同学的电脑跑不动实验的事情,这足以让我满意了。</p><p>为了尽可能加快渲染,除了面片以外的所有图元都是没有着色计算的,直接使用全局统一的颜色,不存在顶点色和存储顶点颜色的 Buffer。如果我要让选中的图元渲染为蓝色🔵,而其他图元都维持黄色🟡,那么我就不得不在选中时从 Buffer 中删去这个图元,然后在取消选择时把它加回去。对一个很长很长(可能有几万、几十万)图元的 Buffer(相当于数组)做随机删除的代价太大了,这不好。不过我们需要的是高亮效果,只要能看到就可以。那么只要先渲染所有图元,再把被选中的图元复制出来单独渲染盖上去就好了。<code>Controller</code> 类有一个 <code>GL::Mesh</code> 成员 <code>highlighted_element</code> 专门用来存放复制出来的图元,每一帧绘制完其他所有图元之后,会关闭深度测试再把它叠加绘制上去。</p><blockquote><p>如果这种做法继续推广,就得到了分层 (layer) 的渲染策略。比如场景内的物体属于视图层,这种特殊的高亮属于 UI 层,通过 Draw Call 排序实现不同层按顺序叠加。现在 Dandelion 还没有那么多样的渲染需求,所以也没有作正式的分层和 Draw Call 重排处理。不过这是 2.0 版本重要的优化特性之一,期待能早日实现。</p></blockquote><h2 id="下回分解">下回分解</h2><p>讲到这里,Dandelion 的搭建工作已经进行了 80%。最后的一部分工作是添加物理动画,但我对此的了解实在少得可怜,虽然在 SiyuanLuo 学弟的帮助下完成了一些简单的模拟,但也还是没有多少值得一提的东西。有的朋友可能还是对这篇文章没有提及几何处理算法感到奇怪,那是因为相比于在框架上实现算法,我更想回忆的是尝试实现交互式编辑的经历。毕竟我所实现的算法都十分经典,又有 Scotty3D 的文档(虽然这部分没有源码)可供参考,一写不好就显得寡淡。</p><p>下一篇文章我会简单聊聊 Dandelion 现有的物理动画,之后就是这个系列的尾声——也是我四年助教生涯的尾声了,我想谈谈自己为什么选择做助教、为什么选择做课改,以及四年间对交大计算机本科教学的一些思考。</p>]]></content:encoded>
<category domain="https://greyishsong.ink/categories/%E7%BC%96%E7%A8%8B/">编程</category>
<category domain="https://greyishsong.ink/tags/%E5%9B%BE%E5%BD%A2%E5%AD%A6/">图形学</category>
<category domain="https://greyishsong.ink/tags/%E8%AF%BE%E7%A8%8B/">课程</category>
<category domain="https://greyishsong.ink/tags/%E7%BC%96%E7%A8%8B/">编程</category>
<category domain="https://greyishsong.ink/tags/%E6%9C%AC%E7%A7%91%E6%95%99%E8%82%B2/">本科教育</category>
<comments>https://greyishsong.ink/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%BA%94%EF%BC%89%EF%BC%9A%E4%BA%A4%E4%BA%92%E5%BC%8F%E5%87%A0%E4%BD%95%E7%BC%96%E8%BE%91/#disqus_thread</comments>
</item>
<item>
<title>西当太白有鸟道</title>
<link>https://greyishsong.ink/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/</link>
<guid>https://greyishsong.ink/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/</guid>
<pubDate>Wed, 13 Nov 2024 13:16:04 GMT</pubDate>
<description><p>神仙居所,凡夫难入。</p></description>
<content:encoded><![CDATA[<p>神仙居所,凡夫难入。</p><span id="more"></span><p>这是我第一次进入秦岭,路程不长,自下板寺至天圆地方。整日阴晦,少有光亮。到大约三千米海拔时,体验了人生第一次高原反应:晕头转向,全凭登山杖支撑,所幸还是到了。现在不想写,也许以后会补些文字。</p><p><img src="/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/DSC_2921.jpg" alt="从略高于下板寺的观景平台上俯瞰山谷"></p><p><img src="/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/DSC_2924.jpg" alt="观景平台的大理石栏杆上可见地衣"></p><p><img src="/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/DSC_2928.jpg" alt="抬头正好有两只乌鸦前后飞过"></p><p><img src="/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/DSC_2930.jpg" alt="远方山岭层叠"></p><p><img src="/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/DSC_2940.jpg" alt="左手边大概是工作人员临时停驻用的屋子"></p><p><img src="/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/DSC_2944.jpg" alt="稍稍向上,云层破开一角"></p><p><img src="/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/DSC_2957.jpg" alt="回望到坐车下板寺时经过的盘山路"></p><p><img src="/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/DSC_2959.jpg" alt="转眼又看不到天空了"></p><p><img src="/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/DSC_2965.jpg" alt="再向上"></p><p><img src="/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/DSC_2970.jpg" alt="一些不认识的植物"></p><p><img src="/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/DSC_2971.jpg" alt="转瞬即逝的阳光照在林地上"></p><p><img src="/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/DSC_2974.jpg" alt="此处以少有草本,冷杉下多是苔藓"></p><p><img src="/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/DSC_2977.jpg" alt=""></p><p><img src="/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/DSC_2979.jpg" alt="树干上也有厚厚一层"></p><p><img src="/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/DSC_2981.jpg" alt="转角处已看不到山谷"></p><p><img src="/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/DSC_2987.jpg" alt="岩石背面苔藓偏红"></p><p><img src="/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/DSC_2993.jpg" alt="歇脚处的台阶上也长着苔藓"></p><p><img src="/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/DSC_2997.jpg" alt="树根"></p><p><img src="/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/DSC_2998.jpg" alt="空气污染极少,地衣很繁盛"></p><p><img src="/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/DSC_3007.jpg" alt="看似幽径,通往不可知处"></p><p><img src="/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/DSC_3008.jpg" alt="地质知识匮乏,不知是不是大堆方解石"></p><p><img src="/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/DSC_3011.jpg" alt="倒下的树干"></p><p><img src="/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/DSC_3022.jpg" alt="洒在扶手上的阳光,之后再也没见到太阳"></p><p><img src="/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/DSC_3023.jpg" alt="看向山峰时已经不太需要仰视"></p><p><img src="/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/DSC_3026.jpg" alt="山顶大雾,只见竖在眼前的刻石"></p><p><img src="/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/DSC_3028.jpg" alt="下山时天气愈发阴郁"></p>]]></content:encoded>
<category domain="https://greyishsong.ink/categories/%E9%9A%8F%E7%AC%94/">随笔</category>
<category domain="https://greyishsong.ink/tags/%E7%94%9F%E6%B4%BB/">生活</category>
<category domain="https://greyishsong.ink/tags/%E6%8B%8D%E6%91%84/">拍摄</category>
<comments>https://greyishsong.ink/%E8%A5%BF%E5%BD%93%E5%A4%AA%E7%99%BD%E6%9C%89%E9%B8%9F%E9%81%93/#disqus_thread</comments>
</item>
<item>
<title>初夏青龙寺小记</title>
<link>https://greyishsong.ink/%E5%88%9D%E5%A4%8F%E9%9D%92%E9%BE%99%E5%AF%BA%E5%B0%8F%E8%AE%B0/</link>
<guid>https://greyishsong.ink/%E5%88%9D%E5%A4%8F%E9%9D%92%E9%BE%99%E5%AF%BA%E5%B0%8F%E8%AE%B0/</guid>
<pubDate>Thu, 16 May 2024 08:45:20 GMT</pubDate>
<description><p>忙得有些生厌,遂出门闲逛。乍一想起来西安四年半了还未去过青龙寺,转念又想,没去过的地方且多了去,想到哪里去哪里逛逛就是了。</p>
<p>看过才知道,青龙寺实名青龙寺遗址,而所谓遗址当指的是被发掘的若干地基及文物,并无地上建筑存留。今之青龙寺遗址博物馆及寺院,多半是钢筋混凝</description>
<content:encoded><![CDATA[<p>忙得有些生厌,遂出门闲逛。乍一想起来西安四年半了还未去过青龙寺,转念又想,没去过的地方且多了去,想到哪里去哪里逛逛就是了。</p><p>看过才知道,青龙寺实名青龙寺遗址,而所谓遗址当指的是被发掘的若干地基及文物,并无地上建筑存留。今之青龙寺遗址博物馆及寺院,多半是钢筋混凝土结构——样式不错,不过在西安也不算出奇。</p><p>此时立夏已一周,牡丹之类俱已开败,园子里已找不到多少花。然而初夏之际,草木茂盛最为可观,加上游人寥寥,是个散步的好去处。枫、柏、竹、迎春、南天竹等等,正值长势最盛、枝叶最富生气之时,而明亮的日光淹没了耳旁的种种声响,将许多枝叶都点亮起来。</p><p><img src="/%E5%88%9D%E5%A4%8F%E9%9D%92%E9%BE%99%E5%AF%BA%E5%B0%8F%E8%AE%B0/DSC_2744.JPG" alt=""></p><p><img src="/%E5%88%9D%E5%A4%8F%E9%9D%92%E9%BE%99%E5%AF%BA%E5%B0%8F%E8%AE%B0/DSC_2749.JPG" alt=""></p><p><img src="/%E5%88%9D%E5%A4%8F%E9%9D%92%E9%BE%99%E5%AF%BA%E5%B0%8F%E8%AE%B0/DSC_2751.JPG" alt=""></p><p><img src="/%E5%88%9D%E5%A4%8F%E9%9D%92%E9%BE%99%E5%AF%BA%E5%B0%8F%E8%AE%B0/DSC_2755.JPG" alt=""></p><p><img src="/%E5%88%9D%E5%A4%8F%E9%9D%92%E9%BE%99%E5%AF%BA%E5%B0%8F%E8%AE%B0/DSC_2757.JPG" alt=""></p><p><img src="/%E5%88%9D%E5%A4%8F%E9%9D%92%E9%BE%99%E5%AF%BA%E5%B0%8F%E8%AE%B0/DSC_2758.JPG" alt=""></p><p>柏树枝、叶,尤其是新叶,尚未显出许多文章中所赞颂的苍翠颜色。然而阳光一照、镜头拉近,满眼是生机热烈,反而是顾不上什么苍翠厚重了。</p><p><img src="/%E5%88%9D%E5%A4%8F%E9%9D%92%E9%BE%99%E5%AF%BA%E5%B0%8F%E8%AE%B0/DSC_2760.JPG" alt=""></p><p><img src="/%E5%88%9D%E5%A4%8F%E9%9D%92%E9%BE%99%E5%AF%BA%E5%B0%8F%E8%AE%B0/DSC_2763.JPG" alt=""></p><p><img src="/%E5%88%9D%E5%A4%8F%E9%9D%92%E9%BE%99%E5%AF%BA%E5%B0%8F%E8%AE%B0/DSC_2767.JPG" alt=""></p><p><img src="/%E5%88%9D%E5%A4%8F%E9%9D%92%E9%BE%99%E5%AF%BA%E5%B0%8F%E8%AE%B0/DSC_2768.JPG" alt=""></p><p><img src="/%E5%88%9D%E5%A4%8F%E9%9D%92%E9%BE%99%E5%AF%BA%E5%B0%8F%E8%AE%B0/DSC_2770.JPG" alt=""></p><p><img src="/%E5%88%9D%E5%A4%8F%E9%9D%92%E9%BE%99%E5%AF%BA%E5%B0%8F%E8%AE%B0/DSC_2774.JPG" alt=""></p><p><img src="/%E5%88%9D%E5%A4%8F%E9%9D%92%E9%BE%99%E5%AF%BA%E5%B0%8F%E8%AE%B0/DSC_2776.JPG" alt=""></p><p><img src="/%E5%88%9D%E5%A4%8F%E9%9D%92%E9%BE%99%E5%AF%BA%E5%B0%8F%E8%AE%B0/DSC_2777.JPG" alt=""></p><p><img src="/%E5%88%9D%E5%A4%8F%E9%9D%92%E9%BE%99%E5%AF%BA%E5%B0%8F%E8%AE%B0/DSC_2780.JPG" alt=""></p><p><img src="/%E5%88%9D%E5%A4%8F%E9%9D%92%E9%BE%99%E5%AF%BA%E5%B0%8F%E8%AE%B0/DSC_2787.JPG" alt=""></p><p><img src="/%E5%88%9D%E5%A4%8F%E9%9D%92%E9%BE%99%E5%AF%BA%E5%B0%8F%E8%AE%B0/DSC_2798.JPG" alt=""></p><p><img src="/%E5%88%9D%E5%A4%8F%E9%9D%92%E9%BE%99%E5%AF%BA%E5%B0%8F%E8%AE%B0/DSC_2799.JPG" alt=""></p><p><img src="/%E5%88%9D%E5%A4%8F%E9%9D%92%E9%BE%99%E5%AF%BA%E5%B0%8F%E8%AE%B0/DSC_2801.JPG" alt=""></p><p><img src="/%E5%88%9D%E5%A4%8F%E9%9D%92%E9%BE%99%E5%AF%BA%E5%B0%8F%E8%AE%B0/DSC_2805.JPG" alt=""></p><p><img src="/%E5%88%9D%E5%A4%8F%E9%9D%92%E9%BE%99%E5%AF%BA%E5%B0%8F%E8%AE%B0/DSC_2807.JPG" alt=""></p>]]></content:encoded>
<category domain="https://greyishsong.ink/categories/%E9%9A%8F%E7%AC%94/">随笔</category>
<category domain="https://greyishsong.ink/tags/%E7%94%9F%E6%B4%BB/">生活</category>
<category domain="https://greyishsong.ink/tags/%E6%8B%8D%E6%91%84/">拍摄</category>
<comments>https://greyishsong.ink/%E5%88%9D%E5%A4%8F%E9%9D%92%E9%BE%99%E5%AF%BA%E5%B0%8F%E8%AE%B0/#disqus_thread</comments>
</item>
<item>
<title>图形学实验框架 Dandelion 始末(四):渲染模式与软渲染流程</title>
<link>https://greyishsong.ink/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E5%9B%9B%EF%BC%89%EF%BC%9A%E6%B8%B2%E6%9F%93%E6%A8%A1%E5%BC%8F%E4%B8%8E%E8%BD%AF%E6%B8%B2%E6%9F%93%E6%B5%81%E7%A8%8B/</link>
<guid>https://greyishsong.ink/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E5%9B%9B%EF%BC%89%EF%BC%9A%E6%B8%B2%E6%9F%93%E6%A8%A1%E5%BC%8F%E4%B8%8E%E8%BD%AF%E6%B8%B2%E6%9F%93%E6%B5%81%E7%A8%8B/</guid>
<pubDate>Mon, 05 Feb 2024 02:47:46 GMT</pubDate>
<description><p>GAMES 101 的实验中包含了经典的光栅化渲染、Whitted-Style Ray-Tracing 和路径追踪,加上闫老师高质量的课程内容,已经把局部光照渲染和全局光照渲染这两种视角展示得很不错了。但我从开始做 101 的实验时就产生了一个遗憾——为什么不能用我自己编写的工具来渲染任何我想要渲染的模型呢?而这正是 Dandelion 想要弥补的地方。</p></description>
<content:encoded><![CDATA[<p>GAMES 101 的实验中包含了经典的光栅化渲染、Whitted-Style Ray-Tracing 和路径追踪,加上闫老师高质量的课程内容,已经把局部光照渲染和全局光照渲染这两种视角展示得很不错了。但我从开始做 101 的实验时就产生了一个遗憾——为什么不能用我自己编写的工具来渲染任何我想要渲染的模型呢?而这正是 Dandelion 想要弥补的地方。</p><span id="more"></span><h2 id="软件渲染的工作方式">软件渲染的工作方式</h2><p>如今硬件渲染管线已经主宰了大多数渲染应用领域,那我为什么还要选择软件渲染呢?这也是受到闫老师 GAMES 101 实验的思路的启发,即一门基础课程应该介绍渲染管线,而不是图形 API ;偏重流程、光照和着色,避免过度强调性能优化。</p><h3 id="渲染引擎与渲染器">渲染引擎与渲染器</h3><p>回顾第一篇文章里我所设想的“功能性要求”:</p><blockquote><p>由于我们希望这个框架能实现各种渲染流程,显然它应该像 Blender 一样可以切换“渲染器”,每个渲染器都对应一种渲染管线。为避免某个实验过于艰深,可以牺牲管线的灵活程度,只保留较少的可编程部分。</p></blockquote><p>为了实现它,我将一次渲染的过程拆分为两部分:</p><ul><li>与渲染算法甚至渲染管线基本无关的部分,包括读入场景、输出 frame buffer 、展示到屏幕上等等</li><li>根据场景、光源和视角生成图像(frame buffer 内容)的部分</li></ul><p>在 Dandelion 中,第一部分由“渲染引擎” <code>RenderEngine</code> 承载,第二部分由“渲染器” <code>Renderer</code> 实现。从界面上启动一次渲染的流程是这样的:</p><ol><li>编辑好场景,设定光源和相机</li><li>点击 <em>Render to Image</em> 按钮,UI 组件调用 <code>RenderEngine::render</code> 方法,传入场景和所选的渲染器开始渲染</li><li><code>RenderEngine</code> 根据传入的类型调用相应渲染器的 <code>render</code> 方法</li><li>渲染器将渲染图写入到 <code>RenderEngine::render_res</code> 中</li><li>调用 <code>glTexImage2D</code> 将 <code>RenderEngine::render_res</code> 转换为一张纹理贴图,然后用 <code>ImGui::Image</code> 展示出来</li></ol><p>在整个流程中,<code>RenderEngine</code> 的主要作用就是传参和提供 buffer ,有些类似设计模式里提到的的适配器 (Adapter) ,主要作用是统一接口。在此之下,真正完成着色计算的是渲染器。</p><p><img src="/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E5%9B%9B%EF%BC%89%EF%BC%9A%E6%B8%B2%E6%9F%93%E6%A8%A1%E5%BC%8F%E4%B8%8E%E8%BD%AF%E6%B8%B2%E6%9F%93%E6%B5%81%E7%A8%8B/rendering.svg" alt="RenderEngine 与 Renderer 之间的数据流向"></p><p>由于 Dandelion 管理场景对象的部分与渲染模块是分离的,渲染器按照约定的层次遍历场景即可获得渲染所需的数据,不必关心读取和解析文件的问题;不同渲染器之间可以共用的配置则存储于 <code>RenderEngine</code> 中,在调用渲染器渲染时传入。</p><h3 id="渲染管线">渲染管线</h3><p>虽然在界面上有三个渲染器选项,但实际上 Dandelion 只实现了两个渲染器,分别是 <code>RasterizerRenderer</code> 和 <code>WhittedRenderer</code> 。这两个渲染器的主干代码分别移植自 GAMES 101 的实验 3 和实验 5 ,不过为了将其适配到 Dandelion 中作了不少修改。我们也修复了 GAMES 101 作业中的一些 bug ,例如缺少正确的透视矫正等等。这部分工作主要由我的同门 JoTaiLang 完成,他几乎将数月的业余时间全部投入到了渲染器的移植和调试中,没有他的帮助,我恐怕绝不可能在七月初完成 1.0 版本。</p><p><img src="/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E5%9B%9B%EF%BC%89%EF%BC%9A%E6%B8%B2%E6%9F%93%E6%A8%A1%E5%BC%8F%E4%B8%8E%E8%BD%AF%E6%B8%B2%E6%9F%93%E6%B5%81%E7%A8%8B/renderer-options.png" alt="界面上的渲染器选项"></p><p><code>RasterizerRenderer</code> 配合另一个类 <code>Rasterizer</code> 实现了包含 Vertex Processing、Rasterization、Fragment Processing 和 Blending 四个阶段的极简光栅化管线,支持 z-buffer 消隐。每个 shader 是一个函数,有相应的 payload 作为输入。不过由于需要处理的变量更多,代码中增加了一个静态结构体 <code>Uniforms</code> ,所有的 shader 都可以从中读取“全局变量”。由于这两个 shader 都是通过 <code>std::function</code> 指定,开发者也可以像使用 GLSL shader 那样修改其中的内容甚至在运行时切换 shader 。渲染时由 <code>RasterizerRenderer::render</code> 将场景中的 mesh 整理成无序的 <code>TriangleList</code> 送入 <code>Rasterizer::draw</code> ,依次对每个 <code>Triangle</code> 进行顶点处理、光栅化和片元处理。</p><p><img src="/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E5%9B%9B%EF%BC%89%EF%BC%9A%E6%B8%B2%E6%9F%93%E6%A8%A1%E5%BC%8F%E4%B8%8E%E8%BD%AF%E6%B8%B2%E6%9F%93%E6%B5%81%E7%A8%8B/rasterizer-renderer.svg" alt="当前的光栅化渲染管线"></p><p>上图中蓝色箭头表示数据流动方向,红色箭头表示函数调用关系,可以看到渲染的核心逻辑都集中在 <code>Rasterizer</code> 内部。但为什么渲染器还要调用 <code>Rasterizer</code> 才能完成渲染呢?这是沿用 GAMES 101 实验框架设计的结果,尽管我不太能理解这样做的意义何在——相反,我甚至觉得这样的设计并不是很好,至少当前就体现出了两个问题:</p><ul><li><code>RenderEngine</code> 是静态对象(由 <code>UI::Toolbar</code> 构造),Renderer 也是静态对象(由 <code>RenderEngine</code> 构造),只有 <code>Rasterizer</code> 在每次渲染时重新构造然后销毁。由于 <code>Rasterizer</code> 中保存了大量的参数和中间结果(例如 MVP 变换、frame buffer 副本和 depth buffer),在 frame buffer 和 depth buffer 每次都需要手动覆写(用背景色或无穷大填充)的情况下,重新构造它们既不能省去覆写的代码,也不能提高执行的效率,这样的设计就显得很多余了。从直觉角度来说,光栅化器和要渲染的场景是无关的,对应到代码中也应该是静态的(或者至少是固定的)。</li><li>我们有一个四阶段的管线,但实际上这四个阶段都不在 <code>RasterizerRenderer::render</code> 函数内执行,而是在 <code>Rasterizer::draw</code> 函数内执行,前者近乎是后者的简单包装。把调用 vertex shader 和 fragment shader 的代码写到 <code>Rasterizer</code> 当中,后来者再读代码时便很容易感到迷惑:这个光栅化器到底是负责光栅化还是整个渲染流程的?如果它负责执行整个流程,那光栅化渲染器又起什么作用?</li></ul><p>当然,我们依然非常感谢 GAMES 101 实验的设计者们给出了功能大致正确并且风格比较规整的代码框架。在初步完成 Dandelion 的过程中,有限的水平和比较紧张的时间一度让我们相当焦虑,因而没有大改光栅化渲染器的结构,基本上就是将原先的顶层入口替换成了 <code>RasterizerRenderer</code> ,再将原先的 <code>Rasterizer</code> 类移植进来并适配 Dandelion 的场景格式。这部分代码在 2.0 版本中多半需要略作重构,至少要避开每次重新构造 <code>Rasterizer</code> 的尴尬。</p><p>话说回来,我们感到时间紧张一方面是因为水平有限,另一方面是因为较大幅度地修改了光线追踪的代码。相较于所有代码都在 <em>渲染</em> 子模块下的光栅化渲染器,Whitted-Style 光线追踪渲染器的实现则分散在 <em>渲染 render</em> 与 <em>工具 utils</em> 两个子模块中。我们首先从 GAMES 101 实验中拆解出负责着色的代码,然后将其移植到渲染子模块;再拆解出射线求交和 BVH 加速的代码,半移植半重写地加入工具子模块。</p><p><img src="/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E5%9B%9B%EF%BC%89%EF%BC%9A%E6%B8%B2%E6%9F%93%E6%A8%A1%E5%BC%8F%E4%B8%8E%E8%BD%AF%E6%B8%B2%E6%9F%93%E6%B5%81%E7%A8%8B/whitted-renderer.svg" alt="Whitted-Style 光线追踪渲染管线"></p><p>图中代表函数的黄色框分成上下两半,上半部分是 <code>WhittedRenderer</code> 自身的方法,下半部分则是 <em>utils</em> 中的工具函数。</p><p>这一设计思路来自于我对 CMU 15/462 实验框架 Scotty3D 的使用经验:它也是个全功能交互式框架,但在渲染方面只考虑了路径追踪 (Path Tracing) 技术,将射线生成、射线求交、BVH 加速等代码全部置于 <code>PT</code> 这个命名空间下,相当于将这些功能统一划归路径追踪渲染器。然而,拾取操作(在屏幕上点击物体就可以选中它,通常被叫做 picking)通过从视点发射一条射线来将屏幕坐标系中的点击操作转换为世界坐标系中的选择过程,因此它同样依赖于射线求交过程。于是 Scotty3D 的 UI 代码中必须调用路径追踪子模块的代码,甚至是一些与渲染功能耦合比较紧密的代码,在我看来逻辑上略显混乱。拆解之后,Dandelion 的 UI 子模块和渲染子模块共同依赖射线相关的工具代码(<em>utils/ray.h</em> 与 <em>utils/bvh.h</em>)。</p><p>最终,光线追踪渲染器的结构变得很简单:以 <code>WhittedRenderer::render</code> 作为渲染入口,在深度为 1 的假想渲染平面上划分像素网格并调用 <code>utils/ray.h</code> 提供的 <code>generate_ray</code> 根据图像坐标生成射线,对每条射线调用 <code>cast_ray</code> 求交并完成着色计算,即可得到图像。</p><blockquote><p>实现 Whitted-Style 光线追踪时,我们为了直接沿用 <code>GL::Material</code> 的材质格式,小小地取了一点巧:在 Phong 模型材质的基础上,约定将 shininess > <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>1</mn><msup><mn>0</mn><mn>3</mn></msup></mrow><annotation encoding="application/x-tex">10^3</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8141em;"></span><span class="mord">1</span><span class="mord"><span class="mord">0</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">3</span></span></span></span></span></span></span></span></span></span></span> 的材质都视为镜面,即着色时只反射环境颜色而没有自身颜色。</p></blockquote><h2 id="拓展-Dandelion-的渲染能力">拓展 Dandelion 的渲染能力</h2><h3 id="性能优化">性能优化</h3><p>由于每个像素都进行相同的计算(着色),渲染是一个非常适合并行优化的过程。例如 Blender 的 Cycles 渲染器在使用 CPU 渲染时可以达到 99% 以上的 CPU 占用率,这意味着实现合理的并行渲染程序可以完全发挥 CPU 算力。</p><p>在移植完 <code>RasterizerRenderer</code> 之后,我们又实现了一个多线程版本的光栅化渲染管线。不过多线程和单线程的渲染逻辑基本一致,所以另外定义一个多线程光栅化渲染器类并不必要,我们也就只增加了若干带有 <code>_mt</code> 后缀的函数以便共用配置和初始化的代码。在不涉及外部交互的情况下,直接使用 <code>std::thread</code> 写出一个多线程版本并不困难。真正决定性能的重点在于:规划哪些数据要被复制给每个线程,成为线程局部数据 (thread local data);哪些数据还是保留在主线程内,成为共享数据。</p><p>JoTaiLang 同学一开始就确定了要将所有的三角形(面片)分配不同的线程,因为使用局部光照模型时不同的面片之间毫无影响,面片不仅可以直接被分配给不同的线程,而且在 join 时也不必合并。然而 frame buffer 和 depth buffer 就不是那么好处理了,不同的线程可能会写同一个像素处的颜色(或深度)值,这种写冲突是不可避免的。此时我的想法是索性准备 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>N</mi><mtext>threads</mtext></msub></mrow><annotation encoding="application/x-tex">N_\text{threads}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.10903em;">N</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em;"><span style="top:-2.55em;margin-left:-0.109em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord text mtight"><span class="mord mtight">threads</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span> 个 buffer ,每个线程首先写自己的 buffer ,到 join 时再将所有的 buffer 根据深度值合并起来。于是 JoTaiLang 分别测试了两种方案的性能:</p><ul><li>全局加锁:当某个线程要写 frame buffer 时,将 depth buffer 和 frame buffer 都锁上,写完一个像素后再释放锁</li><li>多个 buffer:每个线程单独创建 frame buffer 和 depth buffer ,join 后在主线程根据每个 depth buffer 的值决定来合并 frame buffer</li></ul><p>测试的结果并不如我所愿——虽然多个 buffer 的方案完全消除了锁操作,但用时反而比全局加锁的方案更长一些。所以我们最终选择了全局加锁的方案,也没有继续尝试细粒度锁或加速 buffer 合并的方案。不过在仅使用全局锁的情况下,启动 8 个线程便能够达到 4 以上的加速比,可见光栅化三角形与片元着色(无共享数据部分)的开销应当明显大于写 buffer(有冲突的部分)的开销。</p><p>完全无优化的光线追踪会调用 <code>naive_intersect</code> 函数计算光线与 mesh 的交点,这个函数会遍历 mesh 中所有的面片并调用 <code>ray_triangle_intersect</code> 进行求交,实现很简单但效率非常底下。我们移植 GAMES 101 实验中的 BVH 代码后,光线追踪效率有了质的飞跃,甚至可以在几十秒内渲染 Stanford Dragon 2 模型(约 36 万面片)!于是我们就转移精力去写其他部分的代码了,没有为光线追踪实现多线程优化。</p><p>说来我们在移植 BVH 代码时还有个小插曲:GAMES 101 的实验代码完全没有考虑物体的 model 变换(或者说默认 model matrix 是个单位矩阵),因而直接将 BVH 建立在了世界坐标系下。然而 Dandelion 允许用户对物体进行多种线性变换,如果还将 BVH 建立在世界坐标系下,每次变换物体都要更新整个 BVH ,这是不可接受的。因此我们加入了两个修改:</p><ul><li>将 BVH 由场景级降为物体级,每个物体都有一个自己的 BVH(以 <code>Object::bvh</code> 类属性的形式存在)</li><li>BVH 建立在模型坐标系下,物体被线性变换后无需更新 BVH</li></ul><p>不过,这也导致射线(光线)与物体求交之前必须被变换到物体的模型坐标系下,好在这样的代价不大,完全可以接受。</p><p><img src="/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E5%9B%9B%EF%BC%89%EF%BC%9A%E6%B8%B2%E6%9F%93%E6%A8%A1%E5%BC%8F%E4%B8%8E%E8%BD%AF%E6%B8%B2%E6%9F%93%E6%B5%81%E7%A8%8B/show-bvh.png" alt="为测试 BVH 而增加的 BVH 可视化功能"></p><h3 id="更多效果">更多效果</h3><p>目前的光栅化渲染管线尚不支持生成阴影,不过完成渲染后 <code>RasterizerRenderer</code> 中的 depth buffer (z-buffer) 会被更新为当前视角下的深度图,所以只要对 <code>RasterizerRenderer::render</code> 函数稍作扩展就可以在其中实现 2-pass rendering ,并利用深度图实现 shadow mapping 。类似的 multi-pass rendering 理论上也都不需要大改代码。</p><p>对于光线追踪渲染管线来说,只需在材质(以及材质编辑 UI 组件)中加入透明度和折射率属性,再少量修改 <code>WhittedRenderer::cast_ray</code> 的代码,即可支持渲染透明物体。</p><p>以上都是些零打碎敲的修改,如果我们能实现一个多模式的材质系统(至少添加 PBR 材质模式),物体将不再局限于 Phong 光照模型;再进一步,我们就可以将结合了 PBR 的路径追踪渲染管线实现为一个新的渲染器 <code>PathTracingRenderer</code> ,让 Dandelion 能够渲染出更加惊艳的图像。这无疑是 2.0 版本最具挑战性的目标之一,希望我们能够在明年达成。</p><h2 id="下回分解">下回分解</h2><p>渲染模式的大部分工作并非由我完成,因此我对这部分的发言权也有限。而建模模式是我一手设计、实现并调试的。虽然我从 Scotty3D 中移植了不少代码,但也自己实现了一些行之有效的优化。2022 至 2023 学年秋季学期上课时,许多同学的轻薄笔记本在进入 Scotty3D 的建模模式时帧率大跌,甚至卡顿到完全无法操作 (< 5 FPS) 。而 Dandelion 运行在一台主流轻薄本上时,支持建模模式操纵数十万面片的 mesh 并维持 30+ FPS 的帧率,并且还没有牺牲多少易用性和视觉效果!虽说这些优化更近乎是 trick ,但它们的实际效果实在是让我很有成就感。</p><p>下一篇文章我会回忆一下在 Dandelion 中加入建模模式与半边网格 (Halfedge Mesh) 数据结构的过程。除了算法和调试的问题外,建模模式在 GUI 、状态、交互方面带来的复杂性相当大,可以说超过了其他模式的总和,也让我在最初构建 GUI 时犯下的错误彻底爆发。希望这些经验能够帮助后来的同学们,也希望这段记忆能在我的脑海中留得更久一点。</p>]]></content:encoded>
<category domain="https://greyishsong.ink/categories/%E7%BC%96%E7%A8%8B/">编程</category>
<category domain="https://greyishsong.ink/tags/%E5%9B%BE%E5%BD%A2%E5%AD%A6/">图形学</category>
<category domain="https://greyishsong.ink/tags/%E8%AF%BE%E7%A8%8B/">课程</category>
<category domain="https://greyishsong.ink/tags/%E7%BC%96%E7%A8%8B/">编程</category>
<category domain="https://greyishsong.ink/tags/%E6%9C%AC%E7%A7%91%E6%95%99%E8%82%B2/">本科教育</category>
<comments>https://greyishsong.ink/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E5%9B%9B%EF%BC%89%EF%BC%9A%E6%B8%B2%E6%9F%93%E6%A8%A1%E5%BC%8F%E4%B8%8E%E8%BD%AF%E6%B8%B2%E6%9F%93%E6%B5%81%E7%A8%8B/#disqus_thread</comments>
</item>
<item>
<title>图形学实验框架 Dandelion 始末(三):OpenGL API 抽象与实时渲染</title>
<link>https://greyishsong.ink/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%B8%89%EF%BC%89%EF%BC%9AOpenGL-API-%E6%8A%BD%E8%B1%A1%E4%B8%8E%E5%AE%9E%E6%97%B6%E6%B8%B2%E6%9F%93/</link>
<guid>https://greyishsong.ink/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%B8%89%EF%BC%89%EF%BC%9AOpenGL-API-%E6%8A%BD%E8%B1%A1%E4%B8%8E%E5%AE%9E%E6%97%B6%E6%B8%B2%E6%9F%93/</guid>
<pubDate>Sun, 28 Jan 2024 11:33:47 GMT</pubDate>
<description><p>OpenGL 标准描述了一种 C/S 架构的 GPU 编程模式:CPU 上执行的程序是 Client ,通过调用 API 可以查询或更改全局状态,也可以请求渲染;GPU 是 Server ,响应查、改状态或渲染的请求。这种设计思路导致编程时需要时刻考虑许多状态,而每次进行渲染都要更改若干个状态。为了更方便地管理这些状态,我在 Dandelion 框架实现中对 OpenGL 略作封装,屏蔽了大部分细粒度的状态修改操作。</p></description>
<content:encoded><![CDATA[<p>OpenGL 标准描述了一种 C/S 架构的 GPU 编程模式:CPU 上执行的程序是 Client ,通过调用 API 可以查询或更改全局状态,也可以请求渲染;GPU 是 Server ,响应查、改状态或渲染的请求。这种设计思路导致编程时需要时刻考虑许多状态,而每次进行渲染都要更改若干个状态。为了更方便地管理这些状态,我在 Dandelion 框架实现中对 OpenGL 略作封装,屏蔽了大部分细粒度的状态修改操作。</p><span id="more"></span><h2 id="实时渲染需求与顶层-API">实时渲染需求与顶层 API</h2><p>我在第一篇文章中提到过:Dandelion 中存在两种渲染过程,分别是用于预览的实时硬件渲染与用作实验的离线软件渲染。本文只讨论其中硬件渲染的部分,软件渲染与 OpenGL 无关。</p><p>由于 Dandelion 是参照 Blender 设计的,对实时预览的着色效果要求很低,只需分辨出物体的形状即可。除了场景中的物体以外,</p><ul><li>在渲染模式下还需要渲染出相机的视椎体以便调整渲染相机、渲染出设置好的光源</li><li>在建模模式下还需要渲染出 mesh 的顶点和边来强调基本图元</li></ul><p>这些渲染需求可以概括为渲染顶点、线条和面片三类图元,而每一类图元的渲染效果是相同的,不同情况下渲染任务的差别在于待渲染图元的差别。因此,我准备用两个顶层的渲染类来满足两大类渲染需求:</p><ul><li><code>GL::Mesh</code> 面向以 mesh 为数据源的渲染任务</li><li><code>GL::LineSet</code> 面向以线条为数据源的渲染任务</li></ul><p>最初我以为二者既然功能不同,实现起来应该也有明显的差异。但很快就发现 Mesh 有时也需要渲染线框,导致两个类的渲染函数 <code>Mesh::render</code> 与 <code>LineSet::render</code> 实现起来几乎没有区别,或者说 <code>LineSet::render</code> 完全是 <code>Mesh::render</code> 的一部分,只不过去掉了选择被渲染图元类型的代码。所以这里只聊 <code>GL::Mesh</code> 的渲染过程。</p><p>根据我对 <code>GL</code> 这个 namespace 的功能规划,<code>GL::Mesh</code> 只负责处理渲染操作,而不关心这些渲染操作的语义是什么。比如调用 <code>Mesh::render</code> 时指定渲染边和顶点,而不是指定渲染 Mesh 的“线框”;或者指定渲染面片,而不是 Mesh 的“实体”。因此,<code>Mesh::render</code> 需要完成的工作就是根据图元类型绑定 VAO 、设置颜色和材质并发送 draw call ,而 <code>GL::Mesh</code> 类中需要维护的信息就是与这个 Mesh 有关的 VAO 和数据 Buffer 。</p><p>Dandelion 实时渲染的基本流程和上一篇文章中渲染地面网格的过程基本一致:</p><ol><li>在 <code>Controller::render</code> 函数中,根据 <code>main_camera</code> 的参数计算出 View / Projection 矩阵,并设置 shader 中的 uniform 变量。</li><li>进入 <code>Scene::render</code> 函数,遍历所有的 <code>Object</code> 并调用 <code>Object::render</code> 函数渲染物体。</li><li>进入 <code>Object::render</code> 函数,设置 shader uniform 变量传入 Model 矩阵和材质参数,遍历所有的 <code>GL::Mesh</code> 对象,调用 <code>GL::Mesh::render</code> 渲染 mesh 。</li><li>调用 <code>glDrawArrays</code> / <code>glDrawElements</code> 绘制图元。</li></ol><p>在这个流程中,<code>Scene</code> 和 <code>Object</code> 是具有语义的“场景”和“物体”,而 <code>Mesh</code> 则是位于下层的“无语义对象”。这里的无语义,是指它在 UI 上不存在对应物,也不直接与用户产生任何交互,用户不会感受到它的存在。当程序加载模型文件后,场景不再为空,每一帧渲染时 <code>Scene::render</code> 函数就会调用每个物体的 <code>Object::render</code> 函数,而在 <code>Object::render</code> 中会调用 <code>Mesh::render</code> 函数完成渲染。对于绝大多数情况来说,<code>Mesh::render</code> 就是最顶层的渲染 API ,将场景、物体这样具有语义的对象与只负责完成渲染的抽象层分隔开。</p><blockquote><p>之所以说“绝大多数情况下”,是因为诸如光源(绘制为六个点)、地面(绘制为若干网格线)等特殊对象都不是由 <code>Mesh</code> 或 <code>LineSet</code> 渲染的,而是调用了更低层级的 API 。这些例外的原因是我实现的 <code>Mesh</code> 和 <code>LineSet</code> 不够灵活,不支持逐顶点指定颜色等操作,无法实现我想要的效果。加之经验尚浅、开发时间有限,最终留下了一些不甚规整的代码。</p></blockquote><h2 id="低层-API">低层 API</h2><p>有顶层自然有下层,上文也提到 <code>GL::Mesh</code> 和 <code>GL::LineSet</code> 都需要管理 VAO 和各类 Buffer ,而 OpenGL 中管理这些对象的过程也需要设置许多状态。如果各处都直接调用 OpenGL API ,那么到处重复设置状态的代码显然对可读性和后续开发都有负面影响。这个问题引出了第一类低层 API ,即负责管理资源的 API 。另一方面,CPU 端的 OpenGL API 是一套 C API 而不是 C++ API ,它大量使用宏来表示类型,不能很好地配合类型检查;由于渲染操作自身的复杂性,一些函数接口的参数列表很长且有些容易混淆,对于 Dandelion 来说是灵活过度而可读性不足的。因此有了第二类低层 API ,即负责简化 OpenGL API 调用的 API 。</p><h3 id="资源管理">资源管理</h3><p>Dandelion 需要管理的 OpenGL 资源有三种:</p><ul><li>Buffer Object :存储顶点属性或顶点索引的缓冲区,对应 <code>ArrayBuffer</code> 和 <code>ElementArrayBuffer</code> 两个类</li><li>Vertex Array Object :记录顶点属性格式的对象,对应 <code>VertexArrayObject</code> 这个类</li><li>Material :在 Shader 中使用的材质</li></ul><p>根据 <code>GL::Mesh</code> 和 <code>GL::LineSet</code> 的设计,每个 <code>Mesh</code> 或 <code>LineSet</code> 是一个最小的渲染单位,诸如 <code>glDrawArrays</code> 或 <code>glDrawElements</code> 这样的 draw call 都是由它们产生的。因此,每个渲染单位持有一个 VAO 及若干 Buffer Object ,<code>Mesh</code> 还有自己的材质。</p><p><img src="/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%B8%89%EF%BC%89%EF%BC%9AOpenGL-API-%E6%8A%BD%E8%B1%A1%E4%B8%8E%E5%AE%9E%E6%97%B6%E6%B8%B2%E6%9F%93/vertex_array_objects_ebo.png" alt="VAO, VBO 和 EBO 协同工作的示意图(来自 LearnOpenGL CN)"></p><p>通常来说,资源管理的主要工作是管理资源对象的“生命周期”,也就是创建、转移和销毁的过程。而由于 OpenGL 以状态机的方式工作,Dandelion 还需要额外管理资源的切换过程,在渲染不同的渲染单位时正确地绑定所需的资源。</p><p>在上述的三种资源中,材质只是一组静态的数值属性,并没有独立的生命周期。我只需要正确编写 buffer object 与 VAO 的构造和析构函数,就算是完成了管理二者生命周期的代码。除了包装 <code>glGenBuffers</code> 、<code>glGenVertexArrays</code> 等 API ,更重要的是<strong>禁止对 buffer object 或 VAO 的拷贝构造</strong>。因为这些资源并不存在于 Client (CPU) 端,内存中的 <code>ArrayBuffer</code> 实例只不过持有它的编号 (name) 而已。通过拷贝构造复制一个 <code>ArrayBuffer</code> ,只能复制其编号而不能复制真正的 buffer ,这种浅拷贝在 Dandelion 中毫无意义,只会增加产生 bug 的可能性。相应地,虽然目前还没有移动 buffer object 或 VAO 的需求,但把某个 mesh 转移到另一个物体下也是合理的,所以我还是实现了它们的移动构造函数。</p><p>虽然复制一个 mesh 在逻辑上完全合理,但由于持有了不可拷贝构造的 buffer object 与 VAO 对象,所有的渲染单位也都无法拷贝构造。后续版本可能会重新实现 <code>GL::Mesh</code> 的拷贝构造函数以进行深拷贝(创建新的资源)。但无论是否允许拷贝构造,我始终认为应该禁止拷贝赋值以强制开发者借助引用 (reference) 来操作一个 <code>GL::Mesh</code> ,否则很可能一次不经意间的赋值就会徒劳无功地消耗大量资源。</p><p>Buffer object 或 VAO 能够完成的切换操作包括绑定(解绑)自身和操纵顶点属性两种。由于一个 buffer 的数据格式与用途通常不会改变,我倾向于尽量将 buffer object 的各种信息在构造时固定下来。Dandelion 中的 buffer 都是为了提供顶点属性或索引存在的,因此属性或索引的格式可以认为是 buffer 类型的一部分,体现到类定义上就是模板类 <code>GL::ArrayBuffer<T, size></code> 。它通过模板参数 <code>T</code> 指定数据类型、<code>size</code> 指定每个顶点对应多少个数据元素。表示 buffer 使用方式的 hint 信息与对应顶点属性的位置则作为类内属性在构造时指定。这样一来,在调用 <code>glVertexAttribPointer</code> 将该 buffer 指定为相应顶点属性的数据源时,所需的参数都已经包含在类内了,于是我便可以将这个调用也封装起来,变成一个无参的函数 <code>GL::ArrayBuffer::specify_vertex_attribute</code> 。</p><p>在上述封装的基础上,绘制过程就不再需要人工记忆并传递大量参数,大致变成了这样:</p><ol><li>调用 VAO 的 <code>bind()</code> 来记录绑定信息和顶点属性数据源</li><li>调用数据源 buffer 的 <code>bind()</code> 和 <code>to_gpu()</code> 方法传输数据,再调用 <code>specify_vertex_attribute()</code> 方法指定它成为顶点属性的数据源</li><li>调用 VAO 的 <code>release()</code> 解绑,至此数据已经准备完毕</li><li>在需要发起 draw call 处,绑定 VAO 并直接调用 <code>glDrawArrays</code> 或 <code>glDrawElements</code> 即可</li></ol><p>上述第 1 到第 3 步发生在 <code>GL::Mesh</code> 的构造过程中,而第 4 步正是在 <code>GL::Mesh::render</code> 里执行的。</p><h3 id="简化调用">简化调用</h3><p>负责管理资源的封装代码其实已经起到了简化 API 调用过程的作用,不过还有一些封装是与资源无关的,它们只是一些工具代码。</p><h4 id="类型转换">类型转换</h4><p>OpenGL API 在格式上是一套 C API ,这意味着它不支持函数重载、不支持模板函数。因此 <code>glVertexAttribPointer</code> 之类的 API 不得不用参数来指定传输的数据是什么类型,而指定类型的参数也只能是一些宏定义,这样的代码很难直接与 C++ 的泛型系统配合起来。好在现代 C++ 提供了比较完善的编译期计算功能,可以很方便地<strong>将类型转换为宏</strong>。我在代码中实现了一个专门转换类型的函数 <code>get_GL_type_enum</code> :</p><figure><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp"><span class="token keyword">template</span><span class="token operator"><</span><span class="token keyword">typename</span> <span class="token class-name">DataType</span><span class="token operator">></span><span class="token keyword">constexpr</span> GLenum <span class="token function">get_GL_type_enum</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token keyword">constexpr</span> <span class="token punctuation">(</span>std<span class="token double-colon punctuation">::</span>is_same_v<span class="token operator"><</span>DataType<span class="token punctuation">,</span> <span class="token keyword">char</span><span class="token operator">></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> GL_BYTE<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token keyword">constexpr</span> <span class="token punctuation">(</span>std<span class="token double-colon punctuation">::</span>is_same_v<span class="token operator"><</span>DataType<span class="token punctuation">,</span> <span class="token keyword">unsigned</span> <span class="token keyword">char</span><span class="token operator">></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> GL_UNSIGNED_BYTE<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token keyword">constexpr</span> <span class="token punctuation">(</span>std<span class="token double-colon punctuation">::</span>is_same_v<span class="token operator"><</span>DataType<span class="token punctuation">,</span> <span class="token keyword">int</span><span class="token operator">></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> GL_INT<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token keyword">constexpr</span> <span class="token punctuation">(</span>std<span class="token double-colon punctuation">::</span>is_same_v<span class="token operator"><</span>DataType<span class="token punctuation">,</span> <span class="token keyword">unsigned</span> <span class="token keyword">int</span><span class="token operator">></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> GL_UNSIGNED_INT<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token keyword">constexpr</span> <span class="token punctuation">(</span>std<span class="token double-colon punctuation">::</span>is_same_v<span class="token operator"><</span>DataType<span class="token punctuation">,</span> <span class="token keyword">float</span><span class="token operator">></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> GL_FLOAT<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token keyword">constexpr</span> <span class="token punctuation">(</span>std<span class="token double-colon punctuation">::</span>is_same_v<span class="token operator"><</span>DataType<span class="token punctuation">,</span> <span class="token keyword">double</span><span class="token operator">></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> GL_DOUBLE<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> GL_NONE<span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div></figure><p>结合 <code>if constexpr</code> 语法和 <code>is_same_v</code> 模板可以在编译期根据类型返回相应的宏值,于是我就能在 <code>GL::ArrayBuffer::specify_vertex_attribute</code> 中这样传递类型:</p><figure><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp"><span class="token keyword">template</span><span class="token operator"><</span><span class="token keyword">typename</span> <span class="token class-name">T</span><span class="token punctuation">,</span> std<span class="token double-colon punctuation">::</span>size_t size<span class="token operator">></span><span class="token keyword">void</span> <span class="token class-name">ArrayBuffer</span><span class="token operator"><</span>T<span class="token punctuation">,</span> size<span class="token operator">></span><span class="token double-colon punctuation">::</span><span class="token function">specify_vertex_attribute</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span> GLenum data_type <span class="token operator">=</span> <span class="token generic-function"><span class="token function">get_GL_type_enum</span><span class="token generic class-name"><span class="token operator"><</span>T<span class="token operator">></span></span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">glVertexAttribPointer</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token operator">-></span>layout_location<span class="token punctuation">,</span> size<span class="token punctuation">,</span> data_type<span class="token punctuation">,</span> GL_FALSE<span class="token punctuation">,</span> size <span class="token operator">*</span> <span class="token keyword">sizeof</span><span class="token punctuation">(</span>T<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token keyword">void</span><span class="token operator">*</span><span class="token punctuation">)</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">glEnableVertexAttribArray</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token operator">-></span>layout_location<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div></figure><h4 id="修改-Buffer-数据">修改 Buffer 数据</h4><p>当用户修改了某个 mesh 的顶点等属性时,Dandelion 必须相应修改 buffer 内容再传输到显存才能反映到界面上。因此 <code>GL::ArrayBuffer</code> 和 <code>GL::ElementArrayBuffer</code> 都提供了增加、修改数据的方法。之所以不能删除数据,是因为通过 OpenGL API 传递的数据都是数组,删除的代价太大,不如重建一个。</p><p>既然 buffer 中存储的数据是受限的(类型 <code>T</code> 与属性大小 <code>size</code>),那么更新或增加数据的操作当然也应该检查传入的新数据是否符合这些限制。C++ 的 <a href="https://en.cppreference.com/w/cpp/language/parameter_pack">Parameter Pack</a> 机制允许编写参数个数可变的函数,而 <a href="https://en.cppreference.com/w/cpp/language/fold">Fold Expressions</a> 机制让我能够方便地检查所有参数的类型是否一致:</p><figure><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp"><span class="token keyword">template</span><span class="token operator"><</span><span class="token keyword">typename</span> <span class="token class-name">T</span><span class="token punctuation">,</span> std<span class="token double-colon punctuation">::</span>size_t size<span class="token operator">></span><span class="token keyword">template</span><span class="token operator"><</span><span class="token keyword">typename</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> Ts<span class="token operator">></span><span class="token keyword">void</span> <span class="token class-name">ArrayBuffer</span><span class="token operator"><</span>T<span class="token punctuation">,</span> size<span class="token operator">></span><span class="token double-colon punctuation">::</span><span class="token function">append</span><span class="token punctuation">(</span>Ts<span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> values<span class="token punctuation">)</span><span class="token punctuation">{</span> <span class="token comment">// && 这样的双目运算符可以配合 ... 解包参数实现“链式”运算:先检查前两个参数的类型是否都为 T,</span> <span class="token comment">// 然后将检查结果与第三个参数的类型检查结果作逻辑与,以此类推将所有检查结果都。</span> <span class="token keyword">static_assert</span><span class="token punctuation">(</span><span class="token punctuation">(</span>std<span class="token double-colon punctuation">::</span>is_same_v<span class="token operator"><</span><span class="token keyword">decltype</span><span class="token punctuation">(</span>values<span class="token punctuation">)</span><span class="token punctuation">,</span> T<span class="token operator">></span> <span class="token operator">&&</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token string">"ArrayBuffer: all values to be appended must have the same type as T"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// sizeof... 运算符可以获取 parameter pack 中的参数个数</span> <span class="token keyword">static_assert</span><span class="token punctuation">(</span><span class="token keyword">sizeof</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">(</span>values<span class="token punctuation">)</span> <span class="token operator">==</span> size<span class="token punctuation">,</span> <span class="token string">"ArrayBuffer: number of values to be appended must be same as size per vertex"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token operator">-></span>data<span class="token punctuation">.</span><span class="token function">push_back</span><span class="token punctuation">(</span>values<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div></figure><p>不得不说虽然看起来很黑魔法,但理解之后确实方便,堪比 Python 的 <code>all</code> 函数了——更何况这些都是编译期计算,没有任何运行时开销。</p><h2 id="回顾">回顾</h2><p>在一学期结束后再去回顾这部分的设计,最大的败笔是在 <code>GL::Mesh</code> 与 <code>GL::LineSet</code> 中重复写了两遍极其相似的代码。同门师兄弟聊起来的时候,大家提到专用的 LineSet 也是个常见的设计,大概是专业工具中的逻辑更复杂,需要专门优化渲染线条的过程吧。不过 Dandelion 是一个小型的实验框架,既没有那么多的精力处处深入优化,也希望代码尽可能精简以便感兴趣的同学自己分析理解,所以我还是认为最好去掉这部分冗余。接下来的重构过程中,我应该会将 <code>GL::Mesh</code> 与 <code>GL::LineSet</code> 合并为 <code>GL::RenderUnit</code> ,作为产生 draw call 的最小单位加以管理。</p><p>至于 <code>RenderUnit</code> 到底是否应该允许复制,这是一个操作逻辑与性能优化的综合问题。如果我实现了浅拷贝(物体可以共享下层的 buffer object 和 VAO 但不共享名称),就可以引入 OpenGL 实例化绘制的 API ,极大程度地减少渲染大量相同物体时的 draw call 数量,从而让粒子系统成为可能。而如果我实现了深拷贝(创建新的 buffer object 和 VAO),就能创建可分别编辑的实例,为几何编辑(建模)过程中提供不少方便。如果更进一步,浅拷贝与深拷贝都实现后还能实现写时拷贝 (Copy On Write, COW) 机制,<code>Scene</code> / <code>Object</code> 的代码中要拷贝 <code>RenderUnit</code> 就不必专门考虑深浅拷贝了。这些优化工作量相当可观,我目前也还很犹豫是否要动手开始。</p><h2 id="下回分解">下回分解</h2><p>讲到现在为止,我只简单提到 Dandelion 是通过切换模式来完成不同任务(渲染、建模、物理模拟)的,还没有解释切换模式时 UI 层面、场景层面、数据(资源)层面发生了怎样的变化,也没有解释另外的三个模式如何运作。我计划在下一篇文章中聊聊渲染模式以及 Dandelion 离线渲染部分的设计思路,也讲讲如何拓展 Dandelion 的渲染能力。</p>]]></content:encoded>
<category domain="https://greyishsong.ink/categories/%E7%BC%96%E7%A8%8B/">编程</category>
<category domain="https://greyishsong.ink/tags/%E5%9B%BE%E5%BD%A2%E5%AD%A6/">图形学</category>
<category domain="https://greyishsong.ink/tags/%E8%AF%BE%E7%A8%8B/">课程</category>
<category domain="https://greyishsong.ink/tags/%E7%BC%96%E7%A8%8B/">编程</category>
<category domain="https://greyishsong.ink/tags/%E6%9C%AC%E7%A7%91%E6%95%99%E8%82%B2/">本科教育</category>
<comments>https://greyishsong.ink/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%B8%89%EF%BC%89%EF%BC%9AOpenGL-API-%E6%8A%BD%E8%B1%A1%E4%B8%8E%E5%AE%9E%E6%97%B6%E6%B8%B2%E6%9F%93/#disqus_thread</comments>
</item>
<item>
<title>图形学实验框架 Dandelion 始末(二):最初原型实现</title>
<link>https://greyishsong.ink/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%BA%8C%EF%BC%89%EF%BC%9A%E6%9C%80%E5%88%9D%E5%8E%9F%E5%9E%8B%E5%AE%9E%E7%8E%B0/</link>
<guid>https://greyishsong.ink/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%BA%8C%EF%BC%89%EF%BC%9A%E6%9C%80%E5%88%9D%E5%8E%9F%E5%9E%8B%E5%AE%9E%E7%8E%B0/</guid>
<pubDate>Mon, 30 Oct 2023 15:30:29 GMT</pubDate>
<description><p>当我有了初步的需求分析结果,下一步就可以开始写代码了。虽然当初动手实现之前已经有了一些思考和设计,但后来写出的代码还是有不少问题。这一篇文章回顾最初对代码结构的设计,其中有很多粗糙的地方后来被大幅度重构,但事件循环和交互处理的部分还是大致留存下来了。</p></description>
<content:encoded><![CDATA[<p>当我有了初步的需求分析结果,下一步就可以开始写代码了。虽然当初动手实现之前已经有了一些思考和设计,但后来写出的代码还是有不少问题。这一篇文章回顾最初对代码结构的设计,其中有很多粗糙的地方后来被大幅度重构,但事件循环和交互处理的部分还是大致留存下来了。</p><span id="more"></span><blockquote><p>上一篇回顾: <a href="/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E9%9C%80%E6%B1%82%E4%B8%8E%E8%AE%BE%E8%AE%A1">图形学实验框架 Dandelion 始末(一):需求与设计</a></p></blockquote><h2 id="项目配置">项目配置</h2><p>如果动手的第一步是建个文件夹,那么第二步就是创建“项目” (Project) 。一个项目与一些源代码文件的区别主要在于依赖管理和构建管理,而对于 C++ 来说 CMake 已经是构建系统的事实标准了,我就先不尝试其他工具,直接跟随主流用 CMake 来管理项目。</p><h3 id="依赖库整合">依赖库整合</h3><p>提供依赖库的方法有很多,比如包管理器、git submodule ,或者干脆要求使用者去相应项目主页上手动下载都可以。但我做的是个教学框架,配置起来越简单越好。而这几种方法都要专门指定版本,还有可能遭遇某个版本后来被开发者删掉的尴尬。因此我索性将所有的依赖库都放置在项目中,并且一并进入 git 管理。</p><blockquote><p>后来我们发现 Assimp 的代码有些小问题,这种做法反倒还方便了我去手动修复,算是个意外收获了。不过这时候是没有其他想法的,只想尽量让配置简单些。</p></blockquote><p>Dandelion 所依赖的第三方库有这么三种:</p><ul><li>必须编译成库文件再链接的:Assimp / GLFW</li><li>仅头文件 (Header Only) 或者直接随着项目源代码一起编译的:Eigen / GLAD / Dear ImGui / spdlog / stb image / portable file dialog</li><li>同时支持以上两种方式的:fmt</li></ul><p>为了构建简单,我选择以 Header Only 的方式引入 fmt ,因此后两种的配置仅限于写好 <code>target_include_directories</code> 。此时刚刚开始开发,所以我也并未考虑要对 Assimp 和 GLFW 作什么特殊的配置,只简单地用 <code>add_subdirectory</code> 作为子目录加入了项目中。</p><p>在这个过程中遇到的一些小麻烦包括:</p><p>macOS 上构建 GLFW 时需要多加一些依赖,这是因为 macOS 与 Linux 有些差异,某些被叫做 <em>Framework</em> 的库也要引入。</p><figure><div class="code-wrapper"><pre class="line-numbers language-cmake" data-language="cmake"><code class="language-cmake"><span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token variable">APPLE</span><span class="token punctuation">)</span> <span class="token keyword">find_package</span><span class="token punctuation">(</span>OpenGL REQUIRED<span class="token punctuation">)</span> <span class="token keyword">target_include_directories</span><span class="token punctuation">(</span><span class="token punctuation">${</span><span class="token variable">PROJECT_NAME</span><span class="token punctuation">}</span> <span class="token namespace">PRIVATE</span> <span class="token punctuation">${</span>OPENGL_INCLUDE_DIR<span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token keyword">target_link_libraries</span><span class="token punctuation">(</span><span class="token punctuation">${</span><span class="token variable">PROJECT_NAME</span><span class="token punctuation">}</span> <span class="token string">"-framework Cocoa"</span> <span class="token string">"-framework OpenGL"</span> <span class="token string">"-framework IOKit"</span> <span class="token punctuation">${</span>OPENGL_gl_LIBRARY<span class="token punctuation">}</span> <span class="token punctuation">)</span><span class="token keyword">endif</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div></figure><p>spdlog 是自带一份 fmt 的 (bundled fmt) ,这时 fmt 是“依赖的依赖”。而因为“格式化字符串”是个很常用的操作,我想把 fmt 作为直接依赖引入,方便在 Dandelion 的代码中调用 fmt 的函数。要让 spdlog <strong>不使用</strong>自己的 fmt ,就需要额外定义一个宏:</p><figure><div class="code-wrapper"><pre class="line-numbers language-cmake" data-language="cmake"><code class="language-cmake"><span class="token keyword">target_compile_definitions</span><span class="token punctuation">(</span><span class="token punctuation">${</span><span class="token variable">PROJECT_NAME</span><span class="token punctuation">}</span> <span class="token namespace">PRIVATE</span> SPDLOG_FMT_EXTERNAL<span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre></div></figure><p>既然不再需要,我就顺手删掉了 spdlog 自带的 fmt 以减小项目体积。</p><h3 id="构建配置">构建配置</h3><p>我想让项目在以 Debug 配置构建时输出更详细的日志,而以 Release 配置构建时则少输出一些,这可以通过用宏 <code>DEBUG</code> 控制条件编译来实现。相应地,在 CMakeLists 中要指定只有 Debug 配置下才定义这个宏,这里我用了生成表达式 (Generator Expression) 这种比较简洁的写法:</p><figure><div class="code-wrapper"><pre class="line-numbers language-cmake" data-language="cmake"><code class="language-cmake"><span class="token keyword">target_compile_definitions</span><span class="token punctuation">(</span><span class="token punctuation">${</span><span class="token variable">PROJECT_NAME</span><span class="token punctuation">}</span> <span class="token namespace">PRIVATE</span> <span class="token punctuation">$<</span><span class="token punctuation">$<</span>CONFIG:Debug<span class="token punctuation">></span>:DEBUG<span class="token punctuation">></span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre></div></figure><p>而考虑到我们人手很少,又是从头开发,把警告等级调高以便查错是个合适的选择。我是在 Linux 上开发,此时尚未仔细考虑 Windows / macOS 上的问题,于是直接开了 <code>-Wall -Wextra -Werror</code> 几个选项来强制要求消除所有警告。</p><h2 id="第一步:创建运行环境">第一步:创建运行环境</h2><p>从零开始编写代码时,我比较习惯首先从入口点写起,这样写完一步之后就能得到一个可执行的程序。Dandelion 是一个使用 OpenGL 渲染场景、使用 Dear ImGui 创建图形界面的程序。根据 OpenGL 渲染管线的要求和 Dear ImGui 的 API 约定,它在显示图形之前需要做以下几件事情:</p><ol><li>加载 OpenGL 函数地址</li><li>初始化 OpenGL 上下文,创建窗口并加载图标</li><li>设置好 OpenGL 上下文的基本属性(如开启深度测试等)</li><li>初始化 Dear ImGui 上下文</li><li>加载 GUI 使用的汉字字体</li><li>编译并链接用于渲染场景的 shader</li></ol><p>在程序的整个生命周期中,需要与平台 API 打交道的工作基本上仅限于此。为了便于将这些部分与其他逻辑隔离,我用一个 <code>Platform</code> 类来完成它们。主函数中几乎不包含任何逻辑,只创建一个 <code>Platform</code> 实例并将控制权交给它。</p><p>在渲染与交互的过程中,整个程序始终在执行 <em>事件循环</em> ,这是一个用于处理所有输入输出的<strong>死循环</strong>,直到 GUI 被销毁才退出。在事件循环的每一趟,</p><ul><li>按照 OpenGL 双缓冲渲染模式的要求,要调用 <code>glfwSwapBuffers</code> 交换前后两个缓冲以更新画面</li><li>按照 GLFW 的约定,要调用 <code>glfwPollEvents</code> 来获取设备输入</li><li>按照 Dear ImGui 的约定,要调用处理输入和重绘 GUI 的 API</li></ul><p>这些事务也是与平台 API 交互的,并且和具体的功能性逻辑没有关系,所以也是由 <code>Platform</code> 实例执行。而真正执行功能性逻辑(比如布局、离线渲染、建模等等)的部分应该在每趟循环中被调用,并根据当前的状态来判定要做什么操作, <code>Platform</code> 实例的事件循环则是这些功能性逻辑的入口。这样一来,程序的执行顺序就是 主函数 → <code>Platform</code> 构造函数 → <code>Platform::eventloop</code> → <code>Platform</code> 析构函数 → 返回退出。将整个过程画成活动图的形式是这样的:</p><p><img src="/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%BA%8C%EF%BC%89%EF%BC%9A%E6%9C%80%E5%88%9D%E5%8E%9F%E5%9E%8B%E5%AE%9E%E7%8E%B0/life-cycle-overview.svg" alt="life cycle: overview"></p><p><code>Platform</code> 除了初始化的流程比较长,其余的代码都相当短小。<code>eventloop</code> 实际上只有五六个函数调用,而真正主要的部分才刚刚开始。</p><h2 id="第二步:渲染-GUI">第二步:渲染 GUI</h2><p>Dandelion 的交互策略是将每一类功能集中于一个模式中,根据之前的需求分析,它应该需要布局 (layout) 、建模 (Model) 、渲染 (Render) 、模拟 (Simulate) 四个模式。此时要实现的就是布局模式的基本功能。</p><h3 id="从事件循环走向功能逻辑">从事件循环走向功能逻辑</h3><p>Dandelion 的所有功能都是以三维场景(或者物体)的实时显示为基础的,而在当下它还是个单线程的程序,因此在事件循环中通往功能逻辑的主要入口就是 <strong>渲染全部内容的函数</strong> 。每调用一次这个函数,程序就渲染一帧画面。另外,程序也需要处理在这段时间内到来的输入,因此我也设置了处理所有输入的入口函数。</p><blockquote><p>实际上处理输入的入口函数与渲染画面的入口函数可以是同一个,在这个函数里再分别调用其他的函数就行。这里我把它们拆分开并没有什么特殊的用意,只是习惯使然。</p></blockquote><p>单线程意味着:</p><ul><li>在所有的输入或运算全部被处理完之后,程序才能返回到事件循环中调用 <code>glfwSwapBuffers</code> 渲染下一帧,因此这个程序的帧率是不固定的,任务越重帧率越低。</li><li>各种逻辑处理调用呈树形向下展开,树根就是刚才提到的入口函数。</li></ul><p>在上一篇文章中提到,Dandelion 基本上是按照 MVC 架构设计的,因此我的入口函数放在了 <code>Controller</code> 类当中。这个类的唯一实例负责将输入(例如鼠标光标坐标)解释为要进行的交互操作并相应地修改场景数据,但不直接负责渲染任何东西,只调用具体的渲染函数。总而言之,控制器</p><ul><li>有两个入口函数,分别是处理输入的入口 <code>Controller:process_input</code> 和渲染的入口 <code>Controller::render</code></li><li>还有一大堆处理输入的函数,把屏幕坐标、点击和按键转换成相应的函数调用或者属性修改</li><li>是一个单例的类,不会有第二个实例对象(这了我用了 Meyers' Singleton 的方法将其实现为单例类)</li></ul><p>将上面活动图中 Functions 泳道的部分(下半部分)展开来画是这样的:</p><p><img src="/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%BA%8C%EF%BC%89%EF%BC%9A%E6%9C%80%E5%88%9D%E5%8E%9F%E5%9E%8B%E5%AE%9E%E7%8E%B0/life-cycle-eventloop.svg" alt="life cycle: event loop"></p><h3 id="GUI-与-UI-组件的封装">GUI 与 UI 组件的封装</h3><p>通常来说 GUI 有两种实现思路,分别是保留模式 GUI (Retained Mode GUI, RMGUI) 与立即模式 GUI (Immediate Mode GUI, IMGUI)。</p><p>前者的 API 一般是面向对象风格的:用户需要创建一些 UI 组件实例(例如按钮或者文本框),每个 UI 组件实例就是一个内部维护了若干状态的对象。当用户输入触发了某些条件,程序就修改一些对象属性,从而修改 UI 组件的外观。这样的运行流程中并不体现“帧”的概念:调用者创建了一个组件后它就显示出来,只要在处理输入的函数里修改 UI 组件的状态,不需要专门调用某个函数来“绘制” UI 组件。Qt 就是一种典型的 RMGUI 框架,它的信号-槽机制也是一种非常经典的 RMGUI 实现方案。</p><p>后者的 API 则完全不同,在调用者看来,立即模式 GUI 的 API 都是<strong>无状态</strong>的。调用者需要在每一帧调用 API 来绘制 UI 组件,而不是自己维护一个 UI 对象。例如,每一帧都调用 <code>button</code> 函数就会在屏幕上显示一个按钮,按钮的样式由传入的参数决定,并不存在什么按钮对象。这种 GUI 上手起来非常简单,实现方案往往也更轻量化,Dandelion 使用的 Dear ImGui 就是一种 IMGUI 框架。</p><blockquote><p>乍一听起来,每一帧都调用大量的函数来绘制 UI 组件意味着每一帧都要<strong>重绘</strong>整个 GUI ,从而导致严重的性能下降。实际上 IMGUI 框架内部会维护很多状态,从而尽量避免无意义的重绘,实际性能并不会很差。IMGUI 所谓的“无状态”是对调用者无状态,不是指它的内部实现也无状态。</p><p>因为直接调函数创建 GUI 在代码规模小时更方便,所以不少开发者用 IMGUI 框架搭建一些自用的开发工具,比如游戏地图编辑器或者 tile 编辑器等等。</p></blockquote><p>先简单画一个 UI 界面草图,大概是这样的:</p><p><img src="/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%BA%8C%EF%BC%89%EF%BC%9A%E6%9C%80%E5%88%9D%E5%8E%9F%E5%9E%8B%E5%AE%9E%E7%8E%B0/ui-sketch.svg" alt="UI sketch"></p><p>整个界面上的 UI 组件只有两个,分别是工具栏 (Tool Bar) 和菜单栏 (Menu Bar) 。使用 RMGUI 框架时,肯定要创建两个对象来维护这些组件的状态;但是由 UI 组件维护状态的做法不太符合 IMGUI 框架的思路,Dandelion 这样的小规模软件也不需要这样做。</p><p>所以,虽然我在源代码中创建了 <code>Toolbar</code> 和 <code>Menubar</code> 两个类,但这两个类的主要作用是包装 API 调用而非维护状态数据,这与面向对象设计方法中的对象语义有较大的差别。当我构造了这两个类的实例、调用 <code>Toolbar::render</code> 和 <code>Menubar::render</code> 这两个函数时,我的目的是调用 Dear ImGui 的 API 来绘制所有的按钮、Tab 页等元素,要做的是<strong>根据全局状态进行绘制</strong>而不是<strong>接收输入修改状态</strong>。至于调用了哪些函数、绘制了哪些基本组件,这就是一些查询 Dear ImGui 文档并逐个尝试的工作了,繁琐但没有什么值得一提的东西。</p><p>可惜的是,我在刚刚开始写代码时并没有很好地认清这一点,这导致我把最重要的全局状态——当前所处的<strong>工作模式</strong>放在了 <code>Toolbar</code> 这个类中。这破坏了“不将它设计成对象”的原则,让 <code>Toolbar</code> 的封装风格既不像对象也不像函数,埋下了迫使我日后重构代码的隐患。</p><p>到此为止,Dandelion 的代码中已经有了四个类:</p><ul><li><code>Platform</code> 类封装了平台相关的 API 并负责启动程序前的准备工作</li><li><code>Controller</code> 类负责处理输入并调用渲染(绘制)逻辑,也负责维护一些全局状态,持有 <code>Toolbar</code> 和 <code>Menubar</code> 的实例<ul><li><code>Toolbar</code> 负责绘制工具栏,目前也保存着 Dandelion 的工作模式</li><li><code>Menubar</code> 负责绘制菜单栏</li></ul></li></ul><p><code>Controller::render</code> 这个函数首先调用 <code>Controller::process_input</code> 处理输入,然后依次调用 <code>Toolbar::render</code> 和 <code>Menubar::render</code> 来绘制 UI ,最后一步就是调用 <code>Scene::render</code> 来渲染场景了。而 <code>Scene</code> 这个类正是后续保存、处理场景数据的核心。</p><h3 id="渲染一个空场景">渲染一个空场景</h3><p>一个三维场景中至少包括若干<strong>物体</strong> ,而如果考虑渲染模式(离线渲染)的要求,那么场景中还要包括<strong>光源</strong>和<strong>相机</strong>两种对象。Dandelion 是用一个 <code>Scene</code> 对象加若干 <code>Object</code> / <code>Light</code> / <code>Camera</code> 对象存储场景数据的。<code>Scene</code> 对象持有后三种数据对象,而 <code>Controller</code> 对象持有全局唯一的 <code>Scene</code> 对象。</p><blockquote><p>因为以后或许会有创建多个 <code>Scene</code> 实例的需求(以及对滥用单例的担忧),所以我没有将 <code>Scene</code> 这个类也写成单例。</p></blockquote><p>那么根据这样的抽象模型,一个 <code>Scene</code> 对象至少应该能够</p><ul><li>加载模型文件并转换为场景中的物体</li><li>持有物体、光源、相机等数据对象(负责管理它们的内存)</li></ul><p>现在场景中还没有加载任何东西,首先要渲染出背景。Scotty3D 的代码结构中,任何会被显示到屏幕上的对象都有一个用于渲染自身的 <code>render</code> 方法,UI 组件、场景、物体皆然。此时我基本上是摸着 Scotty3D 过河,场景对象方面也在模仿它的设计思路,用 <code>Scene::render</code> 方法来渲染场景。要用经典的渲染管线渲染场景,首先要准备好两个东西:</p><ul><li>顶点数据,由要渲染的对象提供(比如物体的 mesh 、光源的坐标等)</li><li>变换矩阵,除了 Model 矩阵由物体记录以外,View 和 Projection 矩阵都由当前用户的视角决定</li></ul><p>顶点数据已经记录在 <code>Scene</code> 持有的数据对象中了,而视角信息则不然。不过,用户观察场景的视角很适合用一个相机来表示。这个相机具有与渲染器相机类似的属性(位置、观察方向、视角大小、宽高比等等),区别在于观察场景的相机代表了用户的观察视角,所以 GUI 界面上是看不见它的;而渲染器相机是给离线渲染使用的,用户可以直接看到它的位置和朝向。</p><p>为了便于区分,以下用<strong>主相机</strong>指代用户观察场景所用的相机,用<strong>渲染相机</strong>指代软渲染器进行离线渲染时所用的相机。对于只有一个预览窗格的 Dandelion 而言,主相机必定是唯一的,所以主相机参数直接存储在 <code>Controller::main_camera</code> 中。</p><p>由于布局模式下并不需要显示关于渲染的信息,现在暂不考虑光源和相机如何显示,先让 OpenGL 渲染管线渲染一个空场景。这并不难——只要在构造 <code>Scene</code> 对象时直接创建一个主相机并赋予它一组默认参数,再根据相机参数设置 shader 中所用的变换矩阵,就可以显示画面了。虽然场景中没有物体,但最好还是有基本的参照物来标定空间的位置和方向。最常见的参照物是在地平面 (<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>y</mi><mo>=</mo><mn>0</mn></mrow><annotation encoding="application/x-tex">y=0</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.625em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.6444em;"></span><span class="mord">0</span></span></span></span>) 处渲染一片方形网格,这也不难实现,只要设置一组固定的顶点坐标和颜色,然后直接调 OpenGL API 绘制 <code>GL_LINES</code> 类型的图元即可。</p><p>于是,一个空场景的渲染流程就是:</p><ol><li>在 <code>Controller::render</code> 函数中,根据 <code>main_camera</code> 的参数计算出 View / Projection 矩阵,并设置 shader 中的 uniform 变量。</li><li>进入 <code>Scene::render</code> 函数,直接调用 <code>Scene::render_ground</code> 渲染地平面网格。</li><li>进入 <code>Scene::render_ground</code> 函数,通过设置 shader uniform 变量关闭着色计算(渲染地平面网格不需要专门着色,整个网格的颜色是一样的)</li><li>如果是第一次渲染,那么将顶点数据(每条网格线的起点和终点)传入显存(向 OpenGL 的 Buffer 复制数据)。</li><li>调用 <code>glDrawElements</code> 绘制图元。</li></ol><p>这时我终于得到了一个能看到图的软件,它大概长这个样子:</p><p><img src="/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%BA%8C%EF%BC%89%EF%BC%9A%E6%9C%80%E5%88%9D%E5%8E%9F%E5%9E%8B%E5%AE%9E%E7%8E%B0/dandelion-initial.png" alt="initial version of Dandelion"></p><blockquote><p>为了突出三个坐标轴,我额外用红、绿、蓝三色画了线,不过操作方法与绘制灰色的地平面网格完全一样。</p><p>这里虽然有了些菜单和按钮,但尚未实现它们背后的逻辑,所以点击之后也都是没反应的。</p></blockquote><p>然而到目前位置,这还是个只能看不能动的界面。为了能够移动视角,我至少要给 <code>Controller</code> 增加一些处理鼠标输入的方法,从而让用户可以使用鼠标操控主相机。</p><h2 id="第三步:交互处理">第三步:交互处理</h2><p>在三维场景中移动视角的方法有很多,我想要用一种比较方便直观的方案:</p><ul><li>拖动鼠标时视角绕着固定点(当前就是原点)旋转</li><li>滚动滚轮时视角沿着观察方向移动,视野相应放大或缩小</li></ul><p>先不考虑其他的输入,目前需要给 <code>Controller</code> 增加处理鼠标拖动和滚轮的函数,并在这两个函数中修改主相机的参数。而 <code>Controller::process_input</code> 负责判断当前接收到的是哪种输入,并调用这两个函数进行处理。</p><p>Dear ImGui 提供了 <code>ImGuiIO::WantCaptureMouse</code> 属性,用于表示当前光标是否位于某个 UI 组件上。当光标不不在任何 UI 组件上时,它就在渲染的场景上。所以在 <code>Controller:process_input</code> 函数中,要在 <code>ImGuiIO::WantCaptureMouse</code> 为 <code>false</code> 时根据具体输入选择要调用的函数:</p><ul><li>当按下左键拖动时,就调用 <code>Controller::on_mouse_dragged</code> 函数旋转视角</li><li>当滚动滚轮时,就调用 <code>Controller::on_wheel_scrolled</code> 函数调整视野</li></ul><p>后者只需修改主相机的坐标,因而乏善可陈;但前者还有个小麻烦——为了让拖动动作更加自然,需要将光标在屏幕上的坐标变化量(二维)映射成空间中的旋转(三维),这需要做一点点数学建模工作。</p><h3 id="通过模拟轨迹球旋转视角">通过模拟轨迹球旋转视角</h3><p>屏幕上鼠标光标的移动都是二维的,比三维空间中的旋转少一个维度。有一些设备可以直接产生三维的旋转,比如轨迹球 (track ball) 就是一种很经典的三维操纵设备(甚至还有一个 emoji 🖲就是表示轨迹球的)。它的结构中包含一个可以向任意方向旋转的小球,只要抓着这个小球转动,屏幕上的视角就按照相同(或者相反)的方向转动。为了实现类似轨迹球的操控方式,我需要自己将鼠标光标的移动模拟成轨迹球的滚动。</p><p>想象一个表面比较粗糙的轨迹球,用一根手指按在上面让它来回滚动,而指尖与轨迹球的相对位置不动。此时,一次旋转过程可以用初始和结束两个时刻指尖的坐标(起点和终点)表示。</p><p>虽然理论上轨迹球的转动方向不受限制,但它毕竟没有悬浮在空中,而是必须和底座相连。所以实际上轨迹球露出的部分只有上半球,也就是球面方程 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mi>x</mi><mn>2</mn></msup><mo>+</mo><msup><mi>y</mi><mn>2</mn></msup><mo>+</mo><msup><mi>z</mi><mn>2</mn></msup><mo>=</mo><msup><mi>r</mi><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">x^2+y^2+z^2=r^2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8974em;vertical-align:-0.0833em;"></span><span class="mord"><span class="mord mathnormal">x</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1.0085em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.8141em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.04398em;">z</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8141em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.02778em;">r</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span> 中 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>z</mi><mo>></mo><mn>0</mn></mrow><annotation encoding="application/x-tex">z>0</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.5782em;vertical-align:-0.0391em;"></span><span class="mord mathnormal" style="margin-right:0.04398em;">z</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">></span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.6444em;"></span><span class="mord">0</span></span></span></span> 的部分。那么我只要给出指尖位置的 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false">(</mo><mi>x</mi><mo separator="true">,</mo><mi>y</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">(x,y)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mclose">)</span></span></span></span> 坐标,就能唯一确定相应 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>z</mi></mrow><annotation encoding="application/x-tex">z</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal" style="margin-right:0.04398em;">z</span></span></span></span> 坐标。把指尖换成光标,取窗口中心为原点,屏幕平面为 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>z</mi><mo>=</mo><mn>0</mn></mrow><annotation encoding="application/x-tex">z=0</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal" style="margin-right:0.04398em;">z</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.6444em;"></span><span class="mord">0</span></span></span></span> 平面,就可以将光标位置 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi mathvariant="bold">p</mi><mi>s</mi></msub><mo>=</mo><mo stretchy="false">(</mo><msub><mi>x</mi><mi>s</mi></msub><mo separator="true">,</mo><msub><mi>y</mi><mi>s</mi></msub><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">\mathbf{p}_s=(x_s,y_s)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6389em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathbf">p</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">s</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord"><span class="mord mathnormal">x</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">s</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em;"><span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">s</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mclose">)</span></span></span></span> 映射到虚拟的轨迹球位置 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi mathvariant="bold">p</mi><mi>t</mi></msub><mo>=</mo><mo stretchy="false">(</mo><msub><mi>x</mi><mi>t</mi></msub><mo separator="true">,</mo><msub><mi>y</mi><mi>t</mi></msub><mo separator="true">,</mo><msub><mi>z</mi><mi>t</mi></msub><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">\mathbf{p}_t=(x_t, y_t, z_t)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6389em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathbf">p</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.2806em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord"><span class="mord mathnormal">x</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.2806em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.2806em;"><span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.04398em;">z</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.2806em;"><span style="top:-2.55em;margin-left:-0.044em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mclose">)</span></span></span></span> ;而有了轨迹球上旋转的起点和终点 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi mathvariant="bold">p</mi><mrow><mi>t</mi><mo separator="true">,</mo><mn>1</mn></mrow></msub><mo separator="true">,</mo><msub><mi mathvariant="bold">p</mi><mrow><mi>t</mi><mo separator="true">,</mo><mn>2</mn></mrow></msub></mrow><annotation encoding="application/x-tex">\mathbf{p}_{t,1}, \mathbf{p}_{t,2}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.7305em;vertical-align:-0.2861em;"></span><span class="mord"><span class="mord mathbf">p</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">t</span><span class="mpunct mtight">,</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.2861em;"><span></span></span></span></span></span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord"><span class="mord mathbf">p</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">t</span><span class="mpunct mtight">,</span><span class="mord mtight">2</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.2861em;"><span></span></span></span></span></span></span></span></span></span> ,我就可以求出旋转变换了。</p><p>但这个方案还不能完全解决问题。窗口是矩形的,矩形的内切圆部分可以直接求出一个 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>z</mi></mrow><annotation encoding="application/x-tex">z</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal" style="margin-right:0.04398em;">z</span></span></span></span> 坐标,按照</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>z</mi><mo>=</mo><msqrt><mrow><msup><mi>r</mi><mn>2</mn></msup><mo>−</mo><msup><mi>x</mi><mn>2</mn></msup><mo>−</mo><msup><mi>y</mi><mn>2</mn></msup></mrow></msqrt></mrow><annotation encoding="application/x-tex">z=\sqrt{r^2-x^2-y^2}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal" style="margin-right:0.04398em;">z</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1.24em;vertical-align:-0.2333em;"></span><span class="mord sqrt"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0067em;"><span class="svg-align" style="top:-3.2em;"><span class="pstrut" style="height:3.2em;"></span><span class="mord" style="padding-left:1em;"><span class="mord"><span class="mord mathnormal" style="margin-right:0.02778em;">r</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.7401em;"><span style="top:-2.989em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord"><span class="mord mathnormal">x</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.7401em;"><span style="top:-2.989em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.7401em;"><span style="top:-2.989em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span><span style="top:-2.9667em;"><span class="pstrut" style="height:3.2em;"></span><span class="hide-tail" style="min-width:1.02em;height:1.28em;"><svg xmlns="http://www.w3.org/2000/svg" width='400em' height='1.28em' viewBox='0 0 400000 1296' preserveAspectRatio='xMinYMin slice'><path d='M263,681c0.7,0,18,39.7,52,119c34,79.3,68.167,158.7,102.5,238c34.3,79.3,51.8,119.3,52.5,120c340,-704.7,510.7,-1060.3,512,-1067l0 -0c4.7,-7.3,11,-11,19,-11H40000v40H1012.3s-271.3,567,-271.3,567c-38.7,80.7,-84,175,-136,283c-52,108,-89.167,185.3,-111.5,232c-22.3,46.7,-33.8,70.3,-34.5,71c-4.7,4.7,-12.3,7,-23,7s-12,-1,-12,-1s-109,-253,-109,-253c-72.7,-168,-109.3,-252,-110,-252c-10.7,8,-22,16.7,-34,26c-22,17.3,-33.3,26,-34,26s-26,-26,-26,-26s76,-59,76,-59s76,-60,76,-60zM1001 80h400000v40h-400000z'/></svg></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.2333em;"><span></span></span></span></span></span></span></span></span></span></p><p>计算即可,而内切圆以外的部分则不能直接对应到半球面上,光标移动到这里时就无法求出轨迹球坐标了。一种比较好的解决方案是换一种曲面:只取球面的一部分,在边界处与另一个可以无限外延的曲面相接,并保证边界处的切平面相同。OpenGL Wiki 上有一个条目 <a href="https://www.khronos.org/opengl/wiki/Object_Mouse_Trackball">Object Mouse Trackball</a> 选择用旋转双曲面来延展球面,效果是这样的:(图片来自 OpenGL Wiki )</p><p><img src="/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%BA%8C%EF%BC%89%EF%BC%9A%E6%9C%80%E5%88%9D%E5%8E%9F%E5%9E%8B%E5%AE%9E%E7%8E%B0/trackball-composite.png" alt="composition surface for trackball simulation"></p><p>取 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mi>x</mi><mn>2</mn></msup><mo>+</mo><msup><mi>y</mi><mn>2</mn></msup><mo>=</mo><msup><mi>r</mi><mn>2</mn></msup><mi mathvariant="normal">/</mi><mn>2</mn></mrow><annotation encoding="application/x-tex">x^2+y^2=r^2/2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8974em;vertical-align:-0.0833em;"></span><span class="mord"><span class="mord mathnormal">x</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1.0085em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1.0641em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.02778em;">r</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span><span class="mord">/2</span></span></span></span> (上图中红色曲线)为边界,边界以内用球面、以外用旋转双曲面,得到的分段曲面方程是:</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>z</mi><mo>=</mo><mrow><mo fence="true">{</mo><mtable rowspacing="0.36em" columnalign="left left" columnspacing="1em"><mtr><mtd><mstyle scriptlevel="0" displaystyle="false"><msqrt><mrow><msup><mi>r</mi><mn>2</mn></msup><mo>−</mo><msup><mi>x</mi><mn>2</mn></msup><mo>−</mo><msup><mi>y</mi><mn>2</mn></msup></mrow></msqrt></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><mrow><msup><mi>x</mi><mn>2</mn></msup><mo>+</mo><msup><mi>y</mi><mn>2</mn></msup><mo><</mo><mfrac><msup><mi>r</mi><mn>2</mn></msup><mn>2</mn></mfrac></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="false"><mfrac><mrow><msup><mi>r</mi><mn>2</mn></msup><mi mathvariant="normal">/</mi><mn>2</mn></mrow><msqrt><mrow><msup><mi>x</mi><mn>2</mn></msup><mo>+</mo><msup><mi>y</mi><mn>2</mn></msup></mrow></msqrt></mfrac></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><mtext>otherwise</mtext></mstyle></mtd></mtr></mtable></mrow></mrow><annotation encoding="application/x-tex">z=\begin{cases}\sqrt{r^2-x^2-y^2} & x^2+y^2<\frac{r^2}{2} \\\frac{r^2/2}{\sqrt{x^2+y^2}} & \text{otherwise}\end{cases}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal" style="margin-right:0.04398em;">z</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:3.6em;vertical-align:-1.55em;"></span><span class="minner"><span class="mopen"><span class="delimsizing mult"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:2.05em;"><span style="top:-2.5em;"><span class="pstrut" style="height:3.15em;"></span><span class="delimsizinginner delim-size4"><span>⎩</span></span></span><span style="top:-2.492em;"><span class="pstrut" style="height:3.15em;"></span><span style="height:0.016em;width:0.8889em;"><svg xmlns="http://www.w3.org/2000/svg" width='0.8889em' height='0.016em' style='width:0.8889em' viewBox='0 0 888.89 16' preserveAspectRatio='xMinYMin'><path d='M384 0 H504 V16 H384z M384 0 H504 V16 H384z'/></svg></span></span><span style="top:-3.15em;"><span class="pstrut" style="height:3.15em;"></span><span class="delimsizinginner delim-size4"><span>⎨</span></span></span><span style="top:-4.292em;"><span class="pstrut" style="height:3.15em;"></span><span style="height:0.016em;width:0.8889em;"><svg xmlns="http://www.w3.org/2000/svg" width='0.8889em' height='0.016em' style='width:0.8889em' viewBox='0 0 888.89 16' preserveAspectRatio='xMinYMin'><path d='M384 0 H504 V16 H384z M384 0 H504 V16 H384z'/></svg></span></span><span style="top:-4.3em;"><span class="pstrut" style="height:3.15em;"></span><span class="delimsizinginner delim-size4"><span>⎧</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:1.55em;"><span></span></span></span></span></span></span><span class="mord"><span class="mtable"><span class="col-align-l"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.9442em;"><span style="top:-4.0352em;"><span class="pstrut" style="height:3.1089em;"></span><span class="mord"><span class="mord sqrt"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.9578em;"><span class="svg-align" style="top:-3.2em;"><span class="pstrut" style="height:3.2em;"></span><span class="mord" style="padding-left:1em;"><span class="mord"><span class="mord mathnormal" style="margin-right:0.02778em;">r</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.7401em;"><span style="top:-2.989em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord"><span class="mord mathnormal">x</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.7401em;"><span style="top:-2.989em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.7401em;"><span style="top:-2.989em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span><span style="top:-2.9178em;"><span class="pstrut" style="height:3.2em;"></span><span class="hide-tail" style="min-width:1.02em;height:1.28em;"><svg xmlns="http://www.w3.org/2000/svg" width='400em' height='1.28em' viewBox='0 0 400000 1296' preserveAspectRatio='xMinYMin slice'><path d='M263,681c0.7,0,18,39.7,52,119c34,79.3,68.167,158.7,102.5,238c34.3,79.3,51.8,119.3,52.5,120c340,-704.7,510.7,-1060.3,512,-1067l0 -0c4.7,-7.3,11,-11,19,-11H40000v40H1012.3s-271.3,567,-271.3,567c-38.7,80.7,-84,175,-136,283c-52,108,-89.167,185.3,-111.5,232c-22.3,46.7,-33.8,70.3,-34.5,71c-4.7,4.7,-12.3,7,-23,7s-12,-1,-12,-1s-109,-253,-109,-253c-72.7,-168,-109.3,-252,-110,-252c-10.7,8,-22,16.7,-34,26c-22,17.3,-33.3,26,-34,26s-26,-26,-26,-26s76,-59,76,-59s76,-60,76,-60zM1001 80h400000v40h-400000z'/></svg></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.2822em;"><span></span></span></span></span></span></span></span><span style="top:-2.4943em;"><span class="pstrut" style="height:3.1089em;"></span><span class="mord"><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.1089em;"><span style="top:-2.446em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord sqrt mtight"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0628em;"><span class="svg-align" style="top:-3.4286em;"><span class="pstrut" style="height:3.4286em;"></span><span class="mord mtight" style="padding-left:1.19em;"><span class="mord mtight"><span class="mord mathnormal mtight">x</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.7463em;"><span style="top:-2.786em;margin-right:0.0714em;"><span class="pstrut" style="height:2.5em;"></span><span class="sizing reset-size3 size1 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span><span class="mbin mtight">+</span><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.03588em;">y</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.7463em;"><span style="top:-2.786em;margin-right:0.0714em;"><span class="pstrut" style="height:2.5em;"></span><span class="sizing reset-size3 size1 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span><span style="top:-3.0348em;"><span class="pstrut" style="height:3.4286em;"></span><span class="hide-tail mtight" style="min-width:0.853em;height:1.5429em;"><svg xmlns="http://www.w3.org/2000/svg" width='400em' height='1.5429em' viewBox='0 0 400000 1080' preserveAspectRatio='xMinYMin slice'><path d='M95,702c-2.7,0,-7.17,-2.7,-13.5,-8c-5.8,-5.3,-9.5,-10,-9.5,-14c0,-2,0.3,-3.3,1,-4c1.3,-2.7,23.83,-20.7,67.5,-54c44.2,-33.3,65.8,-50.3,66.5,-51c1.3,-1.3,3,-2,5,-2c4.7,0,8.7,3.3,12,10s173,378,173,378c0.7,0,35.3,-71,104,-213c68.7,-142,137.5,-285,206.5,-429c69,-144,104.5,-217.7,106.5,-221l0 -0c5.3,-9.3,12,-14,20,-14H400000v40H845.2724s-225.272,467,-225.272,467s-235,486,-235,486c-2.7,4.7,-9,7,-19,7c-6,0,-10,-1,-12,-3s-194,-422,-194,-422s-65,47,-65,47zM834 80h400000v40h-400000z'/></svg></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.3937em;"><span></span></span></span></span></span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.485em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.02778em;">r</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8913em;"><span style="top:-2.931em;margin-right:0.0714em;"><span class="pstrut" style="height:2.5em;"></span><span class="sizing reset-size3 size1 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span><span class="mord mtight">/2</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.8296em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:1.4442em;"><span></span></span></span></span></span><span class="arraycolsep" style="width:1em;"></span><span class="col-align-l"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.9442em;"><span style="top:-4.0352em;"><span class="pstrut" style="height:3.1089em;"></span><span class="mord"><span class="mord"><span class="mord mathnormal">x</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel"><</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0179em;"><span style="top:-2.655em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">2</span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.394em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.02778em;">r</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8913em;"><span style="top:-2.931em;margin-right:0.0714em;"><span class="pstrut" style="height:2.5em;"></span><span class="sizing reset-size3 size1 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.345em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span><span style="top:-2.4943em;"><span class="pstrut" style="height:3.1089em;"></span><span class="mord"><span class="mord text"><span class="mord">otherwise</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:1.4442em;"><span></span></span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></p><p>有了统一的坐标映射,我就可以将窗口上任意一个屏幕坐标映射到轨迹球曲面坐标。用这个坐标减去原点得到一个向量,再归一化得到方向向量。为了计算拖动过程对应的旋转变换,可以记录上一次进入 <code>on_mouse_dragged</code> 时的屏幕坐标,再分别用上次的和当前的屏幕坐标计算方向向量,最后构造四元数。</p><blockquote><p>上面说的屏幕坐标都是以窗口中心点为原点,实际上通过 Dear ImGui API 获取到的坐标是以窗口左上角为原点的,需要先作一次平移变换。</p><p>构造旋转变换的原理是:从方向向量 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi mathvariant="bold">d</mi><mn>1</mn></msub></mrow><annotation encoding="application/x-tex">\mathbf{d}_1</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8444em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathbf">d</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span> 旋转到方向向量 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi mathvariant="bold">d</mi><mn>2</mn></msub></mrow><annotation encoding="application/x-tex">\mathbf{d}_2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8444em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathbf">d</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span> 时,两个方向向量作一次叉乘就可以得到旋转轴、而借助点积可以得到旋转角,进而用轴-角表示法构造四元数。不过 Eigen 已经提供了 <code>Quaternionf::FromTwoVectors</code> 这个函数,直接传入两个单位向量就能很方便地构造出四元数了。</p></blockquote><h2 id="最后一步:载入模型">最后一步:载入模型</h2><p>有 Assimp 的支持,加载模型文件还是很方便的,在依次调用 portable file dialog API(获取要打开的文件名)、Assimp Importer API(从文件中加载数据并解析为 <code>aiScene</code> 对象)后,所有的顶点和面片数据就可以直接从 <code>aiScene</code> 对象中读取。</p><p>不过,不同的模型文件格式可能具有不同的层次结构。最简单的格式只允许存储一个 mesh ,而复杂的格式则允许存储 mesh、物体、组等层次信息,甚至允许物体和组多层嵌套。我的观点是:作为一个实验框架,Dandelion 应该将同一个文件中加载的 mesh 放在一起,但没有必要支持多层嵌套的物体和组。因此这时我只给 Dandelion 设计了两种数据类:</p><ul><li><code>Mesh</code> 类存储顶点坐标、法线、面片和材质,一个 <code>Mesh</code> 对象只存储一个 mesh</li><li><code>Object</code> 类存储若干个 mesh 以及这个物体的中心坐标、旋转与伸缩参数,可以生成 Model 矩阵</li></ul><p>因此,当程序载入文件时,会生成一个用文件名命名的 <code>Object</code> 和若干用 mesh 名称命名的 <code>Mesh</code> 对象,除此以外的所有层次结构都会被抹去。由于生成 Model 矩阵所需的中心、旋转和伸缩参数都存储在 <code>Object</code> 对象中,所有属于该 <code>Object </code> 的 <code>Mesh</code> 都共享同一个 Model 矩阵,导致用户不能拆开由多个 mesh 组成的物体。</p><p><img src="/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%BA%8C%EF%BC%89%EF%BC%9A%E6%9C%80%E5%88%9D%E5%8E%9F%E5%9E%8B%E5%AE%9E%E7%8E%B0/load-object.png" alt="load an object consisting of multiple meshes"></p><p>前文说到过 Dear ImGui 的 API 是无状态的,例如绘制一个输入浮点数的拖动条要调用 <code>SliderFloat</code> 函数:</p><figure><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp"><span class="token class-name">ImGui</span><span class="token double-colon punctuation">::</span><span class="token function">SliderFloat</span><span class="token punctuation">(</span><span class="token string">"x"</span><span class="token punctuation">,</span> <span class="token operator">&</span><span class="token punctuation">(</span>selected_object<span class="token operator">-></span>center<span class="token punctuation">.</span><span class="token function">x</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token operator">-</span><span class="token number">100.0f</span><span class="token punctuation">,</span> <span class="token number">100.0f</span><span class="token punctuation">,</span> <span class="token string">"%.2f"</span><span class="token punctuation">,</span> ImGuiSliderFlags_AlwaysClamp<span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre></div></figure><p>它的几个参数含义分别是:</p><ol><li>拖动条的标签,即旁边的说明文字</li><li>影响的变量,用鼠标拖动时会修改这个变量的值,因此是传地址的</li><li>拖动条的数值范围(最小值和最大值)</li><li>显示的数字格式</li><li>一些控制样式的 flag</li></ol><p>而为了让这个拖动条能修改场景中物体的参数,我就需要将物体相应参数的地址 <code>selected_object->center.x()</code> 传入其中。渲染工具栏的函数是 <code>Toolbar::render</code> ,这个函数调用 <code>Toolbar::layout_mode</code> 渲染布局模式下的组件,像 <code>SliderFloat</code> 这样的函数就是在这里被调用的。那么为了在调用函数绘制组件时能够访问场景数据,我就要将场景本身以引用的方式传递进来,所以一次典型的交互过程是这样的:</p><p><img src="/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%BA%8C%EF%BC%89%EF%BC%9A%E6%9C%80%E5%88%9D%E5%8E%9F%E5%9E%8B%E5%AE%9E%E7%8E%B0/interaction-with-scene.svg" alt="interaction between the tool bar and the scene"></p><p>这里再次强调:之所以要让 <code>Controller</code> 将场景的引用传递给 <code>Toolbar::render</code> 而非将引用作为状态保存在 <code>Toolbar</code> 里,是因为视图层的 <code>Toolbar</code> 主要代表一系列函数调用而非对象实体,通过接收参数来访问模型层的 <code>Scene</code> 对象更符合之前的设计思路。</p><h2 id="下回分解">下回分解</h2><p>到此为止,我就获得了一个可以加载模型,并能将物体放置到场景中任意位置、能调整物体姿态朝向的小工具,也就满足了布局模式的最低要求。</p><p>这篇文章主要回顾了我从零开始写代码时的封装思路,基本上是按照我当初创建和实现类的顺序来写。然而在涉及模型加载和场景数据的部分,文中并未解释载入的数据是如何存储在 <code>Mesh</code> 对象中、<code>Mesh</code> 对象又是如何被渲染到屏幕上的。</p><p>当然了,我大可以说“这最终都是一些 OpenGL API 调用”,然后把这部分一笔带过。但 OpenGL API 既多又杂,并且在不同的任务需求下有不同的使用思路,不象 Assimp 这种工具库有一种“最佳实践风格”。所以下一篇我会对自己初步封装 OpenGL API 的思路和过程,并从事后角度重新考虑一下当时的想法是否合理。</p>]]></content:encoded>
<category domain="https://greyishsong.ink/categories/%E7%BC%96%E7%A8%8B/">编程</category>
<category domain="https://greyishsong.ink/tags/%E5%9B%BE%E5%BD%A2%E5%AD%A6/">图形学</category>
<category domain="https://greyishsong.ink/tags/%E8%AF%BE%E7%A8%8B/">课程</category>
<category domain="https://greyishsong.ink/tags/%E7%BC%96%E7%A8%8B/">编程</category>
<category domain="https://greyishsong.ink/tags/%E6%9C%AC%E7%A7%91%E6%95%99%E8%82%B2/">本科教育</category>
<comments>https://greyishsong.ink/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%BA%8C%EF%BC%89%EF%BC%9A%E6%9C%80%E5%88%9D%E5%8E%9F%E5%9E%8B%E5%AE%9E%E7%8E%B0/#disqus_thread</comments>
</item>
<item>
<title>图形学实验框架 Dandelion 始末(一):需求与设计</title>
<link>https://greyishsong.ink/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E9%9C%80%E6%B1%82%E4%B8%8E%E8%AE%BE%E8%AE%A1/</link>
<guid>https://greyishsong.ink/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E9%9C%80%E6%B1%82%E4%B8%8E%E8%AE%BE%E8%AE%A1/</guid>
<pubDate>Sat, 30 Sep 2023 06:18:35 GMT</pubDate>
<description><p>和同学聊天时开玩笑说,建设一门课就好像养了一个孩子,时时处处需要关心。今年我为本科的图形学课程编写了全新的实验框架,也算是慢慢走出了完全模仿 CMU 15/462 的阶段,试图更好地在有限的教学条件下展现图形学的趣味。其实在此之前我并未写过如此规模的程序,完成框架的过程也因此成了锻炼自己的过程。如今框架初成,用这几篇文章回顾和总结一些设计与开发的经验,既是一段给后来开发者的说明,也是记一些自己的小故事。</p></description>
<content:encoded><![CDATA[<p>和同学聊天时开玩笑说,建设一门课就好像养了一个孩子,时时处处需要关心。今年我为本科的图形学课程编写了全新的实验框架,也算是慢慢走出了完全模仿 CMU 15/462 的阶段,试图更好地在有限的教学条件下展现图形学的趣味。其实在此之前我并未写过如此规模的程序,完成框架的过程也因此成了锻炼自己的过程。如今框架初成,用这几篇文章回顾和总结一些设计与开发的经验,既是一段给后来开发者的说明,也是记一些自己的小故事。</p><span id="more"></span><blockquote><p>Dandelion 实验框架发布版本:<a href="https://github.com/XJTU-Graphics/dandelion">GitHub</a> <a href="https://gitee.com/xjtu-graphics/dandelion">Gitee</a></p></blockquote><h2 id="开发前的思考">开发前的思考</h2><h3 id="为什么不继续沿用-CMU-的方案?">为什么不继续沿用 CMU 的方案?</h3><p>在 2022 至 2023 学年,我们的图形学实验方案是直接来自 CMU 15/462 <em>Computer Graphics</em> 这门课程的。但我们只有 32 课时可用,而同学的工程能力是不如 CMU 的,因此去掉了许多实验内容,只留下了其中的六个实验,重组成难度较低的三个实验(必做)与相对较难的三个实验(三选一)。</p><p>然而这种策略与其说是删减,还不如说是挑选——因为我们放弃的实验比留用的还要多很多。这导致各实验之间几乎没有关联性可言,很难说是“一套”实验。更麻烦的是 CMU 在光栅图形学上分配的内容较少(只有一个二维光栅化器),所以我们不想采用 15/462 的光栅化实验。为了补齐这一部分,我们又引入了深圳大学的图形学实验,最终带来了更强的割裂感和很多随之而来的麻烦。于是,2022 至 2023 学年的图形学课程就留下了既可喜又遗憾的尾声:可喜的是实验和文档的质量迎来了大幅改善;遗憾的是一路磕磕绊绊,且实验和课堂并不太合拍。</p><p>虽然不完全赞同 CMU 的实验题目设计,但我确实钦佩这些教授与 TA 们为了开发 Scotty3D 实验框架所作的努力。他们留下的是一个集场景布局、模型编辑、离线渲染、骨骼动画与粒子系统于一身的全功能框架,并且代码可读性相当不错。</p><p>那么我们能否修改 Scotty3D 来适应自己的教学需求呢?我认为是可能的。但 Scotty3D 足足有一万六千行代码,却只有一千多行注释,且没有任何架构文档用于介绍模块组织方式和数据交换。我们的核心开发者仅有两个人,技术实力也偏弱,仅仅有代码可读性还是稍显不够,需要花费很多时间才能完全掌握这个具体而微的框架。这个时间是多久呢?也许一两个月,也许四五个月,我没有把握。</p><p>既然如此,不如从头重写一个自己的实验框架吧。这个想法一出现在我的脑海里,就扎根扎得越来越深,不到半个月就驱使我作出了决定。虽然造轮子不是商业美德,但它是学习美德嘛。除了“想做点什么”的动力以外,也有一些实际的考虑:</p><ul><li>自己的框架是真正“可控”的。这种可控不仅是指增减功能的自由,更是指架构和源代码的理解。只要开发过程能坚持及时记录足够的文档和注释,我们完全可以准确地理解每一个类、每一个接口的功能和设计思路,这是后续开发改进的最重要保障。</li><li>相较于效率,Scotty3D 更强调编写简单,这在实际教学过程中产生了一些性能问题。对于常见的建模工具,加载、编辑或渲染几万个三角形面的模型是很容易的。但 Scotty3D 的某些设计导致它在进入建模模式后效率很低,部分同学在轻薄笔记本上甚至要顶着个位数的帧率做实验,造成了不小的麻烦。</li><li>Scotty3D 非常强调光线追踪,很多用于光线追踪的代码与程序主体耦合得很紧,想要再编写其他的软渲染器就要大改框架。但我们认为光线追踪并不是渲染的全部,更想要一个像 Blender 那样可以切换渲染器的框架。</li><li>我们在动画方面的教学力量比较弱,所以其实不需要 Scotty3D 那么强的动画功能。</li></ul><h3 id="我们需要多少功能?">我们需要多少功能?</h3><p>一个像 Blender 一样全能的成品是很诱人的,如果我一开始没有想好要舍弃哪些功能,那么之后颇有可能陷入增添功能的热情不可自拔,进而失去了维护文档的精力,也就背离了“可控”的初衷。正如所有软件需求分析的第一步,我需要确定“做什么、不做什么”。</p><p>首先我同样想要做一个功能全面的框架,所有的实验都在同一个框架下完成。凭借自己有限的经验和知识,我大致将框架开发工作划分成五个部分:</p><ul><li>图形 API 和平台 API 抽象:在不同平台上创建程序的运行环境,负责处理退出时释放资源的操作。</li><li>场景布局和 UI:赋予使用者摆放、操作模型的能力。</li><li>渲染系统和渲染器:设置渲染参数、实现渲染管线。</li><li>建模和编辑:修改模型的几何形状,实现一些几何处理算法。</li><li>动画:基于物理的简单动画。</li></ul><p>作为一个带有中文图形界面的软件,这个框架大概会需要加载字体;而要展示三维数据,至少要使用一种图形 API ,也就需要创建类似上下文的环境并加载可用的函数地址。如果整个框架只能加载指定的几个模型,那实在谈不上有趣,因此它至少应该能任意加载一两种格式的模型文件,也能将载入的模型放在任何位置、摆出任意姿态。到此为止,可以看作是一个带有 GUI 的模型加载和预览工具。</p><p>接下来是对应于图形学三个核心问题(渲染、建模、动画)的功能性要求:</p><ul><li>由于我们希望这个框架能实现各种渲染流程,显然它应该像 Blender 一样可以切换“渲染器”,每个渲染器都对应一种渲染管线。为避免某个实验过于艰深,可以牺牲管线的灵活程度,只保留较少的可编程部分。要加载纹理贴图并保持通用性,势必为“如何识别种类繁多的图像格式”所扰,大幅增加测试成本。因此,尽管纹理贴图是渲染与几何交叉的重要领域,我还是决定暂且放弃它。</li><li>这个框架应该具备完成任意形变的能力,同时也能通过几何处理算法做一些半自动的建模。但真正成熟的建模工具需要为提高建模效率而额外开发很多功能(例如基本的对称、区域选择、区域编辑,高级的雕刻等等),这些都可以不考虑。</li><li>要实现一个骨骼动画系统,至少要处理好关节运动解算、蒙皮变形、关键帧编辑等问题,我们这方面的技术不足,因此直接放弃基于关键帧和骨骼的动画。基于物理仿真的动画则可以比较容易地实现,因为只要舍弃形变和复杂的转动判定,并且只做离散的运动仿真,就不必大量增加功能模块。</li></ul><h3 id="功能以外">功能以外</h3><p>对于一份作业而言,通常功能即一切;然而对于一个软件而言,功能只是起步。我们的框架是用于教学的,那么如何让同学快速上手规模较大的代码,尤其是如何让同学顺畅地理解不属于实验任务的代码,是非常重要的问题。另外,一个功能全面的框架要包含许多调整和设置用的 UI 元素,那么如何减少同学学习软件操作的成本也值得考虑。</p><p>第一个问题通常是由文档解决的。在以往的教学过程中,所有的接口细节都被放入实验手册中,每当我们认为同学需要使用某个接口时就在相应的章节介绍之。要把所有资料都写进实验文档,就要在实验章节末尾附加很长的调用说明和代码片段,排版到分页的 PDF 文档上不便阅读;接口说明与实验讲解绑定的做法也不利于同学之后重新查找。一个成熟的库或框架通常会有树形结构的文档,按照模块组织所有类和接口的含义说明。我准备为所有的接口编写一份类似的开发者文档,既便于同学查找,也有助于后续师弟师妹接手框架继续开发。开发者文档用网页而不是分页文档的形式发布,版式更适合代码,也更方便作小范围更新。</p><p>第二个问题则要求软件操作方法足够简单。现代的三维建模工具往往设置大量快捷键和组合键,以提高创作效率。然而我们的目标是让同学在十五分钟以内了解 Dandelion 的所有操作,点击的方式虽然低效但不必专门学习,比快捷键更合适。而各处修改属性数值的 UI 元素也应该尽可能保持相同的风格,这样尝试一次就能会用。从另一个角度来说,一套好的快捷键系统也需要经过反复的讨论、设计和修改,我们并没有完成它的专业素养和时间。</p><h2 id="自顶向下的设计:从数据展示开始">自顶向下的设计:从数据展示开始</h2><p>软件设计通常有两种思路:一是自顶向下,先设计模块再考虑细节;二是自底向上,先实现功能再尝试组合。</p><p>Dandelion 是一个与用户频繁交互的三维设计工具,当我开始设计它时,首先想到的是一些粗略的交互方式、操作界面,以及它应该如何展示各种数据,而不是某些功能具体如何实现。因此,我是采用自顶向下的思路设计 Dandelion 的。</p><h3 id="界面与模式">界面与模式</h3><p>当我只有一个尝试的念头时,Scotty3D 和 Blender 给了我 Dandelion 最初的设计目标。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E9%9C%80%E6%B1%82%E4%B8%8E%E8%AE%BE%E8%AE%A1/scotty3d.png" alt="Scotty3D"></div><div class="group-image-wrap"><img src="/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E9%9C%80%E6%B1%82%E4%B8%8E%E8%AE%BE%E8%AE%A1/blender.png" alt="Blender"></div></div></div><p>Scotty3D 和 Blender 共有的界面设计思路是“模式切换”:通过顶栏上模式选项卡可以改变当前的操作模式,不同的操作模式对应不同的功能,但共享同一份三维数据(场景数据)。对于一个全功能的设计软件来说,这样有利于将不同功能的 UI 元素聚集在一起,至少我是想不出更好的解决方案了。</p><blockquote><p>回顾一下之前提到的五个部分:图形 API 和平台 API 抽象、场景布局和 UI、渲染系统和渲染器、建模和编辑、动画</p></blockquote><p>确定了采用这种 UI 方案,就确认了所有模式下共用的基础代码:API 抽象和场景预览。</p><h3 id="技术选型">技术选型</h3><p>我希望搭建一个跨平台的框架,那么就需要处理各平台上的渲染、UI、文件交互等问题。另外,图形学实验中涉及一些线性代数计算,也需要一个数学库。</p><p>首先要选择的就是图形 API 和 GUI 框架。Dandelion 对硬件渲染的需求仅限于预览场景,复杂的渲染管线都属于离线渲染(软渲染)部分。因此,我认为没有必要用 Vulkan 等现代 API 来提高性能,选择跨平台更容易、开发也更简单的 OpenGL 更合适。配合 OpenGL 使用的 loader 是 GLFW 和 GLAD 。</p><p>在 GUI 方面,完全跨平台的 C++ GUI 方案主要是 Qt 和各类 IMGUI(立即模式 GUI)。从课程实验框架的角度看,</p><ul><li>Qt 能力更全面,但依赖更重、构建更复杂、学习成本更高</li><li>IMGUI 依赖更少,但稳定性可能不如 Qt</li></ul><p>由于我更看重轻量化和降低学习成本,一个 IMGUI 库更合适些,最后选择了经典的 Dear ImGui 。</p><p>文件交互方面,</p><ul><li>Scotty3D 的做法是自己封装跨平台的文件对话框 API,在 Windows / Linux / macOS 上分别使用 Win32 / GTK3 / Apple 原生 API 创建文件对话框</li><li>Blender 的做法是直接实现一套跨平台自绘 GUI ,直接封装操作系统的文件操作 API</li></ul><p>二者成本都相当高,并且在 Linux 上需要引入额外的依赖 (pkg-config) 来编译 GTK3 。后来我找到了 portable-file-dialog 这个项目,它是对各平台原生文件对话框的简单封装,在 Windows 上使用 Win32 API 、GNOME / KDE 上调用命令、macOS 上使用 AppleScript ,整个项目只有一个文件,依赖的原生 API 或命令(脚本语言)也都很稳定,完全可以满足需求。</p><p>数学计算方面有 GLM 和 Eigen 两个候选项。GLM 更轻并且与 OpenGL API 结合得更好,但缺乏求解方程组的能力,而几何处理和物理仿真很可能需要它,所以我还是选择了 Eigen 。</p><h3 id="GUI-软件的架构模式:MVC">GUI 软件的架构模式:MVC</h3><p>模型-视图-控制器 (Model-View-Controller, MVC) 也许是最经典的现代 GUI 应用架构模式了。这种架构方法将整个软件分为三部分:</p><ul><li>进行逻辑处理并提供数据的<strong>模型</strong></li><li>接收数据进行可视化的<strong>视图</strong></li><li>处理输入和交互逻辑的<strong>控制器</strong></li></ul><p>之后还有 MVVM 等改进的架构,不过对 Dandelion 这个不大的软件来说经典的 MVC 应该够用了。将开发任务按照 MVC 架构进行充足,就形成了一个初步的架构图:</p><p><img src="/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E9%9C%80%E6%B1%82%E4%B8%8E%E8%AE%BE%E8%AE%A1/MVC-architecture.svg" alt="MVC Architecture"></p><p>上图中低层 API 抽象、GUI 组件两部分所对应的功能都受到了 Dear ImGui 这个 GUI 库的影响:使用立即模式 GUI 时,开发者在每一帧调用函数来请求绘制 UI 组件,而 UI 组件内部的状态是不可见的,对外表现为无状态 UI 。因此,与用户交互相关的全局状态需要在控制器部分维护,以此决定要请求绘制哪些 UI 组件。场景预览的部分直接调用 OpenGL API 渲染而不经过 Dear ImGui ,对场景的交互操作(例如旋转、平移视角)也需要通过直接封装 GLFW API 来检测和响应。</p><p>考虑场景预览、渲染、动画、建模等过程,用箭头标出模型层与视图层间的数据流向,可以看到场景存储是很多视图模块的数据源,这正是多个模式共用场景数据的结果。</p><h2 id="最初原型">最初原型</h2><p>根据之前所作的的需求分析、架构设计,第一阶段的原型需要实现的部分是:</p><ul><li>低层 API 抽象</li><li>场景存储与访问</li><li>场景预览</li><li>物体参数</li><li>鼠标和按键输入处理</li><li>全局状态维护</li></ul><p>这个原型完成后的效果大致是一个可以打开模型文件并显示在场景中的预览工具,除此以外没有其他功能。最初的设计至此完成,下一步的工作就是开始设计类和接口,开始写代码了。</p>]]></content:encoded>
<category domain="https://greyishsong.ink/categories/%E7%BC%96%E7%A8%8B/">编程</category>
<category domain="https://greyishsong.ink/tags/%E5%9B%BE%E5%BD%A2%E5%AD%A6/">图形学</category>
<category domain="https://greyishsong.ink/tags/%E8%AF%BE%E7%A8%8B/">课程</category>
<category domain="https://greyishsong.ink/tags/%E7%BC%96%E7%A8%8B/">编程</category>
<category domain="https://greyishsong.ink/tags/%E6%9C%AC%E7%A7%91%E6%95%99%E8%82%B2/">本科教育</category>
<comments>https://greyishsong.ink/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AE%9E%E9%AA%8C%E6%A1%86%E6%9E%B6-Dandelion-%E5%A7%8B%E6%9C%AB%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E9%9C%80%E6%B1%82%E4%B8%8E%E8%AE%BE%E8%AE%A1/#disqus_thread</comments>
</item>
<item>
<title>初涉 λ 演算 (lambda calculus)</title>
<link>https://greyishsong.ink/%E5%88%9D%E6%B6%89-%CE%BB-%E6%BC%94%E7%AE%97-lambda-calculus/</link>
<guid>https://greyishsong.ink/%E5%88%9D%E6%B6%89-%CE%BB-%E6%BC%94%E7%AE%97-lambda-calculus/</guid>
<pubDate>Sun, 28 May 2023 06:59:52 GMT</pubDate>
<description><p>不幸上了一门干瘪枯燥的形式化方法课程,但课讲得不太好并不影响这些知识的趣味,毕竟还可以自己学嘛。课程内容更偏向软件形式化验证,不过会补充 λ 演算的内容,并且用到的证明器 Coq 也比较像是一种函数式语言。而相比于形式化验证,我对 λ 演算和函数式编程的兴趣更多一点。借着几篇博客和一些视频资料了解 λ 演算是什么之后,我也写下了几页的草稿和笔记。这里再整理一下自己的思路,从原始的 λ 演算系统开始构造一些简单的算数和逻辑表达。</p></description>
<content:encoded><![CDATA[<p>不幸上了一门干瘪枯燥的形式化方法课程,但课讲得不太好并不影响这些知识的趣味,毕竟还可以自己学嘛。课程内容更偏向软件形式化验证,不过会补充 λ 演算的内容,并且用到的证明器 Coq 也比较像是一种函数式语言。而相比于形式化验证,我对 λ 演算和函数式编程的兴趣更多一点。借着几篇博客和一些视频资料了解 λ 演算是什么之后,我也写下了几页的草稿和笔记。这里再整理一下自己的思路,从原始的 λ 演算系统开始构造一些简单的算数和逻辑表达。</p><span id="more"></span><p>这篇文章主要是笔记,用来整理我跟着<sup id="fnref:3" class="footnote-ref"><a href="#fn:3" rel="footnote"><span class="hint--top hint--rounded" aria-label="[lambda演算科普](https://www.bilibili.com/video/BV1pU4y1v7Hj)">[3]</span></a></sup>学习 λ 演算时略有混乱的思路。推导这些东西未见得有什么实际作用,但是我觉得好玩,也就记下来了。也感谢<sup id="fnref:1" class="footnote-ref"><a href="#fn:1" rel="footnote"><span class="hint--top hint--rounded" aria-label="[Y组合子的一个启发式推导](https://zhuanlan.zhihu.com/p/547191928)">[1]</span></a></sup> <sup id="fnref:2" class="footnote-ref"><a href="#fn:2" rel="footnote"><span class="hint--top hint--rounded" aria-label="[Y组合子(Y Combinator)| The Little Schemer 第九章](https://zhuanlan.zhihu.com/p/262284625)">[2]</span></a></sup> 两篇文章分享了推导思路的细节,这些细节着实解决了我的疑问。下面的内容有些长,主要原因是我写了不少自己的思考过程,以免日后想再看时发现无从翻找。</p><h2 id="原始的-λ-演算规则">原始的 λ 演算规则</h2><p>对一个编程多于推导的人来说,λ 演算看起来更像是一种“字符串代换规则”,只有两条:</p><ul><li><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mi>E</mi></mrow><annotation encoding="application/x-tex">\lambda x.E</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.05764em;">E</span></span></span></span> 定义一个函数:<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>x</mi></mrow><annotation encoding="application/x-tex">x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">x</span></span></span></span> 表示一个绑定变量(函数参数)、<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>E</mi></mrow><annotation encoding="application/x-tex">E</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.05764em;">E</span></span></span></span> 是表示函数内容的表达式。</li><li><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>E</mi><mtext> </mtext><mi>x</mi></mrow><annotation encoding="application/x-tex">E\;x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.05764em;">E</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span></span></span></span> 表示代入:将变量 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>x</mi></mrow><annotation encoding="application/x-tex">x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">x</span></span></span></span> 代入到变量 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>E</mi></mrow><annotation encoding="application/x-tex">E</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.05764em;">E</span></span></span></span> 中,或者说将 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>E</mi></mrow><annotation encoding="application/x-tex">E</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.05764em;">E</span></span></span></span> 作用于 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>x</mi></mrow><annotation encoding="application/x-tex">x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">x</span></span></span></span> 。</li></ul><p>一个 λ 表达式就是一段包含上面两种情况的文本。对 λ 表达式而言,任何东西都是变量,函数也不例外。λ 演算的主要工作就是写一些表达式,然后进行“归约” (reduction):</p><ul><li>α 等价 (α-equivalence):函数的绑定变量可以随意改名,<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mi>f</mi><mtext> </mtext><mi>x</mi></mrow><annotation encoding="application/x-tex">\lambda x.f\;x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span></span></span></span> 等价于 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>λ</mi><mi>y</mi><mi mathvariant="normal">.</mi><mi>f</mi><mtext> </mtext><mi>y</mi></mrow><annotation encoding="application/x-tex">\lambda y.f\;y</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span></span></span></span></li><li>β 归约:执行代入,<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false">(</mo><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mi>f</mi><mtext> </mtext><mi>x</mi><mo stretchy="false">)</mo><mtext> </mtext><mi>y</mi></mrow><annotation encoding="application/x-tex">(\lambda x.f\;x)\;y</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span></span></span></span> 等价于 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi><mtext> </mtext><mi>y</mi></mrow><annotation encoding="application/x-tex">f\;y</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span></span></span></span></li><li>η 归约:去掉多余的绑定变量。由于将任何表达式 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>y</mi></mrow><annotation encoding="application/x-tex">y</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.625em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span></span></span></span> 代入到 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mi>f</mi><mtext> </mtext><mi>x</mi></mrow><annotation encoding="application/x-tex">\lambda x.f\;x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span></span></span></span> 中都会得到 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi><mtext> </mtext><mi>y</mi></mrow><annotation encoding="application/x-tex">f\;y</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span></span></span></span> ,写成 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mi>f</mi><mtext> </mtext><mi>x</mi></mrow><annotation encoding="application/x-tex">\lambda x.f\;x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span></span></span></span> 和只写 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi></mrow><annotation encoding="application/x-tex">f</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span></span></span></span> 实际上没有区别。</li></ul><p>这样的表达式定义和归约规则看起来简直只剩抽象了,很难和计算联系到一起。但 λ 演算是和图灵机等价的计算模型,它也能构造一个通用计算机,解决图灵机能解决的所有问题,只是所有的“计算”都需要从表达式开始自己构造。</p><h2 id="自然数和算数运算">自然数和算数运算</h2><p>对于计算机来说,一切计算从整数开始。从自然数开始更简单一些,但 λ 演算连数字也没有定义,所以想加减乘除,首先需要定义自然数。任何 λ 表达式都逃不出那两条原始规则,那么 λ 演算意义下的“自然数”也得是某种 λ 表达式。皮亚诺算数体系可以帮助我们定义一个满足算数公理的自然数集合,它有两条基本规则:</p><ol><li>0 是自然数。</li><li>每个自然数有其后继,这个后继也是自然数。</li></ol><p>照着这个规定,邱奇这样定义 λ 演算中的“自然数”:</p><ol><li><p>给定一个函数 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi></mrow><annotation encoding="application/x-tex">f</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span></span></span></span> ,0 就是这个函数<strong>不作用于</strong> <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>x</mi></mrow><annotation encoding="application/x-tex">x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">x</span></span></span></span> 的结果。</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mn>0</mn><mo>≜</mo><mi>λ</mi><mi>f</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mi>x</mi></mrow><annotation encoding="application/x-tex">0\triangleq\lambda f.\lambda x.x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.9987em;vertical-align:-0.082em;"></span><span class="mord">0</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mord">.</span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord mathnormal">x</span></span></span></span></span></p></li><li><p>自然数 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>k</mi></mrow><annotation encoding="application/x-tex">k</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span></span></span></span> 是这个函数连续 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>k</mi></mrow><annotation encoding="application/x-tex">k</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span></span></span></span> 次作用于 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>x</mi></mrow><annotation encoding="application/x-tex">x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">x</span></span></span></span> 的结果,或者说 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>k</mi></mrow><annotation encoding="application/x-tex">k</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span></span></span></span> 的后继比 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>k</mi></mrow><annotation encoding="application/x-tex">k</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span></span></span></span> 多作用一次。写成 λ 表达式是</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>k</mi><mo>+</mo><mn>1</mn><mo>≜</mo><mi>λ</mi><mi>f</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mi>f</mi><mtext> </mtext><mo stretchy="false">(</mo><mi>k</mi><mtext> </mtext><mi>f</mi><mtext> </mtext><mi>x</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">k+1\triangleq\lambda f.\lambda x.f\;(k\;f\;x)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.7778em;vertical-align:-0.0833em;"></span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.9987em;vertical-align:-0.082em;"></span><span class="mord">1</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mord">.</span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span><span class="mclose">)</span></span></span></span></span></p></li></ol><p>按照第二条规则构造的“自然数”就是邱奇数 <em>Church Numerals</em> ,</p><table><thead><tr><th>邱奇数</th><th>λ 表达式</th><th>代数记法</th></tr></thead><tbody><tr><td>0</td><td><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>λ</mi><mi>f</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mi>x</mi></mrow><annotation encoding="application/x-tex">\lambda f.\lambda x.x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mord">.</span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord mathnormal">x</span></span></span></span></td><td><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>x</mi></mrow><annotation encoding="application/x-tex">x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">x</span></span></span></span></td></tr><tr><td>1</td><td><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>λ</mi><mi>f</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mi>f</mi><mtext> </mtext><mi>x</mi></mrow><annotation encoding="application/x-tex">\lambda f.\lambda x.f\;x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mord">.</span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span></span></span></span></td><td><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi><mo stretchy="false">(</mo><mi>x</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">f(x)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mclose">)</span></span></span></span></td></tr><tr><td>2</td><td><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>λ</mi><mi>f</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mi>f</mi><mtext> </mtext><mo stretchy="false">(</mo><mi>f</mi><mtext> </mtext><mi>x</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">\lambda f.\lambda x.f\;(f\;x)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mord">.</span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span><span class="mclose">)</span></span></span></span></td><td><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi><mo stretchy="false">(</mo><mi>f</mi><mo stretchy="false">(</mo><mi>x</mi><mo stretchy="false">)</mo><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">f(f(x))</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mclose">))</span></span></span></span></td></tr><tr><td>3</td><td><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>λ</mi><mi>f</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mi>f</mi><mtext> </mtext><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">(</mo><mi>f</mi><mtext> </mtext><mo stretchy="false">(</mo><mi>f</mi><mtext> </mtext><mi>x</mi><mo stretchy="false">)</mo><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">)</mo></mrow><annotation encoding="application/x-tex">\lambda f.\lambda x.f\;\big(f\;(f\;x)\big)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.2em;vertical-align:-0.35em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mord">.</span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord"><span class="delimsizing size1">(</span></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span><span class="mclose">)</span><span class="mord"><span class="delimsizing size1">)</span></span></span></span></span></td><td><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi><mo stretchy="false">(</mo><mi>f</mi><mo stretchy="false">(</mo><mi>f</mi><mo stretchy="false">(</mo><mi>x</mi><mo stretchy="false">)</mo><mo stretchy="false">)</mo><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">f(f(f(x)))</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mclose">)))</span></span></span></span></td></tr></tbody></table><blockquote><p>这样定义出来的“自然数”确实还是 λ 表达式,并且自身还是个函数。例如当我们归约到 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>λ</mi><mi>f</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mi>f</mi><mtext> </mtext><mi>x</mi></mrow><annotation encoding="application/x-tex">\lambda f.\lambda x.f\;x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mord">.</span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span></span></span></span> 时,就称之为得到了 1 ,而原始的 λ 演算中并没有“值”和“计算(过程)”这样的区分,所有的表达式都可以叫做变量 (variable)。</p><p>当表达式越来越长时,就容易产生演算顺序上的歧义。为方便起见,一般直接规定:函数定义尽可能向右延伸, <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mi>f</mi><mtext> </mtext><mi>x</mi></mrow><annotation encoding="application/x-tex">\lambda x.f\;x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span></span></span></span> 表示 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mo stretchy="false">(</mo><mi>f</mi><mtext> </mtext><mi>x</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">\lambda x.(f\;x)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span><span class="mclose">)</span></span></span></span> ;而作用 (application) 则是左结合的,<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi><mtext> </mtext><mi>g</mi><mtext> </mtext><mi>x</mi></mrow><annotation encoding="application/x-tex">f\;g\;x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span></span></span></span> 表示 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false">(</mo><mi>f</mi><mtext> </mtext><mi>g</mi><mo stretchy="false">)</mo><mtext> </mtext><mi>x</mi></mrow><annotation encoding="application/x-tex">(f\;g)\;x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span></span></span></span> 。</p></blockquote><p>有了邱奇数之后,下一步就是加减乘除了。根据代数知识,加法和乘法都<strong>对自然数集封闭</strong>,这个性质是比较好的;而且正整数加法和乘法都是越算越大,这意味着函数 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi></mrow><annotation encoding="application/x-tex">f</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span></span></span></span> 作用的次数只会增加不会减少,这意味着<strong>不用求逆</strong>。</p><h3 id="加法和乘法">加法和乘法</h3><p>定义邱奇数的乘法最简单,因为在 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>λ</mi><mi>f</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mi>f</mi><mtext> </mtext><mi>x</mi></mrow><annotation encoding="application/x-tex">\lambda f.\lambda x.f\;x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mord">.</span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span></span></span></span> 这样的表达式中,<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>x</mi></mrow><annotation encoding="application/x-tex">x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">x</span></span></span></span> 也可以是一个函数!这样我们就可以让 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>b</mi></mrow><annotation encoding="application/x-tex">b</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">b</span></span></span></span> 作用于 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi></mrow><annotation encoding="application/x-tex">f</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span></span></span></span> 得到 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>b</mi><mtext> </mtext><mi>f</mi></mrow><annotation encoding="application/x-tex">b\;f</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal">b</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span></span></span></span> ,这是 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi></mrow><annotation encoding="application/x-tex">f</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span></span></span></span> 的连续 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>b</mi></mrow><annotation encoding="application/x-tex">b</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">b</span></span></span></span> 次复合;再让 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi></mrow><annotation encoding="application/x-tex">a</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">a</span></span></span></span> 作用于这个复合函数,得到的就是 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi></mrow><annotation encoding="application/x-tex">f</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span></span></span></span> 的连续 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi><mo>×</mo><mi>b</mi></mrow><annotation encoding="application/x-tex">a\times b</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6667em;vertical-align:-0.0833em;"></span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">×</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">b</span></span></span></span> 次复合:</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mo>×</mo><mo>≜</mo><mi>λ</mi><mi>a</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>b</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>f</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mi>a</mi><mtext> </mtext><mo stretchy="false">(</mo><mi>b</mi><mtext> </mtext><mi>f</mi><mo stretchy="false">)</mo><mtext> </mtext><mi>x</mi></mrow><annotation encoding="application/x-tex">\times\triangleq\lambda a.\lambda b.\lambda f.\lambda x.a\;(b\;f)\;x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.0833em;"></span><span class="mord">×</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal">λa</span><span class="mord">.</span><span class="mord mathnormal">λb</span><span class="mord">.</span><span class="mord mathnormal">λ</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mord">.</span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mord mathnormal">b</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span></span></span></span></span></p><p>加法要得到 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi></mrow><annotation encoding="application/x-tex">f</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span></span></span></span> 的 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi><mo>+</mo><mi>b</mi></mrow><annotation encoding="application/x-tex">a+b</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6667em;vertical-align:-0.0833em;"></span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">b</span></span></span></span> 次复合,我们可以分别得到 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi></mrow><annotation encoding="application/x-tex">f</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span></span></span></span> 的连续 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi></mrow><annotation encoding="application/x-tex">a</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">a</span></span></span></span> 次和连续 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>b</mi></mrow><annotation encoding="application/x-tex">b</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">b</span></span></span></span> 次复合,再将 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>b</mi></mrow><annotation encoding="application/x-tex">b</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">b</span></span></span></span> 次复合的作用结果 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>b</mi><mtext> </mtext><mi>f</mi><mtext> </mtext><mi>x</mi></mrow><annotation encoding="application/x-tex">b\;f\;x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal">b</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span></span></span></span> 代入到 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi></mrow><annotation encoding="application/x-tex">a</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">a</span></span></span></span> 次复合的函数中:</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mo>+</mo><mo>≜</mo><mi>λ</mi><mi>a</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>b</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>f</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mo stretchy="false">(</mo><mi>a</mi><mtext> </mtext><mi>f</mi><mo stretchy="false">)</mo><mtext> </mtext><mo stretchy="false">(</mo><mi>b</mi><mtext> </mtext><mi>f</mi><mtext> </mtext><mi>x</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">+\triangleq\lambda a.\lambda b.\lambda f.\lambda x.(a\;f)\;(b\;f\;x)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.0833em;"></span><span class="mord">+</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal">λa</span><span class="mord">.</span><span class="mord mathnormal">λb</span><span class="mord">.</span><span class="mord mathnormal">λ</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mord">.</span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mopen">(</span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mord mathnormal">b</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span><span class="mclose">)</span></span></span></span></span></p><p>由于 λ 表达式允许<strong>函数作用于函数</strong>(因为函数也是变量),有时看起来不太直观。如果换成代数记法把函数参数写在括号里,乘法是 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">(</mo><mi>b</mi><mo stretchy="false">(</mo><mi>f</mi><mo stretchy="false">)</mo><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">)</mo><mo stretchy="false">(</mo><mi>x</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">a\big(b(f)\big)(x)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.2em;vertical-align:-0.35em;"></span><span class="mord mathnormal">a</span><span class="mord"><span class="delimsizing size1">(</span></span><span class="mord mathnormal">b</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mclose">)</span><span class="mord"><span class="delimsizing size1">)</span></span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mclose">)</span></span></span></span> ,加法是 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi><mo stretchy="false">(</mo><mi>f</mi><mo stretchy="false">)</mo><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">(</mo><mi>b</mi><mo stretchy="false">(</mo><mi>f</mi><mo stretchy="false">)</mo><mo stretchy="false">(</mo><mi>x</mi><mo stretchy="false">)</mo><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">)</mo></mrow><annotation encoding="application/x-tex">a(f)\big(b(f)(x)\big)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.2em;vertical-align:-0.35em;"></span><span class="mord mathnormal">a</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mclose">)</span><span class="mord"><span class="delimsizing size1">(</span></span><span class="mord mathnormal">b</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mclose">)</span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mclose">)</span><span class="mord"><span class="delimsizing size1">)</span></span></span></span></span> 。</p><blockquote><p>这样可以更明显地看出: <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi></mrow><annotation encoding="application/x-tex">a</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">a</span></span></span></span> 和 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>b</mi></mrow><annotation encoding="application/x-tex">b</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">b</span></span></span></span> 在上述的运算过程中是<strong>函数到函数的映射</strong>,在分析学中称为<strong>算子</strong>。在 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi><mtext> </mtext><mi>x</mi></mrow><annotation encoding="application/x-tex">f\;x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span></span></span></span> 这个代入中,λ 演算在形式上允许 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi></mrow><annotation encoding="application/x-tex">f</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span></span></span></span> 和 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>x</mi></mrow><annotation encoding="application/x-tex">x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">x</span></span></span></span> 是任何东西,所以我们可以把它们都当作(分析学意义上的)函数,那么邱奇数就都是算子。</p><p>如果 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi></mrow><annotation encoding="application/x-tex">a</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">a</span></span></span></span> 或 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>b</mi></mrow><annotation encoding="application/x-tex">b</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">b</span></span></span></span> 为 0 ,只要注意任何函数 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi></mrow><annotation encoding="application/x-tex">f</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span></span></span></span> 代入 0 之后都不起作用,根据上面的定义就可以验证 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo>×</mo><mi>x</mi><mtext> </mtext><mn>0</mn></mrow><annotation encoding="application/x-tex">\times x\;0</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.7278em;vertical-align:-0.0833em;"></span><span class="mord">×</span><span class="mord mathnormal">x</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord">0</span></span></span></span> 是 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>0</mn></mrow><annotation encoding="application/x-tex">0</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6444em;"></span><span class="mord">0</span></span></span></span> 而 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo>+</mo><mi>x</mi><mtext> </mtext><mn>0</mn></mrow><annotation encoding="application/x-tex">+ x\;0</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.7278em;vertical-align:-0.0833em;"></span><span class="mord">+</span><span class="mord mathnormal">x</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord">0</span></span></span></span> 是 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>x</mi></mrow><annotation encoding="application/x-tex">x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">x</span></span></span></span> 。</p></blockquote><h3 id="前驱与减法">前驱与减法</h3><p>刚才说过,加法和乘法都不用求 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi></mrow><annotation encoding="application/x-tex">f</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span></span></span></span> 的逆,减法就要麻烦一些——因为我们希望知道 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>k</mi></mrow><annotation encoding="application/x-tex">k</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span></span></span></span> 的<strong>前驱</strong>是谁,而函数 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi></mrow><annotation encoding="application/x-tex">f</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span></span></span></span> 是一个任意的 λ 表达式,我们又没有定义代入的“逆运算”是什么。为此可以用一种笨办法求 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>n</mi></mrow><annotation encoding="application/x-tex">n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">n</span></span></span></span> 的前驱:</p><ol><li>最初,我们有一个二元组 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo><</mo><mn>0</mn><mo separator="true">,</mo><mn>0</mn><mo>></mo></mrow><annotation encoding="application/x-tex"><0,0></annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.5782em;vertical-align:-0.0391em;"></span><span class="mrel"><</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8389em;vertical-align:-0.1944em;"></span><span class="mord">0</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord">0</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">></span></span></span></span></li><li>取第二个分量的后继,并用原先的第二个分量作为下次的第一个分量:<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo><</mo><mi>a</mi><mo separator="true">,</mo><mi>b</mi><mo>></mo><mo>→</mo><mo><</mo><mi>b</mi><mo separator="true">,</mo><mi>b</mi><mo>+</mo><mn>1</mn><mo>></mo></mrow><annotation encoding="application/x-tex"><a,b>\rarr<b,b+1></annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.5782em;vertical-align:-0.0391em;"></span><span class="mrel"><</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal">a</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal">b</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">>→<</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal">b</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal">b</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.6835em;vertical-align:-0.0391em;"></span><span class="mord">1</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">></span></span></span></span></li><li>把第二步视为一个函数,这个函数连续 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>n</mi></mrow><annotation encoding="application/x-tex">n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">n</span></span></span></span> 次作用于 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo><</mo><mn>0</mn><mo separator="true">,</mo><mn>0</mn><mo>></mo></mrow><annotation encoding="application/x-tex"><0,0></annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.5782em;vertical-align:-0.0391em;"></span><span class="mrel"><</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8389em;vertical-align:-0.1944em;"></span><span class="mord">0</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord">0</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">></span></span></span></span> 后正好得到 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo><</mo><mi>n</mi><mo>−</mo><mn>1</mn><mo separator="true">,</mo><mi>n</mi><mo>></mo></mrow><annotation encoding="application/x-tex"><n-1,n></annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.5782em;vertical-align:-0.0391em;"></span><span class="mrel"><</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.6667em;vertical-align:-0.0833em;"></span><span class="mord mathnormal">n</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.8389em;vertical-align:-0.1944em;"></span><span class="mord">1</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal">n</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">></span></span></span></span></li></ol><p>有了加法和乘法的经验,第三步不难和邱奇数联系起来。但这个笨办法还是不能马上实现,因为我们还没定义什么是二元组、如何取出二元组的分量。</p><p>和编程不同的是:函数是没有赋值操作的,并<strong>不能记录某种状态</strong>。但函数有参数,复合函数求值的过程中,外层函数可以用自己的参数构造内层函数的参数。相应地,我们在 λ 表达式中不是用变量去记录状态,而是<strong>用参数去传递状态</strong>,这样就能定义出二元组和取分量的函数。</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mtable rowspacing="0.25em" columnalign="right left" columnspacing="0em"><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mi mathvariant="normal">p</mi><mi mathvariant="normal">a</mi><mi mathvariant="normal">i</mi><mi mathvariant="normal">r</mi></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mo>≜</mo><mi>λ</mi><mi>a</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>b</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>f</mi><mi mathvariant="normal">.</mi><mi>f</mi><mtext> </mtext><mi>a</mi><mtext> </mtext><mi>b</mi></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mi mathvariant="normal">f</mi><mi mathvariant="normal">i</mi></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mo>≜</mo><mi>λ</mi><mi>p</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>a</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>b</mi><mi mathvariant="normal">.</mi><mi>a</mi></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mi mathvariant="normal">s</mi><mi mathvariant="normal">e</mi></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mo>≜</mo><mi>λ</mi><mi>p</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>a</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>b</mi><mi mathvariant="normal">.</mi><mi>b</mi></mrow></mstyle></mtd></mtr></mtable><annotation encoding="application/x-tex">\begin{aligned}\mathrm{pair}&\triangleq\lambda a.\lambda b.\lambda f.f\;a\;b \\\mathrm{fi}&\triangleq\lambda p.\lambda a.\lambda b.a \\\mathrm{se}&\triangleq\lambda p.\lambda a.\lambda b.b\end{aligned}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:4.73em;vertical-align:-2.115em;"></span><span class="mord"><span class="mtable"><span class="col-align-r"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:2.615em;"><span style="top:-4.6983em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"><span class="mord mathrm">pair</span></span></span></span><span style="top:-3.1217em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"><span class="mord mathrm">fi</span></span></span></span><span style="top:-1.545em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"><span class="mord mathrm">se</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:2.115em;"><span></span></span></span></span></span><span class="col-align-l"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:2.615em;"><span style="top:-4.6983em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">λa</span><span class="mord">.</span><span class="mord mathnormal">λb</span><span class="mord">.</span><span class="mord mathnormal">λ</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">b</span></span></span><span style="top:-3.1217em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal">p</span><span class="mord">.</span><span class="mord mathnormal">λa</span><span class="mord">.</span><span class="mord mathnormal">λb</span><span class="mord">.</span><span class="mord mathnormal">a</span></span></span><span style="top:-1.545em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal">p</span><span class="mord">.</span><span class="mord mathnormal">λa</span><span class="mord">.</span><span class="mord mathnormal">λb</span><span class="mord">.</span><span class="mord mathnormal">b</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:2.115em;"><span></span></span></span></span></span></span></span></span></span></span></span></p><p>结合二元组、邱奇数,就可以实现前驱函数:</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mrow><mi mathvariant="normal">p</mi><mi mathvariant="normal">r</mi><mi mathvariant="normal">e</mi><mi mathvariant="normal">d</mi></mrow><mo>≜</mo><mi>λ</mi><mi>n</mi><mi mathvariant="normal">.</mi><mrow><mi mathvariant="normal">f</mi><mi mathvariant="normal">i</mi></mrow><mtext> </mtext><mo fence="false" stretchy="true" minsize="1.8em" maxsize="1.8em">(</mo><mi>n</mi><mtext> </mtext><mstyle mathcolor="royalblue"><mi>λ</mi><mi>p</mi><mi mathvariant="normal">.</mi><mrow><mi mathvariant="normal">p</mi><mi mathvariant="normal">a</mi><mi mathvariant="normal">i</mi><mi mathvariant="normal">r</mi></mrow><mtext> </mtext><mo stretchy="false">(</mo><mrow><mi mathvariant="normal">s</mi><mi mathvariant="normal">e</mi></mrow><mtext> </mtext><mi>p</mi><mo stretchy="false">)</mo><mtext> </mtext><mo stretchy="false">(</mo><mo>+</mo><mtext> </mtext><mn>1</mn><mtext> </mtext><mo stretchy="false">(</mo><mrow><mi mathvariant="normal">s</mi><mi mathvariant="normal">e</mi></mrow><mtext> </mtext><mi>p</mi><mo stretchy="false">)</mo><mo stretchy="false">)</mo></mstyle><mtext> </mtext><mo stretchy="false">(</mo><mrow><mi mathvariant="normal">p</mi><mi mathvariant="normal">a</mi><mi mathvariant="normal">i</mi><mi mathvariant="normal">r</mi></mrow><mtext> </mtext><mn>0</mn><mtext> </mtext><mn>0</mn><mo stretchy="false">)</mo><mo fence="false" stretchy="true" minsize="1.8em" maxsize="1.8em">)</mo></mrow><annotation encoding="application/x-tex">\mathrm{pred}\triangleq\lambda n.\mathrm{fi}\;\Big(n\;\textcolor{royalblue}{\lambda p.\mathrm{pair}\;(\mathrm{se}\;p)\;(+\;1\;(\mathrm{se}\;p))}\;(\mathrm{pair}\;0\;0)\Big)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.1111em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathrm">pred</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1.8em;vertical-align:-0.65em;"></span><span class="mord mathnormal">λn</span><span class="mord">.</span><span class="mord"><span class="mord mathrm">fi</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord"><span class="delimsizing size2">(</span></span><span class="mord mathnormal">n</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal" style="color:royalblue;">λ</span><span class="mord mathnormal" style="color:royalblue;">p</span><span class="mord" style="color:royalblue;">.</span><span class="mord" style="color:royalblue;"><span class="mord mathrm" style="color:royalblue;">pair</span></span><span class="mspace" style="color:royalblue;margin-right:0.2778em;"></span><span class="mopen" style="color:royalblue;">(</span><span class="mord" style="color:royalblue;"><span class="mord mathrm" style="color:royalblue;">se</span></span><span class="mspace" style="color:royalblue;margin-right:0.2778em;"></span><span class="mord mathnormal" style="color:royalblue;">p</span><span class="mclose" style="color:royalblue;">)</span><span class="mspace" style="color:royalblue;margin-right:0.2778em;"></span><span class="mopen" style="color:royalblue;">(</span><span class="mord" style="color:royalblue;">+</span><span class="mspace" style="color:royalblue;margin-right:0.2778em;"></span><span class="mord" style="color:royalblue;">1</span><span class="mspace" style="color:royalblue;margin-right:0.2778em;"></span><span class="mopen" style="color:royalblue;">(</span><span class="mord" style="color:royalblue;"><span class="mord mathrm" style="color:royalblue;">se</span></span><span class="mspace" style="color:royalblue;margin-right:0.2778em;"></span><span class="mord mathnormal" style="color:royalblue;">p</span><span class="mclose" style="color:royalblue;">))</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mord"><span class="mord mathrm">pair</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord">0</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord">0</span><span class="mclose">)</span><span class="mord"><span class="delimsizing size2">)</span></span></span></span></span></span></p><p>这个式子有些长,其中蓝色的部分是 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo><</mo><mi>a</mi><mo separator="true">,</mo><mi>b</mi><mo>></mo><mo>→</mo><mo><</mo><mi>b</mi><mo separator="true">,</mo><mi>b</mi><mo>+</mo><mn>1</mn><mo>></mo></mrow><annotation encoding="application/x-tex"><a,b>\rarr<b,b+1></annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.5782em;vertical-align:-0.0391em;"></span><span class="mrel"><</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal">a</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal">b</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">>→<</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal">b</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal">b</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.6835em;vertical-align:-0.0391em;"></span><span class="mord">1</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">></span></span></span></span> 这一步,而 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi mathvariant="normal">p</mi><mi mathvariant="normal">a</mi><mi mathvariant="normal">i</mi><mi mathvariant="normal">r</mi></mrow><mtext> </mtext><mn>0</mn><mtext> </mtext><mn>0</mn></mrow><annotation encoding="application/x-tex">\mathrm{pair}\;0\;0</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8623em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathrm">pair</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord">0</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord">0</span></span></span></span> 则是初值 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo><</mo><mn>0</mn><mo separator="true">,</mo><mn>0</mn><mo>></mo></mrow><annotation encoding="application/x-tex"><0,0></annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.5782em;vertical-align:-0.0391em;"></span><span class="mrel"><</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8389em;vertical-align:-0.1944em;"></span><span class="mord">0</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord">0</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">></span></span></span></span> 。<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>n</mi></mrow><annotation encoding="application/x-tex">n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">n</span></span></span></span> 作用于蓝色函数得到它的连续 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>n</mi></mrow><annotation encoding="application/x-tex">n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">n</span></span></span></span> 次复合,再作用于初值,最后用 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">f</mi><mi mathvariant="normal">i</mi></mrow><annotation encoding="application/x-tex">\mathrm{fi}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord"><span class="mord mathrm">fi</span></span></span></span></span> 取第一个分量。</p><p>减法就是若干次前驱:</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mo>−</mo><mo>≜</mo><mi>λ</mi><mi>a</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>b</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>f</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mi>b</mi><mtext> </mtext><mrow><mi mathvariant="normal">p</mi><mi mathvariant="normal">r</mi><mi mathvariant="normal">e</mi><mi mathvariant="normal">d</mi></mrow><mtext> </mtext><mi>a</mi><mtext> </mtext><mi>f</mi><mtext> </mtext><mi>x</mi></mrow><annotation encoding="application/x-tex">-\triangleq\lambda a.\lambda b.\lambda f.\lambda x.b\;\mathrm{pred}\;a\;f\;x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.0833em;"></span><span class="mord">−</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal">λa</span><span class="mord">.</span><span class="mord mathnormal">λb</span><span class="mord">.</span><span class="mord mathnormal">λ</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mord">.</span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord mathnormal">b</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord"><span class="mord mathrm">pred</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span></span></span></span></span></p><blockquote><p>试着归约 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo>−</mo><mtext> </mtext><mn>1</mn><mtext> </mtext><mn>2</mn></mrow><annotation encoding="application/x-tex">-\;1\;2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.7278em;vertical-align:-0.0833em;"></span><span class="mord">−</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord">1</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord">2</span></span></span></span> 这样的表达式,会发现结果是 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>0</mn></mrow><annotation encoding="application/x-tex">0</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6444em;"></span><span class="mord">0</span></span></span></span> ——这意味着我们定义的减法结果不会是“负数”,<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi><mo><</mo><mi>b</mi></mrow><annotation encoding="application/x-tex">a<b</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.5782em;vertical-align:-0.0391em;"></span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel"><</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">b</span></span></span></span> 时最多得到 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>0</mn></mrow><annotation encoding="application/x-tex">0</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6444em;"></span><span class="mord">0</span></span></span></span> 。</p></blockquote><h3 id="遗留问题:除法">遗留问题:除法</h3><p>眼看快要完成,只剩除法没有定义了!减法依赖前驱,那除法依赖什么呢?学过计算机组成原理的同学可以回忆 ALU 中的除法器,最简单的思路是<strong>循环试减</strong>:用被除数不断循环减去除数,直至不能再减。循环的次数就是商、剩下的是余数。</p><p>问题来了:λ 演算还没有循环,这是个大麻烦。</p><ul><li>直接实现循环需要记录状态,而 λ 演算做不到,我们得转换成函数复合的形式,但现在又没有办法将任意的循环转换成复合。</li><li>循环还需要判断何时退出,但现在也没有办法作条件判断。</li></ul><p>接下来,我们去实现分支结构和“循环”结构。</p><blockquote><p>Dijkstra 提出结构化程序设计时,他证明一种编程语言只需要顺序、分支、循环三种结构就是图灵完备的。</p></blockquote><h2 id="逻辑判断与分支">逻辑判断与分支</h2><p>进行逻辑判断首先要有 true 和 false,在 λ 演算中可以人为规定一下:</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mtable rowspacing="0.25em" columnalign="right left" columnspacing="0em"><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mi mathvariant="normal">t</mi><mi mathvariant="normal">r</mi><mi mathvariant="normal">u</mi><mi mathvariant="normal">e</mi></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mo>≜</mo><mi>λ</mi><mi>a</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>b</mi><mi mathvariant="normal">.</mi><mi>a</mi></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mi mathvariant="normal">f</mi><mi mathvariant="normal">a</mi><mi mathvariant="normal">l</mi><mi mathvariant="normal">s</mi><mi mathvariant="normal">e</mi></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mo>≜</mo><mi>λ</mi><mi>a</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>b</mi><mi mathvariant="normal">.</mi><mi>b</mi></mrow></mstyle></mtd></mtr></mtable><annotation encoding="application/x-tex">\begin{aligned}\mathrm{true}&\triangleq\lambda a.\lambda b.a \\\mathrm{false}&\triangleq\lambda a.\lambda b.b\end{aligned}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:3.1533em;vertical-align:-1.3267em;"></span><span class="mord"><span class="mtable"><span class="col-align-r"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.8267em;"><span style="top:-3.91em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"><span class="mord mathrm">true</span></span></span></span><span style="top:-2.3333em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"><span class="mord mathrm">false</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:1.3267em;"><span></span></span></span></span></span><span class="col-align-l"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.8267em;"><span style="top:-3.91em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">λa</span><span class="mord">.</span><span class="mord mathnormal">λb</span><span class="mord">.</span><span class="mord mathnormal">a</span></span></span><span style="top:-2.3333em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">λa</span><span class="mord">.</span><span class="mord mathnormal">λb</span><span class="mord">.</span><span class="mord mathnormal">b</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:1.3267em;"><span></span></span></span></span></span></span></span></span></span></span></span></p><p>这里把 true 和 false 的定义交换一下其实也没什么影响,有意义的是它们的结构。将 true 视为含有两个绑定变量的函数,依次代入两个变量的结果是前一个,即 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi mathvariant="normal">t</mi><mi mathvariant="normal">r</mi><mi mathvariant="normal">u</mi><mi mathvariant="normal">e</mi></mrow><mtext> </mtext><mi>x</mi><mtext> </mtext><mi>y</mi></mrow><annotation encoding="application/x-tex">\mathrm{true}\;x\;y</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8095em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathrm">true</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span></span></span></span> 等价于 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>x</mi></mrow><annotation encoding="application/x-tex">x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">x</span></span></span></span> ,反之 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi mathvariant="normal">f</mi><mi mathvariant="normal">a</mi><mi mathvariant="normal">l</mi><mi mathvariant="normal">s</mi><mi mathvariant="normal">e</mi></mrow><mtext> </mtext><mi>x</mi><mtext> </mtext><mi>y</mi></mrow><annotation encoding="application/x-tex">\mathrm{false}\;x\;y</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathrm">false</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span></span></span></span> 等价于 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>y</mi></mrow><annotation encoding="application/x-tex">y</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.625em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span></span></span></span> ,这就对应“真”和“假”的两种结果,自然形成了条件分支。</p><p>在上述的定义下,假如一个 λ 表达式 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>E</mi></mrow><annotation encoding="application/x-tex">E</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.05764em;">E</span></span></span></span> 可以被归约为 true 或 false ,那 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>E</mi><mtext> </mtext><mi>x</mi><mtext> </mtext><mi>y</mi></mrow><annotation encoding="application/x-tex">E\;x\;y</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8778em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.05764em;">E</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span></span></span></span> 中的 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>x</mi></mrow><annotation encoding="application/x-tex">x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">x</span></span></span></span> 可以视作条件成立的分支 (if case)、<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>y</mi></mrow><annotation encoding="application/x-tex">y</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.625em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span></span></span></span> 可以视作条件不成立的分支 (else case) 。用分支的观点看待 true 和 false 有助于构造与、或、非这些逻辑运算,例如两个条件 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi></mrow><annotation encoding="application/x-tex">a</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">a</span></span></span></span> 和 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>b</mi></mrow><annotation encoding="application/x-tex">b</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">b</span></span></span></span> 作与运算时先考察 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi></mrow><annotation encoding="application/x-tex">a</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">a</span></span></span></span> ,若 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi></mrow><annotation encoding="application/x-tex">a</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">a</span></span></span></span> 为真则结果为 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>b</mi></mrow><annotation encoding="application/x-tex">b</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">b</span></span></span></span> 、<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi></mrow><annotation encoding="application/x-tex">a</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">a</span></span></span></span> 为假时结果为 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi></mrow><annotation encoding="application/x-tex">a</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">a</span></span></span></span> 。</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mtable rowspacing="0.25em" columnalign="right left" columnspacing="0em"><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><mo lspace="0em" rspace="0em">∧</mo></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mo>≜</mo><mi>λ</mi><mi>a</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>b</mi><mi mathvariant="normal">.</mi><mi>a</mi><mtext> </mtext><mi>b</mi><mtext> </mtext><mi>a</mi></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><mo lspace="0em" rspace="0em">∨</mo></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mo>≜</mo><mi>λ</mi><mi>a</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>b</mi><mi mathvariant="normal">.</mi><mi>a</mi><mtext> </mtext><mi>a</mi><mtext> </mtext><mi>b</mi></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><mi mathvariant="normal">¬</mi></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mo>≜</mo><mi>λ</mi><mi>a</mi><mi mathvariant="normal">.</mi><mi>a</mi><mtext> </mtext><mrow><mi mathvariant="normal">f</mi><mi mathvariant="normal">a</mi><mi mathvariant="normal">l</mi><mi mathvariant="normal">s</mi><mi mathvariant="normal">e</mi></mrow><mtext> </mtext><mrow><mi mathvariant="normal">t</mi><mi mathvariant="normal">r</mi><mi mathvariant="normal">u</mi><mi mathvariant="normal">e</mi></mrow></mrow></mstyle></mtd></mtr></mtable><annotation encoding="application/x-tex">\begin{aligned}\land&\triangleq\lambda a.\lambda b.a\;b\;a \\\lor&\triangleq\lambda a.\lambda b.a\;a\;b \\\lnot&\triangleq\lambda a.a\;\mathrm{false}\;\mathrm{true}\end{aligned}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:4.73em;vertical-align:-2.115em;"></span><span class="mord"><span class="mtable"><span class="col-align-r"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:2.615em;"><span style="top:-4.6983em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord">∧</span></span></span><span style="top:-3.1217em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord">∨</span></span></span><span style="top:-1.545em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord">¬</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:2.115em;"><span></span></span></span></span></span><span class="col-align-l"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:2.615em;"><span style="top:-4.6983em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">λa</span><span class="mord">.</span><span class="mord mathnormal">λb</span><span class="mord">.</span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">b</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">a</span></span></span><span style="top:-3.1217em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">λa</span><span class="mord">.</span><span class="mord mathnormal">λb</span><span class="mord">.</span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">b</span></span></span><span style="top:-1.545em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">λa</span><span class="mord">.</span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord"><span class="mord mathrm">false</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord"><span class="mord mathrm">true</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:2.115em;"><span></span></span></span></span></span></span></span></span></span></span></span></p><p>先试试判零:无论代入什么到 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mrow><mi mathvariant="normal">f</mi><mi mathvariant="normal">a</mi><mi mathvariant="normal">l</mi><mi mathvariant="normal">s</mi><mi mathvariant="normal">e</mi></mrow></mrow><annotation encoding="application/x-tex">\lambda x.\mathrm{false}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord"><span class="mord mathrm">false</span></span></span></span></span> 这个函数中,得到的都是 false 。将 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mrow><mi mathvariant="normal">f</mi><mi mathvariant="normal">a</mi><mi mathvariant="normal">l</mi><mi mathvariant="normal">s</mi><mi mathvariant="normal">e</mi></mrow></mrow><annotation encoding="application/x-tex">\lambda x.\mathrm{false}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord"><span class="mord mathrm">false</span></span></span></span></span> 再代入到 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>n</mi></mrow><annotation encoding="application/x-tex">n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">n</span></span></span></span> 中,</p><ul><li>如果 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>n</mi></mrow><annotation encoding="application/x-tex">n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">n</span></span></span></span> 不是零,那得到的函数还是 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mrow><mi mathvariant="normal">f</mi><mi mathvariant="normal">a</mi><mi mathvariant="normal">l</mi><mi mathvariant="normal">s</mi><mi mathvariant="normal">e</mi></mrow></mrow><annotation encoding="application/x-tex">\lambda x.\mathrm{false}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord"><span class="mord mathrm">false</span></span></span></span></span> ,最后作用于什么都会是 false</li><li>只有 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>n</mi></mrow><annotation encoding="application/x-tex">n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">n</span></span></span></span> 为零时这个函数不起作用,最后代入 true 就可以得到 true</li></ul><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mrow><mi mathvariant="normal">z</mi><mi mathvariant="normal">e</mi><mi mathvariant="normal">r</mi><mi mathvariant="normal">o</mi></mrow><mo>≜</mo><mi>λ</mi><mi>n</mi><mi mathvariant="normal">.</mi><mi>n</mi><mtext> </mtext><mo stretchy="false">(</mo><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mrow><mi mathvariant="normal">f</mi><mi mathvariant="normal">a</mi><mi mathvariant="normal">l</mi><mi mathvariant="normal">s</mi><mi mathvariant="normal">e</mi></mrow><mo stretchy="false">)</mo><mtext> </mtext><mrow><mi mathvariant="normal">t</mi><mi mathvariant="normal">r</mi><mi mathvariant="normal">u</mi><mi mathvariant="normal">e</mi></mrow></mrow><annotation encoding="application/x-tex">\mathrm{zero}\triangleq\lambda n.n\;(\lambda x.\mathrm{false})\;\mathrm{true}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.9987em;vertical-align:-0.082em;"></span><span class="mord"><span class="mord mathrm">zero</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal">λn</span><span class="mord">.</span><span class="mord mathnormal">n</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord"><span class="mord mathrm">false</span></span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord"><span class="mord mathrm">true</span></span></span></span></span></span></p><p>借助“减法不会得到负数”这一特点,还能构造出判断大小的函数:</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mtable rowspacing="0.25em" columnalign="right left" columnspacing="0em"><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><mo lspace="0em" rspace="0em">≤</mo></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mo>≜</mo><mi>λ</mi><mi>a</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>b</mi><mi mathvariant="normal">.</mi><mrow><mi mathvariant="normal">z</mi><mi mathvariant="normal">e</mi><mi mathvariant="normal">r</mi><mi mathvariant="normal">o</mi></mrow><mtext> </mtext><mo stretchy="false">(</mo><mo>−</mo><mtext> </mtext><mi>a</mi><mtext> </mtext><mi>b</mi><mo stretchy="false">)</mo></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><mo lspace="0em" rspace="0em">≥</mo></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mo>≜</mo><mi>λ</mi><mi>a</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>b</mi><mi mathvariant="normal">.</mi><mrow><mi mathvariant="normal">z</mi><mi mathvariant="normal">e</mi><mi mathvariant="normal">r</mi><mi mathvariant="normal">o</mi></mrow><mtext> </mtext><mo stretchy="false">(</mo><mo>−</mo><mtext> </mtext><mi>b</mi><mtext> </mtext><mi>a</mi><mo stretchy="false">)</mo></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><mo lspace="0em" rspace="0em">=</mo></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mo>≜</mo><mi>λ</mi><mi>a</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>b</mi><mi mathvariant="normal">.</mi><mo>∧</mo><mtext> </mtext><mo stretchy="false">(</mo><mo>≤</mo><mtext> </mtext><mi>a</mi><mtext> </mtext><mi>b</mi><mo stretchy="false">)</mo><mtext> </mtext><mo stretchy="false">(</mo><mo>≥</mo><mtext> </mtext><mi>a</mi><mtext> </mtext><mi>b</mi><mo stretchy="false">)</mo></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><mo mathvariant="normal" lspace="0em" rspace="0em">≠</mo></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mo>≜</mo><mi>λ</mi><mi>a</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>b</mi><mi mathvariant="normal">.</mi><mi mathvariant="normal">¬</mi><mtext> </mtext><mo stretchy="false">(</mo><mo>=</mo><mtext> </mtext><mi>a</mi><mtext> </mtext><mi>b</mi><mo stretchy="false">)</mo></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><mo lspace="0em" rspace="0em"><</mo></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mo>≜</mo><mi>λ</mi><mi>a</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>b</mi><mi mathvariant="normal">.</mi><mo>∧</mo><mtext> </mtext><mo stretchy="false">(</mo><mo>≤</mo><mtext> </mtext><mi>a</mi><mtext> </mtext><mi>b</mi><mo stretchy="false">)</mo><mtext> </mtext><mo stretchy="false">(</mo><mo mathvariant="normal">≠</mo><mtext> </mtext><mi>a</mi><mtext> </mtext><mi>b</mi><mo stretchy="false">)</mo></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><mo lspace="0em" rspace="0em">></mo></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mo>≜</mo><mi>λ</mi><mi>a</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>b</mi><mi mathvariant="normal">.</mi><mo>∧</mo><mtext> </mtext><mo stretchy="false">(</mo><mo>≥</mo><mtext> </mtext><mi>a</mi><mtext> </mtext><mi>b</mi><mo stretchy="false">)</mo><mtext> </mtext><mo stretchy="false">(</mo><mo mathvariant="normal">≠</mo><mtext> </mtext><mi>a</mi><mtext> </mtext><mi>b</mi><mo stretchy="false">)</mo></mrow></mstyle></mtd></mtr></mtable><annotation encoding="application/x-tex">\begin{aligned}\leq&\triangleq\lambda a.\lambda b.\mathrm{zero}\;(-\;a\;b) \\\geq&\triangleq\lambda a.\lambda b.\mathrm{zero}\;(-\;b\;a) \\=&\triangleq\lambda a.\lambda b.\land\;(\leq\;a\;b)\;(\geq\;a\;b)\\\neq&\triangleq\lambda a.\lambda b.\lnot\;(=\;a\;b)\\<&\triangleq\lambda a.\lambda b.\land\;(\leq\;a\;b)\;(\neq\;a\;b) \\>&\triangleq\lambda a.\lambda b.\land\;(\geq\;a\;b)\;(\neq\;a\;b)\end{aligned}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:9.46em;vertical-align:-4.48em;"></span><span class="mord"><span class="mtable"><span class="col-align-r"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:4.98em;"><span style="top:-7.0633em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mrel">≤</span></span></span><span style="top:-5.4867em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mrel">≥</span></span></span><span style="top:-3.91em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mrel">=</span></span></span><span style="top:-2.3333em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mrel"><span class="mrel"><span class="mord vbox"><span class="thinbox"><span class="rlap"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="inner"><span class="mord"><span class="mrel"></span></span></span><span class="fix"></span></span></span></span></span><span class="mrel">=</span></span></span></span><span style="top:-0.7567em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mrel"><</span></span></span><span style="top:0.82em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mrel">></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:4.48em;"><span></span></span></span></span></span><span class="col-align-l"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:4.98em;"><span style="top:-7.0633em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">λa</span><span class="mord">.</span><span class="mord mathnormal">λb</span><span class="mord">.</span><span class="mord"><span class="mord mathrm">zero</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mord">−</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">b</span><span class="mclose">)</span></span></span><span style="top:-5.4867em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">λa</span><span class="mord">.</span><span class="mord mathnormal">λb</span><span class="mord">.</span><span class="mord"><span class="mord mathrm">zero</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mord">−</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">b</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">a</span><span class="mclose">)</span></span></span><span style="top:-3.91em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">λa</span><span class="mord">.</span><span class="mord mathnormal">λb</span><span class="mord">.</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">∧</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mopen">(</span><span class="mrel">≤</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">b</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mrel">≥</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">b</span><span class="mclose">)</span></span></span><span style="top:-2.3333em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">λa</span><span class="mord">.</span><span class="mord mathnormal">λb</span><span class="mord">.¬</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">b</span><span class="mclose">)</span></span></span><span style="top:-0.7567em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">λa</span><span class="mord">.</span><span class="mord mathnormal">λb</span><span class="mord">.</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">∧</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mopen">(</span><span class="mrel">≤</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">b</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mrel"><span class="mrel"><span class="mord vbox"><span class="thinbox"><span class="rlap"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="inner"><span class="mord"><span class="mrel"></span></span></span><span class="fix"></span></span></span></span></span><span class="mrel">=</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">b</span><span class="mclose">)</span></span></span><span style="top:0.82em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">λa</span><span class="mord">.</span><span class="mord mathnormal">λb</span><span class="mord">.</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">∧</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mopen">(</span><span class="mrel">≥</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">b</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mrel"><span class="mrel"><span class="mord vbox"><span class="thinbox"><span class="rlap"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="inner"><span class="mord"><span class="mrel"></span></span></span><span class="fix"></span></span></span></span></span><span class="mrel">=</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">b</span><span class="mclose">)</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:4.48em;"><span></span></span></span></span></span></span></span></span></span></span></span></p><p>有了运算、比较和分支,接下来就可以准备“循环”了。</p><h2 id="“循环”?递归!">“循环”?递归!</h2><p>无论哪种编程语言,想要实现循环都需要用一个变量控制,而这对 λ 演算而言是不现实的。所以 λ 演算里不用循环,而是用递归。例如求 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>1</mn></mrow><annotation encoding="application/x-tex">1</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6444em;"></span><span class="mord">1</span></span></span></span> 到 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>n</mi></mrow><annotation encoding="application/x-tex">n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">n</span></span></span></span> 的和,可以表示为一个递归函数:</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mrow><mi mathvariant="normal">s</mi><mi mathvariant="normal">u</mi><mi mathvariant="normal">m</mi></mrow><mo stretchy="false">(</mo><mi>n</mi><mo stretchy="false">)</mo><mo>=</mo><mrow><mo fence="true">{</mo><mtable rowspacing="0.36em" columnalign="left left" columnspacing="1em"><mtr><mtd><mstyle scriptlevel="0" displaystyle="false"><mn>0</mn></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><mrow><mtext>if </mtext><mi>n</mi><mo>=</mo><mn>0</mn></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="false"><mrow><mi>n</mi><mo>+</mo><mrow><mi mathvariant="normal">s</mi><mi mathvariant="normal">u</mi><mi mathvariant="normal">m</mi></mrow><mo stretchy="false">(</mo><mi>n</mi><mo>−</mo><mn>1</mn><mo stretchy="false">)</mo></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><mtext>else</mtext></mstyle></mtd></mtr></mtable></mrow></mrow><annotation encoding="application/x-tex">\mathrm{sum}(n)=\begin{cases}0 &\text{if }n=0 \\n+\mathrm{sum}(n-1) &\text{else}\end{cases}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathrm">sum</span></span><span class="mopen">(</span><span class="mord mathnormal">n</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:3em;vertical-align:-1.25em;"></span><span class="minner"><span class="mopen delimcenter" style="top:0em;"><span class="delimsizing size4">{</span></span><span class="mord"><span class="mtable"><span class="col-align-l"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.69em;"><span style="top:-3.69em;"><span class="pstrut" style="height:3.008em;"></span><span class="mord"><span class="mord">0</span></span></span><span style="top:-2.25em;"><span class="pstrut" style="height:3.008em;"></span><span class="mord"><span class="mord mathnormal">n</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord"><span class="mord mathrm">sum</span></span><span class="mopen">(</span><span class="mord mathnormal">n</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord">1</span><span class="mclose">)</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:1.19em;"><span></span></span></span></span></span><span class="arraycolsep" style="width:1em;"></span><span class="col-align-l"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.69em;"><span style="top:-3.69em;"><span class="pstrut" style="height:3.008em;"></span><span class="mord"><span class="mord text"><span class="mord">if </span></span><span class="mord mathnormal">n</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord">0</span></span></span><span style="top:-2.25em;"><span class="pstrut" style="height:3.008em;"></span><span class="mord"><span class="mord text"><span class="mord">else</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:1.19em;"><span></span></span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></p><p>这样的定义似乎很容易转写成 λ 表达式:</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mrow><mi mathvariant="normal">s</mi><mi mathvariant="normal">u</mi><mi mathvariant="normal">m</mi></mrow><mo>≜</mo><mi>λ</mi><mi>n</mi><mi mathvariant="normal">.</mi><mo stretchy="false">(</mo><mrow><mi mathvariant="normal">z</mi><mi mathvariant="normal">e</mi><mi mathvariant="normal">r</mi><mi mathvariant="normal">o</mi></mrow><mtext> </mtext><mi>n</mi><mo stretchy="false">)</mo><mtext> </mtext><mn>0</mn><mtext> </mtext><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">(</mo><mo>+</mo><mtext> </mtext><mi>n</mi><mtext> </mtext><mo stretchy="false">(</mo><mrow><mi mathvariant="normal">s</mi><mi mathvariant="normal">u</mi><mi mathvariant="normal">m</mi></mrow><mtext> </mtext><mo stretchy="false">(</mo><mrow><mi mathvariant="normal">p</mi><mi mathvariant="normal">r</mi><mi mathvariant="normal">e</mi><mi mathvariant="normal">d</mi></mrow><mtext> </mtext><mi>n</mi><mo stretchy="false">)</mo><mo stretchy="false">)</mo><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">)</mo></mrow><annotation encoding="application/x-tex">\mathrm{sum}\triangleq\lambda n.(\mathrm{zero}\;n)\;0\;\big(+\;n\;(\mathrm{sum}\;(\mathrm{pred}\;n))\big)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.9987em;vertical-align:-0.082em;"></span><span class="mord"><span class="mord mathrm">sum</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1.2em;vertical-align:-0.35em;"></span><span class="mord mathnormal">λn</span><span class="mord">.</span><span class="mopen">(</span><span class="mord"><span class="mord mathrm">zero</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">n</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord">0</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord"><span class="delimsizing size1">(</span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1.2em;vertical-align:-0.35em;"></span><span class="mord mathnormal">n</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mord"><span class="mord mathrm">sum</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mord"><span class="mord mathrm">pred</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">n</span><span class="mclose">))</span><span class="mord"><span class="delimsizing size1">)</span></span></span></span></span></span></p><p>然而,λ 表达式中只存在两种东西:纯函数 (pure function) 和函数作用 (application),而纯函数的定义中不能包含自身,这意味着上面这种递归定义不是一个正确的 λ 表达式。</p><blockquote><p>换个角度理解,所有的 λ 表达式(函数)都是<strong>匿名函数</strong>,在定义中引用自己的名字实际上意味着<strong>在定义中引用自己的定义</strong>。过程式编程中的函数只是某段代码的别称,并不需要一个数学上的严格定义,也就不担心自引用会带来什么问题。然而 λ 演算的函数是严格定义的,并不能这样递归。</p></blockquote><h3 id="用纯函数实现递归">用纯函数实现递归</h3><p>纯函数的定义不能引用自身,那就意味着我们需要想办法在不直接引用自身的情况下实现递归。假如有一个递归函数 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>f</mi><mi>r</mi></msub></mrow><annotation encoding="application/x-tex">f_r</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em;"><span style="top:-2.55em;margin-left:-0.1076em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.02778em;">r</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span> ,它的定义中引用了自身,那么如果把 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>f</mi><mi>r</mi></msub></mrow><annotation encoding="application/x-tex">f_r</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em;"><span style="top:-2.55em;margin-left:-0.1076em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.02778em;">r</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span> 作为一个绑定变量(参数)代入某个函数 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>g</mi></mrow><annotation encoding="application/x-tex">g</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.625em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span></span></span></span> ,让 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>g</mi></mrow><annotation encoding="application/x-tex">g</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.625em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span></span></span></span> 的代入结果是一个功能与 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>f</mi><mi>r</mi></msub></mrow><annotation encoding="application/x-tex">f_r</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em;"><span style="top:-2.55em;margin-left:-0.1076em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.02778em;">r</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span> 一样的函数,不就能避开“直接引用”了吗?</p><p>例如用求和函数 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">s</mi><mi mathvariant="normal">u</mi><mi mathvariant="normal">m</mi></mrow><annotation encoding="application/x-tex">\mathrm{sum}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord"><span class="mord mathrm">sum</span></span></span></span></span> 作为 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>f</mi><mi>r</mi></msub></mrow><annotation encoding="application/x-tex">f_r</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em;"><span style="top:-2.55em;margin-left:-0.1076em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.02778em;">r</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span> ,下面这个 λ 表达式显然也表达了求和的含义,并且它是合法的:</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>g</mi><mo>≜</mo><mi>λ</mi><mi>f</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>n</mi><mi mathvariant="normal">.</mi><mo stretchy="false">(</mo><mrow><mi mathvariant="normal">z</mi><mi mathvariant="normal">e</mi><mi mathvariant="normal">r</mi><mi mathvariant="normal">o</mi></mrow><mtext> </mtext><mi>n</mi><mo stretchy="false">)</mo><mtext> </mtext><mn>0</mn><mtext> </mtext><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">(</mo><mo>+</mo><mtext> </mtext><mi>n</mi><mtext> </mtext><mo stretchy="false">(</mo><mi>f</mi><mtext> </mtext><mo stretchy="false">(</mo><mrow><mi mathvariant="normal">p</mi><mi mathvariant="normal">r</mi><mi mathvariant="normal">e</mi><mi mathvariant="normal">d</mi></mrow><mtext> </mtext><mi>n</mi><mo stretchy="false">)</mo><mo stretchy="false">)</mo><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">)</mo></mrow><annotation encoding="application/x-tex">g\triangleq\lambda f.\lambda n.(\mathrm{zero}\;n)\;0\;\big(+\;n\;(f\;(\mathrm{pred}\;n))\big)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.1111em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1.2em;vertical-align:-0.35em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mord">.</span><span class="mord mathnormal">λn</span><span class="mord">.</span><span class="mopen">(</span><span class="mord"><span class="mord mathrm">zero</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">n</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord">0</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord"><span class="delimsizing size1">(</span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1.2em;vertical-align:-0.35em;"></span><span class="mord mathnormal">n</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mord"><span class="mord mathrm">pred</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">n</span><span class="mclose">))</span><span class="mord"><span class="delimsizing size1">)</span></span></span></span></span></span></p><p><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>g</mi></mrow><annotation encoding="application/x-tex">g</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.625em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span></span></span></span> 这个表达式中缺了一个参数 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi></mrow><annotation encoding="application/x-tex">f</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span></span></span></span> ,需要代入些什么才能继续归约。由于 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>f</mi><mi>r</mi></msub></mrow><annotation encoding="application/x-tex">f_r</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em;"><span style="top:-2.55em;margin-left:-0.1076em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.02778em;">r</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span> 不是合法的 λ 表达式,我们不能直接把它代入 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>g</mi></mrow><annotation encoding="application/x-tex">g</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.625em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span></span></span></span> 中。但只要将某个<strong>功能与 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>f</mi><mi>r</mi></msub></mrow><annotation encoding="application/x-tex">f_r</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em;"><span style="top:-2.55em;margin-left:-0.1076em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.02778em;">r</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span> 一样的函数</strong>代入 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>g</mi></mrow><annotation encoding="application/x-tex">g</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.625em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span></span></span></span> 中,就能得到一个定义合法的求和函数。姑且将这个函数称为 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi></mrow><annotation encoding="application/x-tex">f</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span></span></span></span> ,它</p><ol><li>首先功能与 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>f</mi><mi>r</mi></msub></mrow><annotation encoding="application/x-tex">f_r</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em;"><span style="top:-2.55em;margin-left:-0.1076em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.02778em;">r</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span> 一样,所以是一个求和函数</li><li>代入 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>g</mi></mrow><annotation encoding="application/x-tex">g</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.625em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span></span></span></span> 之后应该得到一个功能与 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>f</mi><mi>r</mi></msub></mrow><annotation encoding="application/x-tex">f_r</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em;"><span style="top:-2.55em;margin-left:-0.1076em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.02778em;">r</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span> 一样的函数——这不是它自己吗?</li></ol><p>第二个条件正是所谓<strong>不动点方程</strong>:</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>f</mi><mo>=</mo><mi>g</mi><mo stretchy="false">(</mo><mi>f</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">f=g(f)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mclose">)</span></span></span></span></span></p><p>函数 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi></mrow><annotation encoding="application/x-tex">f</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span></span></span></span> 就称为 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>g</mi></mrow><annotation encoding="application/x-tex">g</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.625em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span></span></span></span> 的<strong>不动点</strong>。如果而我们能求出这个不动点(函数),将其代入 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>g</mi></mrow><annotation encoding="application/x-tex">g</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.625em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span></span></span></span> ,问题立刻解决。把不动点方程的左边反复代入右边:</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>f</mi><mo>=</mo><mi>g</mi><mo stretchy="false">(</mo><mi>f</mi><mo stretchy="false">)</mo><mo>=</mo><mi>g</mi><mo stretchy="false">(</mo><mi>g</mi><mo stretchy="false">(</mo><mi>f</mi><mo stretchy="false">)</mo><mo stretchy="false">)</mo><mo>=</mo><mi>g</mi><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">(</mo><mi>g</mi><mo stretchy="false">(</mo><mi>g</mi><mo>⋯</mo><mtext> </mtext><mo stretchy="false">)</mo><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">)</mo></mrow><annotation encoding="application/x-tex">f=g(f)=g(g(f))=g\big(g(g\cdots)\big)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mclose">))</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1.2em;vertical-align:-0.35em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mord"><span class="delimsizing size1">(</span></span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="minner">⋯</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mclose">)</span><span class="mord"><span class="delimsizing size1">)</span></span></span></span></span></span></p><p>得到的 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>g</mi><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">(</mo><mi>g</mi><mo stretchy="false">(</mo><mi>g</mi><mo>⋯</mo><mtext> </mtext><mo stretchy="false">)</mo><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">)</mo></mrow><annotation encoding="application/x-tex">g\big(g(g\cdots)\big)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.2em;vertical-align:-0.35em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mord"><span class="delimsizing size1">(</span></span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="minner">⋯</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mclose">)</span><span class="mord"><span class="delimsizing size1">)</span></span></span></span></span> 是一个无限长的表达式,那它的“开方结果”应该也是个无限长的表达式:</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>f</mi><mo>=</mo><mi>g</mi><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">(</mo><mi>g</mi><mo stretchy="false">(</mo><mi>g</mi><mo>⋯</mo><mtext> </mtext><mo stretchy="false">)</mo><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">)</mo><mo>=</mo><mstyle mathcolor="royalblue"><mi>G</mi><mo stretchy="false">(</mo><mi>G</mi><mo stretchy="false">)</mo><mo>=</mo><mi>g</mi><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">(</mo><mi>G</mi><mo stretchy="false">(</mo><mi>G</mi><mo stretchy="false">)</mo><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">)</mo></mstyle></mrow><annotation encoding="application/x-tex">f=g\big(g(g\cdots)\big)=\textcolor{royalblue}{G(G)=g\big(G(G)\big)}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1.2em;vertical-align:-0.35em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mord"><span class="delimsizing size1">(</span></span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="minner">⋯</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mclose">)</span><span class="mord"><span class="delimsizing size1">)</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="color:royalblue;">G</span><span class="mopen" style="color:royalblue;">(</span><span class="mord mathnormal" style="color:royalblue;">G</span><span class="mclose" style="color:royalblue;">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel" style="color:royalblue;">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1.2em;vertical-align:-0.35em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;color:royalblue;">g</span><span class="mord" style="color:royalblue;"><span class="delimsizing size1" style="color:royalblue;"><span style="color:royalblue;">(</span></span></span><span class="mord mathnormal" style="color:royalblue;">G</span><span class="mopen" style="color:royalblue;">(</span><span class="mord mathnormal" style="color:royalblue;">G</span><span class="mclose" style="color:royalblue;">)</span><span class="mord" style="color:royalblue;"><span class="delimsizing size1" style="color:royalblue;"><span style="color:royalblue;">)</span></span></span></span></span></span></span></p><p>蓝色的部分当然是个方程,但也可以理解为一个函数的定义:<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>G</mi></mrow><annotation encoding="application/x-tex">G</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal">G</span></span></span></span> 是一个将任意函数 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>G</mi></mrow><annotation encoding="application/x-tex">G</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal">G</span></span></span></span> 映射为 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>g</mi><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">(</mo><mi>G</mi><mo stretchy="false">(</mo><mi>G</mi><mo stretchy="false">)</mo><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">)</mo></mrow><annotation encoding="application/x-tex">g\big(G(G)\big)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.2em;vertical-align:-0.35em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mord"><span class="delimsizing size1">(</span></span><span class="mord mathnormal">G</span><span class="mopen">(</span><span class="mord mathnormal">G</span><span class="mclose">)</span><span class="mord"><span class="delimsizing size1">)</span></span></span></span></span> 的函数,这里第一个 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>G</mi></mrow><annotation encoding="application/x-tex">G</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal">G</span></span></span></span> 是定义的函数名称,第二个 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>G</mi></mrow><annotation encoding="application/x-tex">G</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal">G</span></span></span></span> 则是任意的参数。</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mtable rowspacing="0.25em" columnalign="right left" columnspacing="0em"><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mi>G</mi><mo>≜</mo><mi>λ</mi><mi>G</mi><mi mathvariant="normal">.</mi><mi>g</mi><mtext> </mtext><mo stretchy="false">(</mo><mi>G</mi><mtext> </mtext><mi>G</mi><mo stretchy="false">)</mo></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow><mstyle scriptlevel="0" displaystyle="false"><mi>α</mi></mstyle><mtext>-equivlence</mtext></mrow><mtext> </mtext><mo>⟹</mo><mtext> </mtext></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mi>G</mi><mo>≜</mo><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mi>g</mi><mtext> </mtext><mo stretchy="false">(</mo><mi>x</mi><mtext> </mtext><mi>x</mi><mo stretchy="false">)</mo></mrow></mstyle></mtd></mtr></mtable><annotation encoding="application/x-tex">\begin{aligned}&G\triangleq\lambda G.g\;(G\;G) \\\text{$\alpha$-equivlence}\implies &G\triangleq\lambda x.g\;(x\;x)\end{aligned}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:3.1533em;vertical-align:-1.3267em;"></span><span class="mord"><span class="mtable"><span class="col-align-r"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.8267em;"><span style="top:-3.91em;"><span class="pstrut" style="height:3em;"></span><span class="mord"></span></span><span style="top:-2.3333em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord text"><span class="mord mathnormal" style="margin-right:0.0037em;">α</span><span class="mord">-equivlence</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">⟹</span><span class="mspace" style="margin-right:0.2778em;"></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:1.3267em;"><span></span></span></span></span></span><span class="col-align-l"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.8267em;"><span style="top:-3.91em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"></span><span class="mord mathnormal">G</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal">G</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mord mathnormal">G</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">G</span><span class="mclose">)</span></span></span><span style="top:-2.3333em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"></span><span class="mord mathnormal">G</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span><span class="mclose">)</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:1.3267em;"><span></span></span></span></span></span></span></span></span></span></span></span></p><p>把这个“开方”结果代回去,得到 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi></mrow><annotation encoding="application/x-tex">f</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span></span></span></span> 实际上是 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>G</mi></mrow><annotation encoding="application/x-tex">G</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal">G</span></span></span></span> 自己代入自己的结果:</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>f</mi><mo>=</mo><mi>G</mi><mtext> </mtext><mi>G</mi><mo>=</mo><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">(</mo><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mi>g</mi><mtext> </mtext><mo stretchy="false">(</mo><mi>x</mi><mtext> </mtext><mi>x</mi><mo stretchy="false">)</mo><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">)</mo><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">(</mo><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mi>g</mi><mtext> </mtext><mo stretchy="false">(</mo><mi>x</mi><mtext> </mtext><mi>x</mi><mo stretchy="false">)</mo><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">)</mo></mrow><annotation encoding="application/x-tex">f=G\;G=\big(\lambda x.g\;(x\;x)\big)\big(\lambda x.g\;(x\;x)\big)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal">G</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">G</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1.2em;vertical-align:-0.35em;"></span><span class="mord"><span class="delimsizing size1">(</span></span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span><span class="mclose">)</span><span class="mord"><span class="delimsizing size1">)</span></span><span class="mord"><span class="delimsizing size1">(</span></span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span><span class="mclose">)</span><span class="mord"><span class="delimsizing size1">)</span></span></span></span></span></span></p><blockquote><p>试试归约 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi></mrow><annotation encoding="application/x-tex">f</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span></span></span></span> :将 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mi>g</mi><mtext> </mtext><mo stretchy="false">(</mo><mi>x</mi><mtext> </mtext><mi>x</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">\lambda x.g\;(x\;x)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span><span class="mclose">)</span></span></span></span> (第二个表达式)作为一个整体代入 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>x</mi></mrow><annotation encoding="application/x-tex">x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">x</span></span></span></span> (第一个表达式的绑定变量)中,得到的是 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>g</mi><mtext> </mtext><mo stretchy="false">(</mo><mi>G</mi><mtext> </mtext><mi>G</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">g\;(G\;G)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mord mathnormal">G</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">G</span><span class="mclose">)</span></span></span></span> ——多套了一层 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>g</mi></mrow><annotation encoding="application/x-tex">g</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.625em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span></span></span></span> ,里面的结构还没变!这意味着 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi></mrow><annotation encoding="application/x-tex">f</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span></span></span></span> 能自己“复制”自己,生成任意长的表达式,正符合递归的要求。</p></blockquote><p>设不动点方程的一般解是 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi><mo>=</mo><mi>Y</mi><mtext> </mtext><mi>g</mi></mrow><annotation encoding="application/x-tex">f=Y\;g</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8778em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span></span></span></span> ,这个解也可以理解为对 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>Y</mi></mrow><annotation encoding="application/x-tex">Y</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span></span></span></span> 的定义:</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mtable rowspacing="0.25em" columnalign="right left" columnspacing="0em"><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mi>Y</mi><mo>≜</mo><mi>λ</mi><mi>g</mi><mi mathvariant="normal">.</mi><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">(</mo><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mi>g</mi><mtext> </mtext><mo stretchy="false">(</mo><mi>x</mi><mtext> </mtext><mi>x</mi><mo stretchy="false">)</mo><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">)</mo><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">(</mo><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mi>g</mi><mtext> </mtext><mo stretchy="false">(</mo><mi>x</mi><mtext> </mtext><mi>x</mi><mo stretchy="false">)</mo><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">)</mo></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow><mtext>inversed </mtext><mstyle scriptlevel="0" displaystyle="false"><mi>β</mi></mstyle><mtext>-reduction</mtext></mrow><mtext> </mtext><mo>⟹</mo><mtext> </mtext></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mi>Y</mi><mo>≜</mo><mi>λ</mi><mi>f</mi><mi mathvariant="normal">.</mi><mo stretchy="false">(</mo><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mi>x</mi><mtext> </mtext><mi>x</mi><mo stretchy="false">)</mo><mtext> </mtext><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">(</mo><mi>λ</mi><mi>x</mi><mi mathvariant="normal">.</mi><mi>f</mi><mtext> </mtext><mo stretchy="false">(</mo><mi>x</mi><mtext> </mtext><mi>x</mi><mo stretchy="false">)</mo><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">)</mo></mrow></mstyle></mtd></mtr></mtable><annotation encoding="application/x-tex">\begin{aligned}&Y\triangleq\lambda g.\big(\lambda x.g\;(x\;x)\big)\big(\lambda x.g\;(x\;x)\big) \\\text{inversed $\beta$-reduction}\implies&Y\triangleq\lambda f.(\lambda x.x\;x)\;\big(\lambda x.f\;(x\;x)\big)\end{aligned}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:3.1533em;vertical-align:-1.3267em;"></span><span class="mord"><span class="mtable"><span class="col-align-r"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.8267em;"><span style="top:-3.91em;"><span class="pstrut" style="height:3em;"></span><span class="mord"></span></span><span style="top:-2.3333em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord text"><span class="mord">inversed </span><span class="mord mathnormal" style="margin-right:0.05278em;">β</span><span class="mord">-reduction</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">⟹</span><span class="mspace" style="margin-right:0.2778em;"></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:1.3267em;"><span></span></span></span></span></span><span class="col-align-l"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.8267em;"><span style="top:-3.91em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"></span><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mord">.</span><span class="mord"><span class="delimsizing size1">(</span></span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span><span class="mclose">)</span><span class="mord"><span class="delimsizing size1">)</span></span><span class="mord"><span class="delimsizing size1">(</span></span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span><span class="mclose">)</span><span class="mord"><span class="delimsizing size1">)</span></span></span></span><span style="top:-2.3333em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"></span><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mord">.</span><span class="mopen">(</span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord mathnormal">x</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord"><span class="delimsizing size1">(</span></span><span class="mord mathnormal">λ</span><span class="mord mathnormal">x</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">x</span><span class="mclose">)</span><span class="mord"><span class="delimsizing size1">)</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:1.3267em;"><span></span></span></span></span></span></span></span></span></span></span></span></p><p>这个东西称为 <strong>Y 组合子</strong> (Y combinator) ,只要将我们写好的纯函数 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>g</mi></mrow><annotation encoding="application/x-tex">g</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.625em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span></span></span></span> 代入 Y 组合子就能得到一个递归函数。回头再看 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>g</mi></mrow><annotation encoding="application/x-tex">g</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.625em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span></span></span></span> 的定义</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>g</mi><mo>≜</mo><mi>λ</mi><mi>f</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>n</mi><mi mathvariant="normal">.</mi><mo stretchy="false">(</mo><mrow><mi mathvariant="normal">z</mi><mi mathvariant="normal">e</mi><mi mathvariant="normal">r</mi><mi mathvariant="normal">o</mi></mrow><mtext> </mtext><mi>n</mi><mo stretchy="false">)</mo><mtext> </mtext><mn>0</mn><mtext> </mtext><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">(</mo><mo>+</mo><mtext> </mtext><mi>n</mi><mtext> </mtext><mo stretchy="false">(</mo><mi>f</mi><mtext> </mtext><mo stretchy="false">(</mo><mrow><mi mathvariant="normal">p</mi><mi mathvariant="normal">r</mi><mi mathvariant="normal">e</mi><mi mathvariant="normal">d</mi></mrow><mtext> </mtext><mi>n</mi><mo stretchy="false">)</mo><mo stretchy="false">)</mo><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">)</mo></mrow><annotation encoding="application/x-tex">g\triangleq\lambda f.\lambda n.(\mathrm{zero}\;n)\;0\;\big(+\;n\;(f\;(\mathrm{pred}\;n))\big)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.1111em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1.2em;vertical-align:-0.35em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mord">.</span><span class="mord mathnormal">λn</span><span class="mord">.</span><span class="mopen">(</span><span class="mord"><span class="mord mathrm">zero</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">n</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord">0</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord"><span class="delimsizing size1">(</span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1.2em;vertical-align:-0.35em;"></span><span class="mord mathnormal">n</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mord"><span class="mord mathrm">pred</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">n</span><span class="mclose">))</span><span class="mord"><span class="delimsizing size1">)</span></span></span></span></span></span></p><p>其实就是计算求和结果的过程中<strong>与递归无关的部分</strong>(返回零、进行加法),而与递归有关的部分则属于 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi></mrow><annotation encoding="application/x-tex">f</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span></span></span></span> 。一个表示递归求和的合法 λ 表达式可以用 Y 组合子定义:</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mrow><mi mathvariant="normal">s</mi><mi mathvariant="normal">u</mi><mi mathvariant="normal">m</mi></mrow><mo>≜</mo><mi>Y</mi><mtext> </mtext><mo fence="false" stretchy="true" minsize="2.4em" maxsize="2.4em">(</mo><mi>λ</mi><mi>f</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>n</mi><mi mathvariant="normal">.</mi><mo stretchy="false">(</mo><mrow><mi mathvariant="normal">z</mi><mi mathvariant="normal">e</mi><mi mathvariant="normal">r</mi><mi mathvariant="normal">o</mi></mrow><mtext> </mtext><mi>n</mi><mo stretchy="false">)</mo><mtext> </mtext><mn>0</mn><mtext> </mtext><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">(</mo><mo>+</mo><mtext> </mtext><mi>n</mi><mtext> </mtext><mo stretchy="false">(</mo><mi>f</mi><mtext> </mtext><mo stretchy="false">(</mo><mrow><mi mathvariant="normal">p</mi><mi mathvariant="normal">r</mi><mi mathvariant="normal">e</mi><mi mathvariant="normal">d</mi></mrow><mtext> </mtext><mi>n</mi><mo stretchy="false">)</mo><mo stretchy="false">)</mo><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">)</mo><mo fence="false" stretchy="true" minsize="2.4em" maxsize="2.4em">)</mo></mrow><annotation encoding="application/x-tex">\mathrm{sum}\triangleq Y\;\bigg(\lambda f.\lambda n.(\mathrm{zero}\;n)\;0\;\big(+\;n\;(f\;(\mathrm{pred}\;n))\big)\bigg)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.9987em;vertical-align:-0.082em;"></span><span class="mord"><span class="mord mathrm">sum</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.4em;vertical-align:-0.95em;"></span><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord"><span class="delimsizing size3">(</span></span><span class="mord mathnormal">λ</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mord">.</span><span class="mord mathnormal">λn</span><span class="mord">.</span><span class="mopen">(</span><span class="mord"><span class="mord mathrm">zero</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">n</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord">0</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord"><span class="delimsizing size1">(</span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:2.4em;vertical-align:-0.95em;"></span><span class="mord mathnormal">n</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mopen">(</span><span class="mord"><span class="mord mathrm">pred</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">n</span><span class="mclose">))</span><span class="mord"><span class="delimsizing size1">)</span></span><span class="mord"><span class="delimsizing size3">)</span></span></span></span></span></span></p><blockquote><p>构造 Y 组合子为什么要“开方”呢?其实不需要特殊理由,纯粹是一种构造技巧,选“开立方”、“开任意次方”甚至“夹心饼干”都行——来自知乎网友 canonical 的推导 <sup id="fnref:1" class="footnote-ref"><a href="#fn:1" rel="footnote"><span class="hint--top hint--rounded" aria-label="[Y组合子的一个启发式推导](https://zhuanlan.zhihu.com/p/547191928)">[1]</span></a></sup>,推荐去看一下</p></blockquote><h3 id="最后一环:除法">最后一环:除法</h3><p>用递归实现循环试减,就能定义除法和模运算:</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mtable rowspacing="0.25em" columnalign="right left" columnspacing="0em"><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><mi mathvariant="normal">/</mi></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mo>≜</mo><mi>Y</mi><mo fence="false" stretchy="true" minsize="2.4em" maxsize="2.4em">(</mo><mi>λ</mi><mi>d</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>a</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>b</mi><mi mathvariant="normal">.</mi><mo stretchy="false">(</mo><mo><</mo><mtext> </mtext><mi>a</mi><mtext> </mtext><mi>b</mi><mo stretchy="false">)</mo><mtext> </mtext><mn>0</mn><mtext> </mtext><mstyle mathcolor="royalblue"><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">(</mo><mo>+</mo><mtext> </mtext><mn>1</mn><mtext> </mtext><mo stretchy="false">(</mo><mi>d</mi><mtext> </mtext><mo stretchy="false">(</mo><mo>−</mo><mtext> </mtext><mi>a</mi><mtext> </mtext><mi>b</mi><mo stretchy="false">)</mo><mtext> </mtext><mi>b</mi><mo stretchy="false">)</mo><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">)</mo></mstyle><mo fence="false" stretchy="true" minsize="2.4em" maxsize="2.4em">)</mo></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mspace></mspace><mspace width="1em"/><mrow><mi mathvariant="normal">m</mi><mi mathvariant="normal">o</mi><mi mathvariant="normal">d</mi></mrow><mtext> </mtext><mtext> </mtext></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mo>≜</mo><mi>Y</mi><mo fence="false" stretchy="true" minsize="2.4em" maxsize="2.4em">(</mo><mi>λ</mi><mi>m</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>a</mi><mi mathvariant="normal">.</mi><mi>λ</mi><mi>b</mi><mi mathvariant="normal">.</mi><mo stretchy="false">(</mo><mo><</mo><mtext> </mtext><mi>a</mi><mtext> </mtext><mi>b</mi><mo stretchy="false">)</mo><mtext> </mtext><mi>a</mi><mtext> </mtext><mstyle mathcolor="royalblue"><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">(</mo><mi>m</mi><mtext> </mtext><mo stretchy="false">(</mo><mo>−</mo><mtext> </mtext><mi>a</mi><mtext> </mtext><mi>b</mi><mo stretchy="false">)</mo><mtext> </mtext><mi>b</mi><mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em">)</mo></mstyle><mo fence="false" stretchy="true" minsize="2.4em" maxsize="2.4em">)</mo></mrow></mstyle></mtd></mtr></mtable><annotation encoding="application/x-tex">\begin{aligned}/&\triangleq Y\bigg(\lambda d.\lambda a.\lambda b.(<\;a\;b)\;0\;\textcolor{royalblue}{\big(+\;1\;(d\;(-\;a\;b)\;b)\big)}\bigg) \\\mod&\triangleq Y\bigg(\lambda m.\lambda a.\lambda b.(<\;a\;b)\;a\;\textcolor{royalblue}{\big(m\;(-\;a\;b)\;b\big)}\bigg)\end{aligned}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:5.4001em;vertical-align:-2.45em;"></span><span class="mord"><span class="mtable"><span class="col-align-r"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:2.95em;"><span style="top:-4.95em;"><span class="pstrut" style="height:3.45em;"></span><span class="mord"><span class="mord">/</span></span></span><span style="top:-2.25em;"><span class="pstrut" style="height:3.45em;"></span><span class="mord"><span class="mspace allowbreak"></span><span class="mspace" style="margin-right:1em;"></span><span class="mord"><span class="mord"><span class="mord mathrm">mod</span></span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mspace" style="margin-right:0.1667em;"></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:2.45em;"><span></span></span></span></span></span><span class="col-align-l"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:2.95em;"><span style="top:-4.95em;"><span class="pstrut" style="height:3.45em;"></span><span class="mord"><span class="mord"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span><span class="mord"><span class="delimsizing size3">(</span></span><span class="mord mathnormal">λ</span><span class="mord mathnormal">d</span><span class="mord">.</span><span class="mord mathnormal">λa</span><span class="mord">.</span><span class="mord mathnormal">λb</span><span class="mord">.</span><span class="mopen">(</span><span class="mrel"><</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">b</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord">0</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord" style="color:royalblue;"><span class="delimsizing size1" style="color:royalblue;"><span style="color:royalblue;">(</span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin" style="color:royalblue;">+</span><span class="mspace" style="color:royalblue;margin-right:0.2778em;"></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord" style="color:royalblue;">1</span><span class="mspace" style="color:royalblue;margin-right:0.2778em;"></span><span class="mopen" style="color:royalblue;">(</span><span class="mord mathnormal" style="color:royalblue;">d</span><span class="mspace" style="color:royalblue;margin-right:0.2778em;"></span><span class="mopen" style="color:royalblue;">(</span><span class="mord" style="color:royalblue;">−</span><span class="mspace" style="color:royalblue;margin-right:0.2778em;"></span><span class="mord mathnormal" style="color:royalblue;">a</span><span class="mspace" style="color:royalblue;margin-right:0.2778em;"></span><span class="mord mathnormal" style="color:royalblue;">b</span><span class="mclose" style="color:royalblue;">)</span><span class="mspace" style="color:royalblue;margin-right:0.2778em;"></span><span class="mord mathnormal" style="color:royalblue;">b</span><span class="mclose" style="color:royalblue;">)</span><span class="mord" style="color:royalblue;"><span class="delimsizing size1" style="color:royalblue;"><span style="color:royalblue;">)</span></span></span><span class="mord"><span class="delimsizing size3">)</span></span></span></span><span style="top:-2.25em;"><span class="pstrut" style="height:3.45em;"></span><span class="mord"><span class="mord"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel amsrm">≜</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span><span class="mord"><span class="delimsizing size3">(</span></span><span class="mord mathnormal">λm</span><span class="mord">.</span><span class="mord mathnormal">λa</span><span class="mord">.</span><span class="mord mathnormal">λb</span><span class="mord">.</span><span class="mopen">(</span><span class="mrel"><</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">b</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord" style="color:royalblue;"><span class="delimsizing size1" style="color:royalblue;"><span style="color:royalblue;">(</span></span></span><span class="mord mathnormal" style="color:royalblue;">m</span><span class="mspace" style="color:royalblue;margin-right:0.2778em;"></span><span class="mopen" style="color:royalblue;">(</span><span class="mord" style="color:royalblue;">−</span><span class="mspace" style="color:royalblue;margin-right:0.2778em;"></span><span class="mord mathnormal" style="color:royalblue;">a</span><span class="mspace" style="color:royalblue;margin-right:0.2778em;"></span><span class="mord mathnormal" style="color:royalblue;">b</span><span class="mclose" style="color:royalblue;">)</span><span class="mspace" style="color:royalblue;margin-right:0.2778em;"></span><span class="mord mathnormal" style="color:royalblue;">b</span><span class="mord" style="color:royalblue;"><span class="delimsizing size1" style="color:royalblue;"><span style="color:royalblue;">)</span></span></span><span class="mord"><span class="delimsizing size3">)</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:2.45em;"><span></span></span></span></span></span></span></span></span></span></span></span></p><p>蓝色部分表示当被除数剩余部分仍大于除数时,商加一、被除数继续减去除数,最终累加结果是商、剩下的是余数。至此,自然数、加减乘除、条件分支和递归都已经初步构造好了,λ 演算终于有了一些“计算”的样子。</p><h2 id="参考资料">参考资料</h2><section class="footnotes"><div class="footnote-list"><ol><li><span id="fn:1" class="footnote-text"><span><a href="https://zhuanlan.zhihu.com/p/547191928">Y组合子的一个启发式推导</a><a href="#fnref:1" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:2" class="footnote-text"><span><a href="https://zhuanlan.zhihu.com/p/262284625">Y组合子(Y Combinator)| The Little Schemer 第九章</a><a href="#fnref:2" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:3" class="footnote-text"><span><a href="https://www.bilibili.com/video/BV1pU4y1v7Hj">lambda演算科普</a><a href="#fnref:3" rev="footnote" class="footnote-backref"> ↩</a></span></span></li></ol></div></section>]]></content:encoded>
<category domain="https://greyishsong.ink/categories/%E6%95%B0%E5%AD%A6/">数学</category>
<category domain="https://greyishsong.ink/tags/%CE%BB-%E6%BC%94%E7%AE%97/">λ 演算</category>
<category domain="https://greyishsong.ink/tags/%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BC%96%E7%A8%8B/">函数式编程</category>
<comments>https://greyishsong.ink/%E5%88%9D%E6%B6%89-%CE%BB-%E6%BC%94%E7%AE%97-lambda-calculus/#disqus_thread</comments>
</item>
<item>
<title>Lady Godiva (鹤田芳里) & Whale (Ivan D. Handoko)</title>
<link>https://greyishsong.ink/Lady-Godiva-%E9%B9%A4%E7%94%B0%E8%8A%B3%E9%87%8C-Whale-Ivan-D-Handoko/</link>
<guid>https://greyishsong.ink/Lady-Godiva-%E9%B9%A4%E7%94%B0%E8%8A%B3%E9%87%8C-Whale-Ivan-D-Handoko/</guid>
<pubDate>Mon, 20 Mar 2023 04:58:13 GMT</pubDate>
<description><p>《折纸侦探团》合集 25 期里的两个双色作品,用的都是 40 cm 的西口纸。</p></description>
<content:encoded><![CDATA[<p>《折纸侦探团》合集 25 期里的两个双色作品,用的都是 40 cm 的西口纸。</p><span id="more"></span><h2 id="Lady-Godiva">Lady Godiva</h2><p>Lady Godiva, 中文译名是戈黛娃夫人,不过更有名的应该是以她命名的巧克力品牌歌帝梵。</p><blockquote><p>Lady Godiva took pity on the people of Coventry, who were suffering grievously under her husband's oppressive taxation. Lady Godiva appealed again and again to her husband, who obstinately refused to lower the taxes. At last, weary of her entreaties, he said he would grant her request if she would strip naked and ride on a horse through the streets of the town. Lady Godiva took him at his word, and after issuing a proclamation that all persons should stay indoors and shut their windows, she rode through the town, clothed only in her long hair.<sup id="fnref:1" class="footnote-ref"><a href="#fn:1" rel="footnote"><span class="hint--top hint--rounded" aria-label="[Lady Godiva](https://en.wikipedia.org/wiki/Lady_Godiva)">[1]</span></a></sup></p><p>依据传说,戈黛娃于1040年嫁给了麦西亚伯爵利奥弗里克。当时利奥夫里克对考文垂市民们强加重税,戈黛娃夫人不断的向丈夫求情希望减免税收,但都被他顽固的拒绝了。利奥夫里克对妻子的不断求情感到厌烦,宣称只要她能裸体骑马绕行市内的街道,他便愿意减税。心地善良的戈黛娃夫人真的照着他的话去做,要所有市民待在屋内并紧闭门窗后,她一丝不挂、披着一头长发骑马游街。<sup id="fnref:2" class="footnote-ref"><a href="#fn:2" rel="footnote"><span class="hint--top hint--rounded" aria-label="[戈黛娃夫人](https://zh.wikipedia.org/zh-cn/%E6%88%88%E9%BB%9B%E5%A8%83%E5%A4%AB%E4%BA%BA)">[2]</span></a></sup></p></blockquote><p>折之前我并不了解这个传说,甚至在保存照片时把这位夫人的名字误写作 Godvia, 好在最后改过来了。</p><p><img src="/Lady-Godiva-%E9%B9%A4%E7%94%B0%E8%8A%B3%E9%87%8C-Whale-Ivan-D-Handoko/DSC_2172.jpg" alt=""></p><p><img src="/Lady-Godiva-%E9%B9%A4%E7%94%B0%E8%8A%B3%E9%87%8C-Whale-Ivan-D-Handoko/DSC_2173.jpg" alt=""></p><p><img src="/Lady-Godiva-%E9%B9%A4%E7%94%B0%E8%8A%B3%E9%87%8C-Whale-Ivan-D-Handoko/DSC_2174.jpg" alt=""></p><h2 id="Whale">Whale</h2><p>一位新加坡爱好者的设计,折图有点粗糙,不过没什么大坑。纸层分布挺均匀,但好多地方缺乏自锁从而容易散开,费了我不少功夫来整形。作品名就叫 Whale, 显然是须鲸的经典外形,我觉得成品头部比例有些长,更像是小一点的种类。</p><p>鲸的形状很不好放平,为了能拍出自然一点的姿势,最后硬是把我的四面、六面、十二面骰子都拿来当支架,像天平架子似的把它架起来。眼尖的话,应该不难看到第二张图右鳍的影子形状有点不对,就是垫着骰子啦。</p><p><img src="/Lady-Godiva-%E9%B9%A4%E7%94%B0%E8%8A%B3%E9%87%8C-Whale-Ivan-D-Handoko/DSC_2275.JPG" alt=""></p><p><img src="/Lady-Godiva-%E9%B9%A4%E7%94%B0%E8%8A%B3%E9%87%8C-Whale-Ivan-D-Handoko/DSC_2281.JPG" alt=""></p><p><img src="/Lady-Godiva-%E9%B9%A4%E7%94%B0%E8%8A%B3%E9%87%8C-Whale-Ivan-D-Handoko/DSC_2282.JPG" alt=""></p><p>最后放一张翻肚皮结尾:(可见成品的另一个缺点是有些扁平,所以俯视或者仰视视角的效果不太好)</p><p><img src="/Lady-Godiva-%E9%B9%A4%E7%94%B0%E8%8A%B3%E9%87%8C-Whale-Ivan-D-Handoko/DSC_2286.JPG" alt=""></p><h2 id="参考">参考</h2><section class="footnotes"><div class="footnote-list"><ol><li><span id="fn:1" class="footnote-text"><span><a href="https://en.wikipedia.org/wiki/Lady_Godiva">Lady Godiva</a><a href="#fnref:1" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:2" class="footnote-text"><span><a href="https://zh.wikipedia.org/zh-cn/%E6%88%88%E9%BB%9B%E5%A8%83%E5%A4%AB%E4%BA%BA">戈黛娃夫人</a><a href="#fnref:2" rev="footnote" class="footnote-backref"> ↩</a></span></span></li></ol></div></section>]]></content:encoded>
<category domain="https://greyishsong.ink/categories/%E6%89%8B%E5%B7%A5%E8%89%BA/">手工艺</category>
<category domain="https://greyishsong.ink/tags/%E6%8A%98%E7%BA%B8/">折纸</category>
<category domain="https://greyishsong.ink/tags/%E7%94%9F%E6%B4%BB/">生活</category>
<comments>https://greyishsong.ink/Lady-Godiva-%E9%B9%A4%E7%94%B0%E8%8A%B3%E9%87%8C-Whale-Ivan-D-Handoko/#disqus_thread</comments>
</item>
<item>
<title>港村邻里(一):新渭沙湿地公园</title>
<link>https://greyishsong.ink/%E6%B8%AF%E6%9D%91%E9%82%BB%E9%87%8C%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E6%96%B0%E6%B8%AD%E6%B2%99%E6%B9%BF%E5%9C%B0%E5%85%AC%E5%9B%AD/</link>
<guid>https://greyishsong.ink/%E6%B8%AF%E6%9D%91%E9%82%BB%E9%87%8C%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E6%96%B0%E6%B8%AD%E6%B2%99%E6%B9%BF%E5%9C%B0%E5%85%AC%E5%9B%AD/</guid>
<pubDate>Tue, 14 Mar 2023 06:23:13 GMT</pubDate>
<description><p>想标题的时候,莫名想要开一个新的系列。有想到除了这次逛的新渭沙外,还有咸阳湖二期可以再写一篇,至少不会写完开头就结束,那就开吧。中国西部科技创新港,社会各界惯用的简称是创新港;又因远离市区,同学们也常用港村的戏称。</p>
<p>港村坐落渭河之滨,有时和父母开玩笑说是河滩上,周边也的确水草丰茂,乃至校园里也可以种芦苇。周围的三个湿地公园里,新渭沙是唯一没有真正紧邻渭河的,只有小小的一条无名支流纵贯其中。今年逛的时候,乱花渐欲迷人眼的阶段快要过去了,然而还剩下的草木也不错。</p></description>
<content:encoded><![CDATA[<p>想标题的时候,莫名想要开一个新的系列。有想到除了这次逛的新渭沙外,还有咸阳湖二期可以再写一篇,至少不会写完开头就结束,那就开吧。中国西部科技创新港,社会各界惯用的简称是创新港;又因远离市区,同学们也常用港村的戏称。</p><p>港村坐落渭河之滨,有时和父母开玩笑说是河滩上,周边也的确水草丰茂,乃至校园里也可以种芦苇。周围的三个湿地公园里,新渭沙是唯一没有真正紧邻渭河的,只有小小的一条无名支流纵贯其中。今年逛的时候,乱花渐欲迷人眼的阶段快要过去了,然而还剩下的草木也不错。</p><span id="more"></span><p>从务本路出门,穿过道科就已经到了这条小河边上。再往北去有条小路,甚至没有人行道,好在往来的汽车开得也不快。没有售票处、没有大门,甚至没有标牌,走两步就算进入新渭沙湿地公园了。</p><p>路两旁就是不太好的草地,没有各种野花野草,单调得近乎足球场草坪。树也整整齐齐,树坑只一排,两两等距。再往里走,草木稍微随便些,有随处可见的折叠椅和吊床,这还是真少见。我逛过的绿地不外乎保护区和城市公园两种,前者人迹寥寥,当然不会有什么吊床;后者草多而树少,更不会有什么人把吊床挂在行道树上在路旁触手可及的地方假寐。这儿的小树间距往往刚好,吊床和帐篷三三两两散落在草地上,下午三点的阳光播撒让人不想动的气息,让一个从实验室走出来的工作狂很不适应。</p><p>路旁先是松树和连翘,然后是桃花,都已是一副晚春的架势,半开半谢。连翘花下垂着,桃花有些残缺,但阳光耀眼,于是它们也耀眼。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="/%E6%B8%AF%E6%9D%91%E9%82%BB%E9%87%8C%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E6%96%B0%E6%B8%AD%E6%B2%99%E6%B9%BF%E5%9C%B0%E5%85%AC%E5%9B%AD/DSC_2200.JPG" alt=""></div><div class="group-image-wrap"><img src="/%E6%B8%AF%E6%9D%91%E9%82%BB%E9%87%8C%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E6%96%B0%E6%B8%AD%E6%B2%99%E6%B9%BF%E5%9C%B0%E5%85%AC%E5%9B%AD/DSC_2204.JPG" alt=""></div></div></div><p><img src="/%E6%B8%AF%E6%9D%91%E9%82%BB%E9%87%8C%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E6%96%B0%E6%B8%AD%E6%B2%99%E6%B9%BF%E5%9C%B0%E5%85%AC%E5%9B%AD/DSC_2201.JPG" alt=""></p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="/%E6%B8%AF%E6%9D%91%E9%82%BB%E9%87%8C%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E6%96%B0%E6%B8%AD%E6%B2%99%E6%B9%BF%E5%9C%B0%E5%85%AC%E5%9B%AD/DSC_2209.JPG" alt=""></div><div class="group-image-wrap"><img src="/%E6%B8%AF%E6%9D%91%E9%82%BB%E9%87%8C%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E6%96%B0%E6%B8%AD%E6%B2%99%E6%B9%BF%E5%9C%B0%E5%85%AC%E5%9B%AD/DSC_2210.JPG" alt=""></div></div></div><p><img src="/%E6%B8%AF%E6%9D%91%E9%82%BB%E9%87%8C%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E6%96%B0%E6%B8%AD%E6%B2%99%E6%B9%BF%E5%9C%B0%E5%85%AC%E5%9B%AD/DSC_2214.JPG" alt=""></p><p>白花、红萼、绿叶,盛开时三种颜色不能凑齐,将谢时反而正好,观赏期延长许多。未长开的叶子像茶叶,想起新冠康复后喝不清茶叶的味道,继续难过。</p><p>一路往前走,依然感觉不过是人工种了些草的河滩。花很好,但它即使不在湿地公园也很好,只有这些还是无趣。来都来了,又没抱很高的期望,于是继续走,开始分不清东南西北地绕圈。河边上有些黑顶白颊的小鸟,警惕性不太强,能在六七步远处拍照。</p><p><img src="/%E6%B8%AF%E6%9D%91%E9%82%BB%E9%87%8C%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E6%96%B0%E6%B8%AD%E6%B2%99%E6%B9%BF%E5%9C%B0%E5%85%AC%E5%9B%AD/DSC_2218.JPG" alt=""></p><p>这湿地公园里是修了不少步道的,总好像把城市公园的某些建设方式搬过来。当然此处偏僻,可以多种些花木,也可种芦苇,多少有一些其他的禽鸟。可还是要批评一下,多种草木种得像花农的花田,然而丈许大的花田实在是奇怪,遑论路旁的树插秧一般,更加退化到街边花坛的水准了。</p><p><img src="/%E6%B8%AF%E6%9D%91%E9%82%BB%E9%87%8C%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E6%96%B0%E6%B8%AD%E6%B2%99%E6%B9%BF%E5%9C%B0%E5%85%AC%E5%9B%AD/DSC_2252.JPG" alt=""></p><p><img src="/%E6%B8%AF%E6%9D%91%E9%82%BB%E9%87%8C%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E6%96%B0%E6%B8%AD%E6%B2%99%E6%B9%BF%E5%9C%B0%E5%85%AC%E5%9B%AD/DSC_2265.JPG" alt=""></p><p><img src="/%E6%B8%AF%E6%9D%91%E9%82%BB%E9%87%8C%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E6%96%B0%E6%B8%AD%E6%B2%99%E6%B9%BF%E5%9C%B0%E5%85%AC%E5%9B%AD/DSC_2258.JPG" alt=""></p><p>栽种没什么趣味,花确实养得不错。紫叶李开得比校内要好,向阳处满树花瓣反射阳光,可以遮盖枝叶。而越向上花越是密集簇拥,成片的亮点浮动。若是红花红叶,想必如同火炬;白花是没有那般热烈的,只像浪花溅起泡沫,干干净净向空中升去。大片相连时又显出深浅变化来,潮水似的涨落,也有朋友说想到量子的。</p><p><img src="/%E6%B8%AF%E6%9D%91%E9%82%BB%E9%87%8C%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E6%96%B0%E6%B8%AD%E6%B2%99%E6%B9%BF%E5%9C%B0%E5%85%AC%E5%9B%AD/DSC_2238.JPG" alt=""></p><p><img src="/%E6%B8%AF%E6%9D%91%E9%82%BB%E9%87%8C%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E6%96%B0%E6%B8%AD%E6%B2%99%E6%B9%BF%E5%9C%B0%E5%85%AC%E5%9B%AD/DSC_2241.JPG" alt=""></p><p><img src="/%E6%B8%AF%E6%9D%91%E9%82%BB%E9%87%8C%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E6%96%B0%E6%B8%AD%E6%B2%99%E6%B9%BF%E5%9C%B0%E5%85%AC%E5%9B%AD/DSC_2244.JPG" alt=""></p><p>迎春和连翘一块一块种在一起,也许全开时分不太清,现在迎春花期差不多过去,自然就分出黄绿相间的方块儿,我是感到有些好笑。在迎春的方块儿里翻找,看到一朵不错便顺手拍下,回来才发现这是七瓣的迎春,虽不比四叶的酢浆草罕见,也可以堪比去邮局寄信时邮政师傅突然翻出张少见的邮票了。此地还遇见天蛾,这是出乎预料的。这种白天飞出来的蛾子似乎不太少见,但躲人水平很高,远远就绕开人飞,平时实在难找。</p><p><img src="/%E6%B8%AF%E6%9D%91%E9%82%BB%E9%87%8C%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E6%96%B0%E6%B8%AD%E6%B2%99%E6%B9%BF%E5%9C%B0%E5%85%AC%E5%9B%AD/DSC_2255.JPG" alt=""></p><p><img src="/%E6%B8%AF%E6%9D%91%E9%82%BB%E9%87%8C%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E6%96%B0%E6%B8%AD%E6%B2%99%E6%B9%BF%E5%9C%B0%E5%85%AC%E5%9B%AD/DSC_2262.JPG" alt=""></p><p>一大圈绕出来其实没什么可叹的景色,不过出门走走还是比操场转圈来得有趣些,但愿咸阳湖值得期待。</p>]]></content:encoded>
<category domain="https://greyishsong.ink/categories/%E9%9A%8F%E7%AC%94/">随笔</category>
<category domain="https://greyishsong.ink/tags/%E7%94%9F%E6%B4%BB/">生活</category>
<category domain="https://greyishsong.ink/tags/%E6%8B%8D%E6%91%84/">拍摄</category>
<comments>https://greyishsong.ink/%E6%B8%AF%E6%9D%91%E9%82%BB%E9%87%8C%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E6%96%B0%E6%B8%AD%E6%B2%99%E6%B9%BF%E5%9C%B0%E5%85%AC%E5%9B%AD/#disqus_thread</comments>
</item>
<item>
<title>我手中的花花草草</title>
<link>https://greyishsong.ink/%E6%88%91%E6%89%8B%E4%B8%AD%E7%9A%84%E8%8A%B1%E8%8A%B1%E8%8D%89%E8%8D%89/</link>
<guid>https://greyishsong.ink/%E6%88%91%E6%89%8B%E4%B8%AD%E7%9A%84%E8%8A%B1%E8%8A%B1%E8%8D%89%E8%8D%89/</guid>
<pubDate>Thu, 01 Sep 2022 11:04:48 GMT</pubDate>
<description><p>做纸花竟也两年半有余了。翻一翻自己手里的照片,也想起这些花花草草陪伴我度过的日子。棉纸的失败尝试也好,后来皱纹纸用顺手了也罢,总是怀念每一束被我送出去的花。不过花没做多少,我就有点想尝试点儿新想法,不想总是按图索骥了。</p></description>
<content:encoded><![CDATA[<p>做纸花竟也两年半有余了。翻一翻自己手里的照片,也想起这些花花草草陪伴我度过的日子。棉纸的失败尝试也好,后来皱纹纸用顺手了也罢,总是怀念每一束被我送出去的花。不过花没做多少,我就有点想尝试点儿新想法,不想总是按图索骥了。</p><span id="more"></span><p>起初是忙碌的时候感到休息的时间太琐碎,而折纸总是一上手就停不下来,还要提前清理桌子才能开始。不知何时看到一本做纸花的书,彼时不明就里地去购买书上提及的棉纸,最后却买到了一种完全不合预期的东西。这纸张虽然也叫做“棉纸”,却不像是用通常的纸浆造出的,似乎是加入了棉纤维而光滑坚韧,麻烦正在于此:过于光滑以致胶粘不住,过于坚韧以致塑形无法保持。用它做了秋海棠与天竺葵,前者正面观感尚可但极易散架,后者则呆板缺乏生趣,难以让我满意。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="/%E6%88%91%E6%89%8B%E4%B8%AD%E7%9A%84%E8%8A%B1%E8%8A%B1%E8%8D%89%E8%8D%89/%E7%A7%8B%E6%B5%B7%E6%A3%A0.jpg" alt=""></div><div class="group-image-wrap"><img src="/%E6%88%91%E6%89%8B%E4%B8%AD%E7%9A%84%E8%8A%B1%E8%8A%B1%E8%8D%89%E8%8D%89/%E5%A4%A9%E7%AB%BA%E8%91%B5.jpg" alt=""></div></div></div><p>两次失败有点丧气,放下纸花一段时间,继续折纸。还是又一次看到有同好用皱纹纸做花,恰好淘宝推荐了一本书,这才捡起来,重新买皱纹纸继续做。这一次顺利许多,零碎尝试过后做了栀子和香豌豆搭的花束,收获了超过预期的成果,遂决定送出。</p><p><img src="/%E6%88%91%E6%89%8B%E4%B8%AD%E7%9A%84%E8%8A%B1%E8%8A%B1%E8%8D%89%E8%8D%89/%E6%A0%80%E5%AD%90.jpg" alt=""></p><p>这一束做到一半时,亲戚来家里聊天一眼便看中,想我做一束鲜艳些的用来摆餐桌。于是送出了这一束又做了一把玫瑰——是玫瑰不是月季——并在尚未拍照留影时就送走了。我因上学而不常在家,但后来在电话中得知亲戚有些炫耀式地介绍我的花,还是很开心。往后的暑假里为了解闷又开始做木百合,只来得及做一支就匆匆开学。这一支在家里放到现在已然被晒得褪色,我也由此推测皱纹纸花在日晒下的存放时间大概是一二年。</p><p><img src="/%E6%88%91%E6%89%8B%E4%B8%AD%E7%9A%84%E8%8A%B1%E8%8A%B1%E8%8D%89%E8%8D%89/%E6%9C%A8%E7%99%BE%E5%90%88.jpg" alt=""></p><p>在长时间的沉寂里是有一些难以言说的苦涩,身体状况不佳也让我很少出门走动。上课、做实验、勉强追求更高的成绩,也在精神稍有改善的时候拿起几个月前留在桌上的边角料,做了一枝小小的白花,并姑且称之为茉莉。</p><p><img src="/%E6%88%91%E6%89%8B%E4%B8%AD%E7%9A%84%E8%8A%B1%E8%8A%B1%E8%8D%89%E8%8D%89/%E8%8C%89%E8%8E%89.jpg" alt=""></p><p>同期开工的向日葵则在两次失败后被搁置,两朵看起来像怪异雏菊的残次品也被丢弃。期末考试和暑期交流项目带来的各种安排占据了我绝大多数状态尚可的时间,可惜最后保研还是差了一点,Offer 也不想要了。九月底开始复习考研有点辛苦,但我的复习过程又称不上艰难。高数题做累了就去听徐涛老师的政治课,政治课听累了就回宿舍偷空做向日葵。再一次尝试时我买了食用色素,试图用它给黄色的皱纹纸染上一部分橙色。调过几次颜色、染过几回手指以后我很顺利地得到了自己想要的颜色和效果,然而给这些宽度不及一根筷子的花瓣染色简直比做题还要累,尤其是需要染百八十片花瓣的时候。最后得到的是一种特别快乐的颜色,看一眼就会开心。</p><p><img src="/%E6%88%91%E6%89%8B%E4%B8%AD%E7%9A%84%E8%8A%B1%E8%8A%B1%E8%8D%89%E8%8D%89/%E5%90%91%E6%97%A5%E8%91%B5-1.jpg" alt=""></p><p>我想不出什么好办法来做密集的管状花,只好把纸条剪成流苏,再把每一个细条卷起来。这办法效果不错,缺点是相当费手。一个花序卷出来也不知道需要一小时还是两小时,手指酸得拿不住笔。为了在这种重复劳动的间隙中增加一些趣味,我又开始做玫瑰,这次整个花瓣都是自己染色。我想要所谓的香槟色,并把它理解为介于橙色和金色之间的很浅的颜色。更大的花瓣更容易染色,可被颜料浸湿后变形也更厉害。本着随便试试的原则,我没太在意花瓣的形状和布局,很快也有了一些试验之作。</p><p><img src="/%E6%88%91%E6%89%8B%E4%B8%AD%E7%9A%84%E8%8A%B1%E8%8A%B1%E8%8D%89%E8%8D%89/%E5%90%91%E6%97%A5%E8%91%B5-2.jpg" alt=""></p><p>橙色染得有些深,那么索性用颜料加一些更深的变化好了。就这样我在考研复试结束前后搭出了这一束花的雏形:向日葵、玫瑰配尤加利叶,之后添了点儿满天星。</p><p><img src="/%E6%88%91%E6%89%8B%E4%B8%AD%E7%9A%84%E8%8A%B1%E8%8A%B1%E8%8D%89%E8%8D%89/%E5%90%91%E6%97%A5%E8%91%B5-3.jpg" alt=""></p><p>写毕业论文时,一边写一边做点叶子加上去,终于是凑了一束还算满意的,在结题后送与导师作纪念。此时身体恢复无恙,同时也在做许多大花瓣,论文提交之后正好有空。但几个月的紧张之后积累下来的懒惰实在太多,以至于直到离校前两天我才做好了一束广玉兰。第一次见到这种优雅的大花是初中毕业后去黄山旅游,山脚下的黄山市有一条街道,行道树正是广玉兰。之后又在杭州旅馆旁冷清的街道上,这一次已是高中毕业,即将参加自主招生考试了。走前我把花带下楼,在彭康书院的小院子里拍了半天,并开玩笑地发说说称楼下的广玉兰是我四年来对书院最深的印象。但还有楼下的四季桂呢,至少不相上下吧。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="/%E6%88%91%E6%89%8B%E4%B8%AD%E7%9A%84%E8%8A%B1%E8%8A%B1%E8%8D%89%E8%8D%89/%E5%B9%BF%E7%8E%89%E5%85%B0-1.jpg" alt=""></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="/%E6%88%91%E6%89%8B%E4%B8%AD%E7%9A%84%E8%8A%B1%E8%8A%B1%E8%8D%89%E8%8D%89/%E5%B9%BF%E7%8E%89%E5%85%B0-2.jpg" alt=""></div><div class="group-image-wrap"><img src="/%E6%88%91%E6%89%8B%E4%B8%AD%E7%9A%84%E8%8A%B1%E8%8A%B1%E8%8D%89%E8%8D%89/%E5%B9%BF%E7%8E%89%E5%85%B0-3.jpg" alt=""></div></div></div><p>放假回家依然有点忙,忙起来之后的原则就是:不做大件,来点新的。我的手艺有限,做出的花乍看有五分像真的,然而稍加分辨就会感觉相去甚远。既然做不成真花,那么能做成真花做不出的东西吗?铁丝、纸张这样的材料,塑形的自由度毕竟胜过枝条,我大概可以做出更丰富的形状。回想起看过的插画里动物和花草融合的形象,我决定试试用纸张实现类似的效果。</p><p>动物就选鸟,然而怎么用纸张塑造鸟身的形状是个大问题。起先是用铁丝做骨架,再用纸条蒙皮。但实践很快就证实了我这方面的笨拙,最后得到的半成品堪称惨烈,自己都羞于承认这是只鸟。在朋友的提醒和 B 站视频的帮助下,我改用纸团和纸胶带做实心的身体(是的,读者愿意这么想的话,那就是艺术创想)。感谢五中的作业本,这是我能找到的最薄、最软但又不至于像纸巾一样不成形的纸了。尽管它也许不太适合书写,但真的很适合用来揉纸团。在这个过程中,我深深理解了尼尔叔叔对报纸团的喜爱——某种意义上这绝对堪比粘土,成品还比粘土轻!有了一个大概成形的身体以后就是无穷无尽的剪毛工作了,把纸张剪成碎羽毛的过程每次都会剪到手酸,而每次拇指累得张不开剪刀时我就去贴羽毛。</p><p>自从折过一只火烈鸟之后,我就觉得它站着的样子神似某种盆栽,所以这次的成品就是一只盆栽火烈鸟。它生活的地方可能没有玫瑰,但这不重要,只要我能做出来,它就可以从一枝玫瑰上长出来。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="/%E6%88%91%E6%89%8B%E4%B8%AD%E7%9A%84%E8%8A%B1%E8%8A%B1%E8%8D%89%E8%8D%89/%E7%81%AB%E7%83%88%E9%B8%9F-1.jpg" alt=""></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="/%E6%88%91%E6%89%8B%E4%B8%AD%E7%9A%84%E8%8A%B1%E8%8A%B1%E8%8D%89%E8%8D%89/%E7%81%AB%E7%83%88%E9%B8%9F-2.jpg" alt=""></div><div class="group-image-wrap"><img src="/%E6%88%91%E6%89%8B%E4%B8%AD%E7%9A%84%E8%8A%B1%E8%8A%B1%E8%8D%89%E8%8D%89/%E7%81%AB%E7%83%88%E9%B8%9F-3.jpg" alt=""></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="/%E6%88%91%E6%89%8B%E4%B8%AD%E7%9A%84%E8%8A%B1%E8%8A%B1%E8%8D%89%E8%8D%89/%E7%81%AB%E7%83%88%E9%B8%9F-5.jpg" alt=""></div><div class="group-image-wrap"><img src="/%E6%88%91%E6%89%8B%E4%B8%AD%E7%9A%84%E8%8A%B1%E8%8A%B1%E8%8D%89%E8%8D%89/%E7%81%AB%E7%83%88%E9%B8%9F-6.jpg" alt=""></div></div></div><p>感谢老爸帮忙完成了地台的框架的榫卯结合部分,土堆则是用皱纹纸和热熔胶堆起来的。试图画上眼睛的尝试很失败,因此我不得不用更多的羽毛盖住了头,并安慰自己种出来的总有点不一样。不过我对羽毛的笨拙模仿效果尚可,鸟身和枝叶接合的部分也还不错,不失为一个成功的尝试。家里没有水粉颜料,水彩的遮盖力不太好,我也不太会把握深浅,最后还是靠“涂深了就用白色盖上去”的办法弥补了不少失误。</p><p>最后放一张标准侧身照(蝴蝶其实很小,然而花更小),希望下次尝试成功!</p><p><img src="/%E6%88%91%E6%89%8B%E4%B8%AD%E7%9A%84%E8%8A%B1%E8%8A%B1%E8%8D%89%E8%8D%89/%E7%81%AB%E7%83%88%E9%B8%9F-4.jpg" alt=""></p>]]></content:encoded>
<category domain="https://greyishsong.ink/categories/%E6%89%8B%E5%B7%A5%E8%89%BA/">手工艺</category>
<category domain="https://greyishsong.ink/tags/%E7%94%9F%E6%B4%BB/">生活</category>
<category domain="https://greyishsong.ink/tags/%E7%BA%B8%E8%8A%B1/">纸花</category>
<comments>https://greyishsong.ink/%E6%88%91%E6%89%8B%E4%B8%AD%E7%9A%84%E8%8A%B1%E8%8A%B1%E8%8D%89%E8%8D%89/#disqus_thread</comments>
</item>
<item>
<title>七月闲逛</title>
<link>https://greyishsong.ink/%E4%B8%83%E6%9C%88%E9%97%B2%E9%80%9B/</link>
<guid>https://greyishsong.ink/%E4%B8%83%E6%9C%88%E9%97%B2%E9%80%9B/</guid>
<pubDate>Wed, 25 Aug 2021 13:03:33 GMT</pubDate>
<description><p>期末考完,本科生涯的尾声已然不远,然而西安还没怎么转过。所以抓紧一些时间,信马由缰地多走几步,看看城墙里的老城区。</p>
<p>也懒得写什么游记,只记一下拍照时是怎样的想法,免得日后一忘皆空。</p></description>
<content:encoded><![CDATA[<p>期末考完,本科生涯的尾声已然不远,然而西安还没怎么转过。所以抓紧一些时间,信马由缰地多走几步,看看城墙里的老城区。</p><p>也懒得写什么游记,只记一下拍照时是怎样的想法,免得日后一忘皆空。</p><span id="more"></span><img src="/%E4%B8%83%E6%9C%88%E9%97%B2%E9%80%9B/1.jpg" class=""><p>下地铁,自和平门入城墙,门外绕城的小河。拍摄时不过八点多,曝光很短所以比较暗。</p><img src="/%E4%B8%83%E6%9C%88%E9%97%B2%E9%80%9B/2.jpg" class=""><p>城门楼的灯光还不错,比兴庆宫的好。</p><img src="/%E4%B8%83%E6%9C%88%E9%97%B2%E9%80%9B/3.jpg" class=""><p>拆迁时蒙在老旧房屋骨架上的金属箔,灯光昏暗,只有星星点点的反光。</p><img src="/%E4%B8%83%E6%9C%88%E9%97%B2%E9%80%9B/4.jpg" class=""><p>真的很黑,连外墙上剥落的贴砖也看不太清楚。</p><img src="/%E4%B8%83%E6%9C%88%E9%97%B2%E9%80%9B/5.jpg" class=""><p>存在于三四岁记忆中的小卖部样式,在太原不曾再见过。</p><img src="/%E4%B8%83%E6%9C%88%E9%97%B2%E9%80%9B/6.jpg" class=""><img src="/%E4%B8%83%E6%9C%88%E9%97%B2%E9%80%9B/7.jpg" class=""><p>仿佛上个时代的粮油店,家里似乎从来没在这样的店里买过粮油。</p><img src="/%E4%B8%83%E6%9C%88%E9%97%B2%E9%80%9B/8.jpg" class=""><img src="/%E4%B8%83%E6%9C%88%E9%97%B2%E9%80%9B/9.jpg" class=""><p>有一角街区日料聚集,做旧的水平不错。</p><img src="/%E4%B8%83%E6%9C%88%E9%97%B2%E9%80%9B/10.jpg" class=""><p>不像餐馆的日料店,远看会怀疑是卖首饰的。</p><img src="/%E4%B8%83%E6%9C%88%E9%97%B2%E9%80%9B/11.jpg" class=""><p>这家店窄小到只有两张桌子。</p><img src="/%E4%B8%83%E6%9C%88%E9%97%B2%E9%80%9B/12.jpg" class=""><p>想起吃饭时已经很晚,推门进去询问的结果是还剩最后一点米饭。</p><img src="/%E4%B8%83%E6%9C%88%E9%97%B2%E9%80%9B/13.jpg" class=""><img src="/%E4%B8%83%E6%9C%88%E9%97%B2%E9%80%9B/14.jpg" class=""><p>鳗鱼饭成功地吸引了店家养的小狗,然而寿司的效果就比较差。</p><img src="/%E4%B8%83%E6%9C%88%E9%97%B2%E9%80%9B/15.jpg" class=""><p>挂在水管上的路牌,有路人在此合影,虽然离真正的西安站还有些远。</p><img src="/%E4%B8%83%E6%9C%88%E9%97%B2%E9%80%9B/16.jpg" class=""><p>工地上竖起的工程机械,有一点像小提琴。</p>]]></content:encoded>
<category domain="https://greyishsong.ink/categories/%E9%9A%8F%E7%AC%94/">随笔</category>
<category domain="https://greyishsong.ink/tags/%E7%94%9F%E6%B4%BB/">生活</category>
<category domain="https://greyishsong.ink/tags/%E6%8B%8D%E6%91%84/">拍摄</category>
<comments>https://greyishsong.ink/%E4%B8%83%E6%9C%88%E9%97%B2%E9%80%9B/#disqus_thread</comments>
</item>
<item>
<title>SDN实验(四):测定链路时延</title>
<link>https://greyishsong.ink/SDN%E5%AE%9E%E9%AA%8C%EF%BC%88%E5%9B%9B%EF%BC%89%EF%BC%9A%E6%B5%8B%E5%AE%9A%E9%93%BE%E8%B7%AF%E6%97%B6%E5%BB%B6/</link>
<guid>https://greyishsong.ink/SDN%E5%AE%9E%E9%AA%8C%EF%BC%88%E5%9B%9B%EF%BC%89%EF%BC%9A%E6%B5%8B%E5%AE%9A%E9%93%BE%E8%B7%AF%E6%97%B6%E5%BB%B6/</guid>
<pubDate>Mon, 16 Aug 2021 10:39:53 GMT</pubDate>
<description><p>当一个 SDN 网络建立时,Ryu 控制器通过 LLDP 数据包与交换机通信,获取网络的各种信息。借助 LLDP 数据包上的时间戳,控制器可以测定两个交换机之间的传输时延,用于寻找数据传输的最短路径等。</p></description>
<content:encoded><![CDATA[<p>当一个 SDN 网络建立时,Ryu 控制器通过 LLDP 数据包与交换机通信,获取网络的各种信息。借助 LLDP 数据包上的时间戳,控制器可以测定两个交换机之间的传输时延,用于寻找数据传输的最短路径等。</p><span id="more"></span><h2 id="在-Ryu-中维护端口时延">在 Ryu 中维护端口时延</h2><p>Ryu 维护的端口信息在<code>ryu/ryu/topology/switch.py</code>中定义为<code>PortData</code>类,可以修改其<code>__init__</code>来增加属性。当 LLDP 数据包规划从某个端口发送时,对应实例就会更新时间戳<code>timestamp = time.time()</code>。</p><p>这里再加上一个时延属性</p><figure><div class="code-wrapper"><pre class="line-numbers language-python" data-language="python"><code class="language-python">self<span class="token punctuation">.</span>delay <span class="token operator">=</span> <span class="token number">0</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre></div></figure><p>用于记录<strong>根据该端口转发出的 LLDP 包计算的时延</strong>。听来有些拗口,若用图示转发过程</p><p><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="313px" height="322px" viewBox="-0.5 -0.5 313 322" content="<mxfile host="Electron" modified="2021-08-15T15:04:57.437Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.6.13 Chrome/89.0.4389.128 Electron/12.0.7 Safari/537.36" etag="mHM4eCmGfbP5u2dw-28u" version="14.6.13" type="device"><diagram id="8ygaH7sbF4w9pxboXoVX" name="第 1 页">7Zlbb5swFMc/TR4XAcaBPOa27WHTKmXS1r1ULrhg1WBknFs//QwYYky6XJomndo8RJxjc2zO/2cfnPTAJFl/4SiLv7MQ055jhesemPYcB4Kh/C4cm8oBoFM5Ik7CymVvHXPyhJXTUt4FCXHe6igYo4JkbWfA0hQHouVDnLNVu9sDo+1RMxThjmMeINr1/iKhiJXXHgy3DV8xiWI1tO94VUOC6s7qSfIYhWylucCsByacMVFdJesJpkXu6rxU931+prWZGMepOOSGJ/hz9Ac/LgHkOIM08MEP9klFWSK6UA+sJis2dQbkvLPiMllHhcL9gOQB6wcsyRYC8/wOpeFdhjnJYswRzftZ0APjWCRU3mPLy4yRVHacLeU8c+ULUR7jYl6WNB4IpRNGGS/HA9YQQjiW/lxw9oi1lvFo4k9B01LrITM5XmIuiFTtG7rH9IblRBCWyrZ7JgRLtA4jSqKiQbBMepGyAlxMUTrYQlCSykFrmsoJslRo05iVH+lXuZOR8fpZUexGarlEMEuw4BvZRd0AgKJDLY+hMldb1jy/csUaZZ7qhhTdURN4C4C8UAwcwYNzKg/5ioggxnk/wqmEIbjL2YOonB84HIxDs+0pHHbxAK0dPMBX4gF88HBNHlzvjfHg7ucBp+GoKLxF3ijKc2LUA/nofPNbN26LRPZhbU7XKrGVtdGtG8mSfJJCjNLZgcAau9PBqMk+Djvl3ci9nDtb8ADvXwQC8QiLfZtnV8s9WtU+jikSZNme7i4B1Qg3xcLRKolroGIZNaJ6THWX/p5gBHL8diBgG4GqPHQClTw1j306YvCMiGlU3bagOgIxvCaiiuUPlFkB60NlbqMVxkYzroJr/Zq8D1dwTVxdy8B1cCKuLjQCXRjXwRlwbQiDOmBH06Vhbzk69/ab3VqdA1l9pkxeaGs138rM1+9DWQUDI5BzWVa9l7PaoWLiwTH0jqDiWirurWsH7ziD6xZI/z2raCa/UfXotThsB7LNRf3KKg7fs4rmy6p9qoqOeUAyA72yivXS12SUh0OpC6WykJqCyoOg+JeEKUuxceJVrs5J1DywJiQMi2HGq5gIPM9QWXZX8kQufZwt0rA5UOePuDyFl0bv5YdU4A5bGtSSaCi5O1Aya9/Zzqh290fNuf0/aGH8kGCVn3P9sATbNevqIjldkbo74PsSyfUuJpI0t/88VJvh9u8bMPsL</diagram></mxfile>" style="background-color: rgb(255, 255, 255);"><defs/><g><rect x="121" y="20" width="78" height="70" fill="none" stroke="none" pointer-events="all"/><rect x="121" y="20" width="0" height="0" fill="none" stroke="#bac8d3" stroke-width="2" pointer-events="all"/><path d="M 187.86 79.93 L 187.86 67.2 L 121 67.2 L 121 79.93 Z" fill="#09555b" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 167.16 74.09 L 184.13 74.09" fill="none" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 199 56.59 L 132.14 56.59 L 121 67.2 L 187.86 67.2 Z" fill="#09555b" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 199 67.73 L 199 56.59 L 187.86 67.2 L 187.86 79.93 Z M 176.17 90 L 176.17 87.34 L 182.02 78.86 L 182.02 84.16 Z M 176.17 87.34 L 176.17 90 L 122.59 90 L 122.59 87.34 Z M 176.17 87.34 L 122.59 87.34 L 128.42 78.86 L 182.02 78.86 Z M 178.83 60.84 L 178.83 27.43 L 133.19 27.43 L 133.19 60.84 Z" fill="#09555b" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 136.92 34.84 C 136.92 31.14 140.1 30.61 140.1 30.61 C 140.1 30.61 167.69 30.61 170.87 30.61 C 175.12 30.61 175.12 34.84 175.12 34.84 C 175.12 34.84 175.12 51.29 175.12 53.93 C 175.12 56.07 171.4 57.12 171.4 57.12 C 171.4 57.12 143.81 57.12 140.63 57.12 C 137.97 57.12 136.92 53.93 136.92 53.93 Z" fill="none" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 186.79 20 L 140.63 20 L 133.19 27.43 L 178.83 27.43 Z" fill="#09555b" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 186.79 53.93 L 186.79 20 L 178.83 27.43 L 178.83 60.84 Z" fill="#09555b" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><rect x="1" y="220" width="50" height="75" fill="none" stroke="none" pointer-events="all"/><path d="M 1 295 L 1 227.92 L 40.79 227.92 L 40.79 295 Z M 12.84 220 L 1 227.92 L 40.79 227.92 L 51 220 Z M 51 284.43 L 51 220 L 40.79 227.92 L 40.79 295 Z" fill="#09555b" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 16.6 240.07 L 34.34 240.07 L 34.34 242.18 L 16.6 242.18 L 16.6 245.35 L 8.53 241.13 L 16.6 236.38 Z M 27.89 258.56 L 10.15 258.56 L 10.15 255.92 L 27.89 255.92 L 27.89 252.75 L 35.95 256.98 L 27.89 261.73 Z" fill="#ffffff" stroke="none" pointer-events="all"/><rect x="261" y="220" width="50" height="75" fill="none" stroke="none" pointer-events="all"/><path d="M 261 295 L 261 227.92 L 300.79 227.92 L 300.79 295 Z M 272.84 220 L 261 227.92 L 300.79 227.92 L 311 220 Z M 311 284.43 L 311 220 L 300.79 227.92 L 300.79 295 Z" fill="#09555b" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 276.6 240.07 L 294.34 240.07 L 294.34 242.18 L 276.6 242.18 L 276.6 245.35 L 268.53 241.13 L 276.6 236.38 Z M 287.89 258.56 L 270.15 258.56 L 270.15 255.92 L 287.89 255.92 L 287.89 252.75 L 295.95 256.98 L 287.89 261.73 Z" fill="#ffffff" stroke="none" pointer-events="all"/><path d="M 261 257.5 L 57.37 257.5" fill="none" stroke="#0b4d6a" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 52.12 257.5 L 59.12 254 L 57.37 257.5 L 59.12 261 Z" fill="#0b4d6a" stroke="#0b4d6a" stroke-miterlimit="10" pointer-events="all"/><path d="M 188.08 79.5 L 282.36 214.78" fill="none" stroke="#0b4d6a" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 285.36 219.08 L 278.49 215.34 L 282.36 214.78 L 284.23 211.34 Z" fill="#0b4d6a" stroke="#0b4d6a" stroke-miterlimit="10" pointer-events="all"/><path d="M 26 220 L 118.76 95.11" fill="none" stroke="#0b4d6a" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 121.89 90.9 L 120.53 98.6 L 118.76 95.11 L 114.91 94.43 Z" fill="#0b4d6a" stroke="#0b4d6a" stroke-miterlimit="10" pointer-events="all"/><path d="M 71 240 L 244.63 240" fill="none" stroke="#c75b57" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 249.88 240 L 242.88 243.5 L 244.63 240 L 242.88 236.5 Z" fill="#c75b57" stroke="#c75b57" stroke-miterlimit="10" pointer-events="all"/><path d="M 251 210 L 184.42 105.37" fill="none" stroke="#c75b57" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 181.6 100.94 L 188.31 104.97 L 184.42 105.37 L 182.41 108.73 Z" fill="#c75b57" stroke="#c75b57" stroke-miterlimit="10" pointer-events="all"/><path d="M 131 110 L 64.65 204.78" fill="none" stroke="#c75b57" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 61.64 209.08 L 62.79 201.34 L 64.65 204.78 L 68.52 205.36 Z" fill="#c75b57" stroke="#c75b57" stroke-miterlimit="10" pointer-events="all"/><rect x="140" y="0" width="40" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 38px; height: 1px; padding-top: 10px; margin-left: 141px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">Controller</div></div></div></foreignObject><text x="160" y="14" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">Contro...</text></switch></g><rect x="6" y="300" width="40" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 38px; height: 1px; padding-top: 310px; margin-left: 7px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">S1</div></div></div></foreignObject><text x="26" y="314" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">S1</text></switch></g><rect x="266" y="300" width="40" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 38px; height: 1px; padding-top: 310px; margin-left: 267px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">S2</div></div></div></foreignObject><text x="286" y="314" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">S2</text></switch></g></g><switch><g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/><a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank"><text text-anchor="middle" font-size="10px" x="50%" y="100%">Viewer does not support full SVG 1.1</text></a></switch></svg></p><p>则 LLDP 包的生命周期是:(以绿色箭头所示为例)</p><ol><li>由 Ryu 控制器构造,发送时对应 S2 交换机上某个端口的<code>PortData</code>实例更新时间戳。</li><li>被 S2 交换机转发给 S1 交换机。</li><li>被 S1 交换机发送给控制器,触发 Packet-In ,控制器收到后,可以解析出这个 LLDP 包的源端口是 S2 上的某个端口。</li></ol><p>RyuApp 是运行于控制器的程序,维护的<code>PortData</code>也都在控制器端。第一步的时间戳是控制器发送 LLDP 包的时间,第三步的“当前时间”是控制器收到包的时间。因此,用当前时间减去时间戳,得到的就是 LLDP 数据包经过三步所用的总时延。这个时延维护在<code>PortData</code>实例的<code>delay</code>属性里。</p><p>为了用上述方法更新<code>delay</code>属性,还需要改动<code>ryu/ryu/topology/switch.py</code>中的<code>Switches</code>类,在它的<code>lldp_packet_in_handler</code>方法中更新<code>self.ports</code><sup><a href="#1">[1]</a></sup><a name="back-1"></a>维护的所有端口。</p><p>用类似的方法可以得到反方向(红色箭头所示)的时延。</p><h2 id="估计往返时延-RTT">估计往返时延 RTT</h2><p>网络链路的时延是实时变化的,只能进行估计式的测量。</p><p>LLDP 数据包被控制器发给交换机后,转发一次(一跳)就会被再次发回控制器。在上方图示的过程中,往返两次转发的总时延可以表示为 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>t</mi><mrow><mi>l</mi><mi>l</mi><mi>d</mi><mi>p</mi><mn>1</mn></mrow></msub><mo>+</mo><msub><mi>t</mi><mrow><mi>l</mi><mi>l</mi><mi>d</mi><mi>p</mi><mn>2</mn></mrow></msub></mrow><annotation encoding="application/x-tex">t_{lldp1}+t_{lldp2}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.9012em;vertical-align:-0.2861em;"></span><span class="mord"><span class="mord mathnormal">t</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.01968em;">ll</span><span class="mord mathnormal mtight">d</span><span class="mord mathnormal mtight">p</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.2861em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.9012em;vertical-align:-0.2861em;"></span><span class="mord"><span class="mord mathnormal">t</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.01968em;">ll</span><span class="mord mathnormal mtight">d</span><span class="mord mathnormal mtight">p</span><span class="mord mtight">2</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.2861em;"><span></span></span></span></span></span></span></span></span></span> ,另一方面,这也是如下两项的和:</p><ul><li>交换机 S1 和 S2 之间的往返时延 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>R</mi><mi>T</mi><msub><mi>T</mi><mrow><mn>1</mn><mo separator="true">,</mo><mn>2</mn></mrow></msub></mrow><annotation encoding="application/x-tex">RTT_{1,2}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.9694em;vertical-align:-0.2861em;"></span><span class="mord mathnormal" style="margin-right:0.13889em;">RT</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.13889em;">T</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:-0.1389em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">1</span><span class="mpunct mtight">,</span><span class="mord mtight">2</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.2861em;"><span></span></span></span></span></span></span></span></span></span></li><li>控制器到两台交换机的往返时延 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>t</mi><mrow><mi>e</mi><mi>c</mi><mi>h</mi><mi>o</mi><mn>1</mn></mrow></msub><mo>+</mo><msub><mi>t</mi><mrow><mi>e</mi><mi>c</mi><mi>h</mi><mi>o</mi><mn>2</mn></mrow></msub></mrow><annotation encoding="application/x-tex">t_{echo1}+t_{echo2}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.7651em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal">t</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">ec</span><span class="mord mathnormal mtight">h</span><span class="mord mathnormal mtight">o</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.7651em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal">t</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">ec</span><span class="mord mathnormal mtight">h</span><span class="mord mathnormal mtight">o</span><span class="mord mtight">2</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span></li></ul><p>由此得到 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>R</mi><mi>T</mi><msub><mi>T</mi><mrow><mn>1</mn><mo separator="true">,</mo><mn>2</mn></mrow></msub><mo>=</mo><msub><mi>t</mi><mrow><mi>l</mi><mi>l</mi><mi>d</mi><mi>p</mi><mn>1</mn></mrow></msub><mo>+</mo><msub><mi>t</mi><mrow><mi>l</mi><mi>l</mi><mi>d</mi><mi>p</mi><mn>2</mn></mrow></msub><mo>−</mo><mo stretchy="false">(</mo><msub><mi>t</mi><mrow><mi>e</mi><mi>c</mi><mi>h</mi><mi>o</mi><mn>1</mn></mrow></msub><mo>+</mo><msub><mi>t</mi><mrow><mi>e</mi><mi>c</mi><mi>h</mi><mi>o</mi><mn>2</mn></mrow></msub><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">RTT_{1,2}=t_{lldp1}+t_{lldp2}-(t_{echo1}+t_{echo2})</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.9694em;vertical-align:-0.2861em;"></span><span class="mord mathnormal" style="margin-right:0.13889em;">RT</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.13889em;">T</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:-0.1389em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">1</span><span class="mpunct mtight">,</span><span class="mord mtight">2</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.2861em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.9012em;vertical-align:-0.2861em;"></span><span class="mord"><span class="mord mathnormal">t</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.01968em;">ll</span><span class="mord mathnormal mtight">d</span><span class="mord mathnormal mtight">p</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.2861em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.9012em;vertical-align:-0.2861em;"></span><span class="mord"><span class="mord mathnormal">t</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.01968em;">ll</span><span class="mord mathnormal mtight">d</span><span class="mord mathnormal mtight">p</span><span class="mord mtight">2</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.2861em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord"><span class="mord mathnormal">t</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">ec</span><span class="mord mathnormal mtight">h</span><span class="mord mathnormal mtight">o</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal">t</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">ec</span><span class="mord mathnormal mtight">h</span><span class="mord mathnormal mtight">o</span><span class="mord mtight">2</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mclose">)</span></span></span></span> ,可以用这个式子估计往返时延<sup><a href="#2">[2]</a></sup><a name="back-2"></a>(忽略了构造、解析、转发数据包的时间)。</p><p>OpenFlow 的 Echo Request/Reply 消息可以用于测量 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>t</mi><mrow><mi>e</mi><mi>c</mi><mi>h</mi><mi>o</mi><mn>1</mn></mrow></msub><mo separator="true">,</mo><msub><mi>t</mi><mrow><mi>e</mi><mi>c</mi><mi>h</mi><mi>o</mi><mn>2</mn></mrow></msub></mrow><annotation encoding="application/x-tex">t_{echo1},t_{echo2}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8095em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal">t</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">ec</span><span class="mord mathnormal mtight">h</span><span class="mord mathnormal mtight">o</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord"><span class="mord mathnormal">t</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">ec</span><span class="mord mathnormal mtight">h</span><span class="mord mathnormal mtight">o</span><span class="mord mtight">2</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span> 两个时延,具体方法是:周期性发送以时间戳作为数据的 Echo Request ,时间戳会被 Echo Reply 回送,与当前时间相减即得时延。</p><figure><div class="code-wrapper"><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token decorator annotation punctuation">@set_ev_cls</span><span class="token punctuation">(</span>ofp_event<span class="token punctuation">.</span>EventOFPEchoReply<span class="token punctuation">,</span> MAIN_DISPATCHER<span class="token punctuation">)</span><span class="token keyword">def</span> <span class="token function">_echo_reply_handler</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> ev<span class="token punctuation">)</span><span class="token punctuation">:</span> <span class="token keyword">try</span><span class="token punctuation">:</span> <span class="token comment"># evaluate the echo delay from controller to `ev.msg.datapath`</span> self<span class="token punctuation">.</span>echo_delay<span class="token punctuation">[</span>ev<span class="token punctuation">.</span>msg<span class="token punctuation">.</span>datapath<span class="token punctuation">.</span><span class="token builtin">id</span><span class="token punctuation">]</span> <span class="token operator">=</span> delay <span class="token keyword">except</span><span class="token punctuation">:</span> <span class="token keyword">return</span> <span class="token keyword">def</span> <span class="token function">_send_echo_request</span><span class="token punctuation">(</span>self<span class="token punctuation">)</span><span class="token punctuation">:</span> <span class="token comment"># run as a coroutine</span> <span class="token keyword">while</span> <span class="token boolean">True</span><span class="token punctuation">:</span> <span class="token keyword">for</span> each datapath <span class="token comment"># construct and send echo request</span> <span class="token comment"># sleep some time</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div></figure><h3 id="网络同步对数据的影响">网络同步对数据的影响</h3><p>应用运行在控制器上,但数据需要等待网络传输。LLDP 时延和 Echo 往返时延都需要等待数据包传回,任何一个为空都不能计算 RTT ;即使这些数据都就位,也未必正确。</p><p>控制器端调用<code>dp.send_msg</code>,会把相应数据包送入消息队列。如果大量数据包同时发送,有些数据包可能等待一段时间,但附加在数据包中的时间戳是<strong>送入队列</strong>的时间,并非实际发出的时间。于是 Echo 往返时延有可能异常地长——例如数十毫秒——甚至比测出的 LLDP 时延还要长。依此计算,网络的拓扑图中将出现负权边,即到达在发送之前,这显然不可能。</p><p>那么,一方面需要让 Echo Request 较为均匀地发送,避免集中;另一方面需要检查算出的链路往返时延是否为负,避免构造出负权边。</p><h3 id="协程调度问题">协程调度问题</h3><p>实验是在 ARPANET 网络拓扑上进行,一共有三个协程:</p><ul><li>消息处理协程(由 App Manager 启动的主协程)</li><li>更新拓扑并下发生成树的协程(解决 ARP 广播风暴)</li><li>发送 Echo Request 获取时延的协程</li></ul><p>每个协程都要等待其他协程主动让出 CPU 才能执行,因此任何一个协程必须控制自己的执行时间,否则其他协程可能不能正常执行<sup><a href="#3">[3]</a></sup><a name="back-3"></a>。对于发送 Echo Request 的协程,每发送一轮之后应该<code>hub.sleep</code>主动休眠足够长的时间,所以最后的<code>_send_echo_request</code>是这样:</p><figure><div class="code-wrapper"><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">_send_echo_request</span><span class="token punctuation">(</span>self<span class="token punctuation">)</span><span class="token punctuation">:</span> <span class="token keyword">while</span> <span class="token boolean">True</span><span class="token punctuation">:</span> <span class="token keyword">for</span> dpid <span class="token keyword">in</span> self<span class="token punctuation">.</span>id2dp<span class="token punctuation">:</span> <span class="token comment"># construct and send echo request</span> hub<span class="token punctuation">.</span>sleep<span class="token punctuation">(</span><span class="token number">0.1</span><span class="token punctuation">)</span> hub<span class="token punctuation">.</span>sleep<span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div></figure><h2 id="说明">说明</h2><p>SDN 课内实验共有四次,第一次的“自学习交换机”参考资料多如牛毛,我自不必赘述;最后一次实验介绍的 VeriFlow 工具,精华在于论文<sup><a href="#4">[4]</a></sup><a name="back-4"></a>,对此我只有献丑之技,没有剖析之能,亦不再记录。</p><p>这几篇文章大多来自作业和实验报告,以提取思路、增进理解为主,增删过一些内容。时间所限,我并没有太多时间将实验的内容和相关技术资料认真整理成笔记,只借此简单提取、留档;也希望可以作为日后同学粗疏的参考。若有所助力,不胜荣幸。</p><p>Ryu 的资料主要有两个来源,均为官方维护人员编写:</p><ul><li><p>Read The Docs 文档,介绍 Ryu 中关于应用、协议 API ,但对于源码结构、协程调度没有讲解。</p><p><a href="https://ryu.readthedocs.io/en/latest/index.html">https://ryu.readthedocs.io/en/latest/index.html</a></p></li><li><p>Ryu Book ,讲解 Ryu 如何使用、如何工作,详尽全面,只是不太方便查。</p><p><a href="http://osrg.github.io/ryu-book/en/Ryubook.pdf">http://osrg.github.io/ryu-book/en/Ryubook.pdf</a></p></li></ul><h2 id="注释">注释</h2><p><a name="1">[1]</a><a href="#back-1">^</a>这是一个键为<code>Port</code>实例,值为<code>PortData</code>实例的字典。</p><p><a name="2">[2]</a><a href="#back-2">^</a>思路来自实验指导书。</p><p><a name="3">[3]</a><a href="#back-3">^</a>我的程序因此出现了各种奇怪的 Bug ,包括但不限于:时而正常工作时而卡在发送 Echo Request 的过程中、根据 Echo Reply 计算的时延很大(很小)、始终不能向所有的交换机发出 Echo Request ,等等。不外乎是影响了消息处理和更新拓扑的协程正常工作。</p><p><a name="4">[4]</a><a href="#back-4">^</a><a href="https://www.usenix.org/system/files/conference/nsdi13/nsdi13-final100.pdf">Ahmed Khurshid, Xuan Zou, Wenxuan Zhou, Matthew Caesar, P. Brighten Godfrey. VeriFlow: Verifying Network-Wide Invariants in Real Time. In NSDI</a></p>]]></content:encoded>
<category domain="https://greyishsong.ink/categories/%E7%BC%96%E7%A8%8B/">编程</category>
<category domain="https://greyishsong.ink/tags/SDN/">SDN</category>
<category domain="https://greyishsong.ink/tags/%E8%BD%AF%E4%BB%B6%E5%AE%9A%E4%B9%89%E7%BD%91%E7%BB%9C/">软件定义网络</category>
<category domain="https://greyishsong.ink/tags/Mininet/">Mininet</category>
<category domain="https://greyishsong.ink/tags/Ryu/">Ryu</category>
<comments>https://greyishsong.ink/SDN%E5%AE%9E%E9%AA%8C%EF%BC%88%E5%9B%9B%EF%BC%89%EF%BC%9A%E6%B5%8B%E5%AE%9A%E9%93%BE%E8%B7%AF%E6%97%B6%E5%BB%B6/#disqus_thread</comments>
</item>
<item>
<title>SDN实验(三):构造生成树进行ARP广播</title>
<link>https://greyishsong.ink/SDN%E5%AE%9E%E9%AA%8C%EF%BC%88%E4%B8%89%EF%BC%89%EF%BC%9A%E6%9E%84%E9%80%A0%E7%94%9F%E6%88%90%E6%A0%91%E8%BF%9B%E8%A1%8CARP%E5%B9%BF%E6%92%AD/</link>
<guid>https://greyishsong.ink/SDN%E5%AE%9E%E9%AA%8C%EF%BC%88%E4%B8%89%EF%BC%89%EF%BC%9A%E6%9E%84%E9%80%A0%E7%94%9F%E6%88%90%E6%A0%91%E8%BF%9B%E8%A1%8CARP%E5%B9%BF%E6%92%AD/</guid>
<pubDate>Fri, 13 Aug 2021 03:14:19 GMT</pubDate>
<description><p>在传统网络中,如果存在环路,可以用生成树协议解决广播风暴问题。生成树协议运行在交换机上,各交换机平等协商产生生成树根和通往根的路径,该过程是分布的。</p>
<p>SDN 的控制器可以获得全局的拓扑信息,直接构造整个生成树,再通过流表改变交换机的(广播)转发行为,进而避免广播风暴。</p></description>
<content:encoded><![CDATA[<p>在传统网络中,如果存在环路,可以用生成树协议解决广播风暴问题。生成树协议运行在交换机上,各交换机平等协商产生生成树根和通往根的路径,该过程是分布的。</p><p>SDN 的控制器可以获得全局的拓扑信息,直接构造整个生成树,再通过流表改变交换机的(广播)转发行为,进而避免广播风暴。</p><span id="more"></span><h2 id="识别广播">识别广播</h2><p>数据包的广播可以通过网络协议中的特定保留地址实现。ARP 协议建立在以太网协议之上,一个 ARP 请求包的目的 MAC 地址是 <code>ff:ff:ff:ff:ff:ff</code> 。</p><p>调用 Ryu API 构造 <code>eth_dst</code> 匹配域为广播 MAC 地址的流表项,即可规定交换机对 ARP 广播数据的转发行为。</p><h2 id="改变广播行为">改变广播行为</h2><p>广播是将数据包传遍整个网络,朴素思路是每个交换机收到数据包后向所有其他端口转发,但存在环路就会产生广播风暴。若一个节点只将数据包转发给自己在生成树上的邻居,则转发过程不存在环路和广播风暴的问题。证明很容易:</p><p>规定首个发出数据包的节点为根(生成树是无根树),则整个转发过程等价于对生成树的一次广度优先遍历,因此必然是不重不漏的。</p><p>构建一个模仿 ARPANET<sup><a href="#1">[1]</a></sup><a name="back-1"></a> 的网络:</p><img src="/SDN%E5%AE%9E%E9%AA%8C%EF%BC%88%E4%B8%89%EF%BC%89%EF%BC%9A%E6%9E%84%E9%80%A0%E7%94%9F%E6%88%90%E6%A0%91%E8%BF%9B%E8%A1%8CARP%E5%B9%BF%E6%92%AD/arpanet.png" class=""><p>在这个网络中,每个交换机上连接有一台终端,任意终端之间都可以通信。对于每个交换机,在生成树上进行广播的行为有两种:</p><ul><li>若收到终端发来的数据包,则发给所有相邻的交换机;</li><li>若收到交换机发来的数据包,则发给所有其他相邻交换机。</li></ul><p>实现这些行为需要控制器获取全局拓扑、构造生成树、下发流表项。</p><h3 id="获取全局拓扑">获取全局拓扑</h3><p>这只是一个<strong>小实验</strong>,获取拓扑最简单的方法是直接用</p><figure><div class="code-wrapper"><pre class="line-numbers language-python" data-language="python"><code class="language-python">hosts <span class="token operator">=</span> get_all_host<span class="token punctuation">(</span>self<span class="token punctuation">)</span>switches <span class="token operator">=</span> get_all_switch<span class="token punctuation">(</span>self<span class="token punctuation">)</span>links <span class="token operator">=</span> get_all_link<span class="token punctuation">(</span>self<span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre></div></figure><p>获得所有的终端、交换机和链路。以交换机作为节点、链路作为边<sup><a href="#2">[2]</a></sup><a name="back-2"></a>建图,构造生成树。数据包的转发过程不涉及终端,因此终端并不在图中,但需要记录每个交换机连接终端的端口,以区别于连接交换机的端口。</p><figure><div class="code-wrapper"><pre class="line-numbers language-python" data-language="python"><code class="language-python">new_g <span class="token operator">=</span> Graph<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token keyword">for</span> switch <span class="token keyword">in</span> switches<span class="token punctuation">:</span> new_g<span class="token punctuation">.</span>add_node<span class="token punctuation">(</span>switch<span class="token punctuation">.</span>dp<span class="token punctuation">.</span><span class="token builtin">id</span><span class="token punctuation">)</span> <span class="token comment"># map dpid to Datapath object</span> self<span class="token punctuation">.</span>id2dp<span class="token punctuation">[</span>switch<span class="token punctuation">.</span>dp<span class="token punctuation">.</span><span class="token builtin">id</span><span class="token punctuation">]</span> <span class="token operator">=</span> switch<span class="token punctuation">.</span>dp<span class="token keyword">for</span> host <span class="token keyword">in</span> hosts<span class="token punctuation">:</span> <span class="token comment"># record the port connected to host</span> self<span class="token punctuation">.</span>hostport<span class="token punctuation">[</span>host<span class="token punctuation">.</span>port<span class="token punctuation">.</span>dpid<span class="token punctuation">]</span> <span class="token operator">=</span> host<span class="token punctuation">.</span>port<span class="token punctuation">.</span>port_no <span class="token comment"># map host's MAC address to the dpid it connected</span> self<span class="token punctuation">.</span>mac2dpid<span class="token punctuation">[</span>host<span class="token punctuation">.</span>mac<span class="token punctuation">]</span> <span class="token operator">=</span> host<span class="token punctuation">.</span>port<span class="token punctuation">.</span>dpid<span class="token keyword">for</span> link <span class="token keyword">in</span> links<span class="token punctuation">:</span> <span class="token comment"># an edge is described as (src_dpid, src_port, dst_dpid, dst_port)</span> new_g<span class="token punctuation">.</span>add_edge<span class="token punctuation">(</span> link<span class="token punctuation">.</span>src<span class="token punctuation">.</span>dpid<span class="token punctuation">,</span> link<span class="token punctuation">.</span>src<span class="token punctuation">.</span>port_no<span class="token punctuation">,</span> link<span class="token punctuation">.</span>dst<span class="token punctuation">.</span>dpid<span class="token punctuation">,</span> link<span class="token punctuation">.</span>dst<span class="token punctuation">.</span>port_no <span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div></figure><p>随着网络设备的增减,拓扑信息会发生改变,控制器维护的拓扑信息也应改变。直接的办法是:周期性获取拓扑,和消息处理相互独立。可以用 Ryu 提供的 <code>ryu.lib.hub</code> 类,它是对 eventlet 的封装,可以分出一个协程负责获取拓扑信息。具体实现分两部分:</p><ul><li><p>类初始化时创建协程:</p><figure><div class="code-wrapper"><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">__init__</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> <span class="token operator">*</span>args<span class="token punctuation">,</span> <span class="token operator">**</span>kwargs<span class="token punctuation">)</span><span class="token punctuation">:</span> self<span class="token punctuation">.</span>topo_thread <span class="token operator">=</span> hub<span class="token punctuation">.</span>spawn<span class="token punctuation">(</span>self<span class="token punctuation">.</span>_update_topology<span class="token punctuation">)</span> <span class="token comment"># ...</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre></div></figure></li><li><p>周期性执行:</p><figure><div class="code-wrapper"><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">_update_topology</span><span class="token punctuation">(</span>self<span class="token punctuation">)</span><span class="token punctuation">:</span> <span class="token keyword">while</span> <span class="token boolean">True</span><span class="token punctuation">:</span> <span class="token comment"># update topoloty information</span> <span class="token comment"># generate spanning tree</span> <span class="token comment"># add flow table entries</span> hub<span class="token punctuation">.</span>sleep<span class="token punctuation">(</span><span class="token number">5</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div></figure></li></ul><div class="note note-warning"> <p>运行<code>_update_topology</code>的协程和处理消息的协程是属于<strong>同一线程</strong>的。当更新拓扑信息的协程放弃执行(调用<code>hub.sleep</code>)时,应用才能处理消息。因此,执行间隔不能太短,否则更新拓扑信息将占用绝大多数执行时间,导致不能正常处理消息——比如处理速度低于到达速度,消息队列爆掉。</p> </div><h3 id="构造生成树">构造生成树</h3><p>不考虑边权问题,那么生成树的构造就很随便了。这里用邻接表存图,用简化的 Kruskal 算法(不排序)来构造一棵生成树:</p><figure><div class="code-wrapper"><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">class</span> <span class="token class-name">Edge</span><span class="token punctuation">:</span> <span class="token comment"># (from_node, from_port, to_node, to_port, weight)</span><span class="token keyword">class</span> <span class="token class-name">UnionFindSet</span><span class="token punctuation">:</span> <span class="token keyword">def</span> <span class="token function">__init__</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> nodes<span class="token punctuation">:</span> <span class="token builtin">list</span><span class="token punctuation">)</span><span class="token punctuation">:</span> <span class="token comment"># initialize each node's father as itself</span> <span class="token keyword">def</span> <span class="token function">find</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> x<span class="token punctuation">)</span><span class="token punctuation">:</span> <span class="token comment"># find which set that `x` belongs to</span> <span class="token keyword">def</span> <span class="token function">union</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> x<span class="token punctuation">,</span> y<span class="token punctuation">)</span><span class="token punctuation">:</span> <span class="token comment"># union the two sets that `x` and `y` belong to</span><span class="token keyword">class</span> <span class="token class-name">Graph</span><span class="token punctuation">:</span> <span class="token keyword">def</span> <span class="token function">__init__</span><span class="token punctuation">(</span>self<span class="token punctuation">)</span><span class="token punctuation">:</span> self<span class="token punctuation">.</span>nodes <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span> self<span class="token punctuation">.</span>edges <span class="token operator">=</span> defaultdict<span class="token punctuation">(</span><span class="token keyword">lambda</span><span class="token punctuation">:</span> <span class="token builtin">list</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token keyword">def</span> <span class="token function">add_node</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> node_id<span class="token punctuation">:</span> <span class="token builtin">int</span><span class="token punctuation">)</span><span class="token punctuation">:</span> self<span class="token punctuation">.</span>nodes<span class="token punctuation">.</span>append<span class="token punctuation">(</span>node_id<span class="token punctuation">)</span> <span class="token keyword">def</span> <span class="token function">add_edge</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> from_node <span class="token punctuation">:</span> <span class="token builtin">int</span><span class="token punctuation">,</span> from_port <span class="token punctuation">:</span> <span class="token builtin">int</span><span class="token punctuation">,</span> to_node <span class="token punctuation">:</span> <span class="token builtin">int</span><span class="token punctuation">,</span> to_port <span class="token punctuation">:</span> <span class="token builtin">int</span><span class="token punctuation">,</span> weight <span class="token punctuation">:</span> <span class="token builtin">float</span> <span class="token operator">=</span> <span class="token number">1</span> <span class="token punctuation">)</span><span class="token punctuation">:</span> self<span class="token punctuation">.</span>edges<span class="token punctuation">[</span>from_node<span class="token punctuation">]</span><span class="token punctuation">.</span>append<span class="token punctuation">(</span> Edge<span class="token punctuation">(</span>from_port<span class="token punctuation">,</span> to_node<span class="token punctuation">,</span> to_port<span class="token punctuation">,</span> weight<span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token keyword">def</span> <span class="token function">spanning_tree</span><span class="token punctuation">(</span>self<span class="token punctuation">)</span><span class="token punctuation">:</span> <span class="token comment"># spanning tree</span> st <span class="token operator">=</span> Graph<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">if</span> <span class="token builtin">len</span><span class="token punctuation">(</span>self<span class="token punctuation">.</span>nodes<span class="token punctuation">)</span> <span class="token operator">==</span> <span class="token number">0</span><span class="token punctuation">:</span> <span class="token keyword">return</span> st ufset <span class="token operator">=</span> UnionFindSet<span class="token punctuation">(</span>self<span class="token punctuation">.</span>nodes<span class="token punctuation">)</span> <span class="token keyword">for</span> node <span class="token keyword">in</span> self<span class="token punctuation">.</span>nodes<span class="token punctuation">:</span> st<span class="token punctuation">.</span>add_node<span class="token punctuation">(</span>node<span class="token punctuation">)</span> <span class="token keyword">for</span> node <span class="token keyword">in</span> self<span class="token punctuation">.</span>nodes<span class="token punctuation">:</span> <span class="token keyword">for</span> edge <span class="token keyword">in</span> self<span class="token punctuation">.</span>edges<span class="token punctuation">[</span>node<span class="token punctuation">]</span><span class="token punctuation">:</span> <span class="token keyword">if</span> ufset<span class="token punctuation">.</span>find<span class="token punctuation">(</span>node<span class="token punctuation">)</span> <span class="token operator">!=</span> ufset<span class="token punctuation">.</span>find<span class="token punctuation">(</span>edge<span class="token punctuation">.</span>to_node<span class="token punctuation">)</span><span class="token punctuation">:</span> st<span class="token punctuation">.</span>add_edge<span class="token punctuation">(</span> node<span class="token punctuation">,</span> edge<span class="token punctuation">.</span>from_port<span class="token punctuation">,</span> edge<span class="token punctuation">.</span>to_node<span class="token punctuation">,</span> edge<span class="token punctuation">.</span>to_port <span class="token punctuation">)</span> st<span class="token punctuation">.</span>add_edge<span class="token punctuation">(</span> edge<span class="token punctuation">.</span>to_node<span class="token punctuation">,</span> edge<span class="token punctuation">.</span>to_port<span class="token punctuation">,</span> node<span class="token punctuation">,</span> edge<span class="token punctuation">.</span>from_port <span class="token punctuation">)</span> ufset<span class="token punctuation">.</span>union<span class="token punctuation">(</span>node<span class="token punctuation">,</span> edge<span class="token punctuation">.</span>to_node<span class="token punctuation">)</span> <span class="token keyword">return</span> st<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div></figure><h3 id="表达为流表项">表达为流表项</h3><p>设一个节点(交换机)在生成树上有 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>k</mi></mrow><annotation encoding="application/x-tex">k</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span></span></span></span> 个邻居,则一共有 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>k</mi><mo>+</mo><mn>1</mn></mrow><annotation encoding="application/x-tex">k+1</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.7778em;vertical-align:-0.0833em;"></span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.6444em;"></span><span class="mord">1</span></span></span></span> 条流表项需要下发到这个节点:</p><ul><li>前 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>k</mi></mrow><annotation encoding="application/x-tex">k</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span></span></span></span> 条:任意邻居发来广播包时,转发给其余 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>k</mi><mo>−</mo><mn>1</mn></mrow><annotation encoding="application/x-tex">k-1</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.7778em;vertical-align:-0.0833em;"></span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.6444em;"></span><span class="mord">1</span></span></span></span> 个邻居;</li><li>最后一条:终端发来广播包时,转发给所有邻居。</li></ul><figure><div class="code-wrapper"><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token comment"># here `node` means dpid</span><span class="token keyword">for</span> node <span class="token keyword">in</span> self<span class="token punctuation">.</span>st<span class="token punctuation">.</span>nodes<span class="token punctuation">:</span> <span class="token keyword">if</span> self<span class="token punctuation">.</span>hostport<span class="token punctuation">[</span>node<span class="token punctuation">]</span> <span class="token keyword">is</span> <span class="token boolean">None</span><span class="token punctuation">:</span> <span class="token keyword">return</span> dp <span class="token operator">=</span> self<span class="token punctuation">.</span>id2dp<span class="token punctuation">[</span>node<span class="token punctuation">]</span> parser <span class="token operator">=</span> dp<span class="token punctuation">.</span>ofproto_parser <span class="token keyword">for</span> edge <span class="token keyword">in</span> self<span class="token punctuation">.</span>st<span class="token punctuation">.</span>edges<span class="token punctuation">[</span>node<span class="token punctuation">]</span><span class="token punctuation">:</span> <span class="token comment"># when a broadcast packet comes from a neighbor, send it to</span> <span class="token comment"># all OTHER neighbors and the host</span> <span class="token keyword">match</span> <span class="token operator">=</span> parser<span class="token punctuation">.</span>OFPMatch<span class="token punctuation">(</span> in_port <span class="token operator">=</span> edge<span class="token punctuation">.</span>from_port<span class="token punctuation">,</span> eth_dst <span class="token operator">=</span> <span class="token string">'ff:ff:ff:ff:ff:ff'</span> <span class="token punctuation">)</span> actions <span class="token operator">=</span> <span class="token punctuation">[</span> parser<span class="token punctuation">.</span>OFPActionOutput<span class="token punctuation">(</span>e<span class="token punctuation">.</span>from_port<span class="token punctuation">)</span> <span class="token keyword">for</span> e <span class="token keyword">in</span> self<span class="token punctuation">.</span>st<span class="token punctuation">.</span>edges<span class="token punctuation">[</span>node<span class="token punctuation">]</span> <span class="token keyword">if</span> e<span class="token punctuation">.</span>from_port <span class="token operator">!=</span> edge<span class="token punctuation">.</span>from_port <span class="token punctuation">]</span> actions<span class="token punctuation">.</span>append<span class="token punctuation">(</span>parser<span class="token punctuation">.</span>OFPActionOutput<span class="token punctuation">(</span>self<span class="token punctuation">.</span>hostport<span class="token punctuation">[</span>node<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span> self<span class="token punctuation">.</span>add_flow<span class="token punctuation">(</span>dp<span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token keyword">match</span><span class="token punctuation">,</span> actions<span class="token punctuation">)</span> <span class="token comment"># when a broadcast packet comes from the host, send it to</span> <span class="token comment"># all neighbors</span> <span class="token keyword">match</span> <span class="token operator">=</span> parser<span class="token punctuation">.</span>OFPMatch<span class="token punctuation">(</span> in_port <span class="token operator">=</span> self<span class="token punctuation">.</span>hostport<span class="token punctuation">[</span>node<span class="token punctuation">]</span><span class="token punctuation">,</span> eth_dst <span class="token operator">=</span> <span class="token string">'ff:ff:ff:ff:ff:ff'</span> <span class="token punctuation">)</span> actions <span class="token operator">=</span> <span class="token punctuation">[</span> parser<span class="token punctuation">.</span>OFPActionOutput<span class="token punctuation">(</span>e<span class="token punctuation">.</span>from_port<span class="token punctuation">)</span> <span class="token keyword">for</span> e <span class="token keyword">in</span> self<span class="token punctuation">.</span>st<span class="token punctuation">.</span>edges<span class="token punctuation">[</span>node<span class="token punctuation">]</span> <span class="token punctuation">]</span> self<span class="token punctuation">.</span>add_flow<span class="token punctuation">(</span>dp<span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token keyword">match</span><span class="token punctuation">,</span> actions<span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div></figure><h2 id="说明">说明</h2><p>这是我对实验的一点小改动,和老师给出的两种思路并不相同。另两种解决 ARP 广播风暴的方法是:</p><ul><li>由控制器作 ARP 转发:控制器记录每个交换机连接终端的端口,让所有的 ARP 请求都触发 Packet-In ,再将这些请求一步发给上述端口。相当于不经过中间转发,直接广播到所有终端。</li><li>以<code>(dpid, mac, dst_ip)</code>作为键,<code>port</code>作为值维护 ARP 广播数据包的转发记录。每个交换机收到广播包时检查对应的键,若值不存在则记录值并洪泛,值存在则丢弃数据包。</li></ul><h2 id="注释">注释</h2><p><a name="1">[1]</a><a href="#back-1">^</a>历史上第一个网络,今天互联网的雏形。</p><p><a name="2">[2]</a><a href="#back-2">^</a>用<code>get_all_links</code>获取到的链路是单向链路,两台交换机之间的链路会以一来一去的形式出现两次。</p>]]></content:encoded>
<category domain="https://greyishsong.ink/categories/%E7%BC%96%E7%A8%8B/">编程</category>
<category domain="https://greyishsong.ink/tags/SDN/">SDN</category>
<category domain="https://greyishsong.ink/tags/%E8%BD%AF%E4%BB%B6%E5%AE%9A%E4%B9%89%E7%BD%91%E7%BB%9C/">软件定义网络</category>
<category domain="https://greyishsong.ink/tags/Mininet/">Mininet</category>
<category domain="https://greyishsong.ink/tags/Ryu/">Ryu</category>
<comments>https://greyishsong.ink/SDN%E5%AE%9E%E9%AA%8C%EF%BC%88%E4%B8%89%EF%BC%89%EF%BC%9A%E6%9E%84%E9%80%A0%E7%94%9F%E6%88%90%E6%A0%91%E8%BF%9B%E8%A1%8CARP%E5%B9%BF%E6%92%AD/#disqus_thread</comments>
</item>
<item>
<title>SDN实验(二):动态变更转发路径</title>
<link>https://greyishsong.ink/SDN%E5%AE%9E%E9%AA%8C%EF%BC%88%E4%BA%8C%EF%BC%89%EF%BC%9A%E5%8A%A8%E6%80%81%E5%8F%98%E6%9B%B4%E8%BD%AC%E5%8F%91%E8%B7%AF%E5%BE%84/</link>
<guid>https://greyishsong.ink/SDN%E5%AE%9E%E9%AA%8C%EF%BC%88%E4%BA%8C%EF%BC%89%EF%BC%9A%E5%8A%A8%E6%80%81%E5%8F%98%E6%9B%B4%E8%BD%AC%E5%8F%91%E8%B7%AF%E5%BE%84/</guid>
<pubDate>Mon, 02 Aug 2021 10:38:27 GMT</pubDate>
<description><p>原计划一面实验,一面总结,然而直到七月底都杂务缠身,只好现在才腾出手来,做一点回忆式的记录。</p>
<p>这一篇在于实现软件控制数据包的转发路径,路径的动态变化由控制器决定,而不是由交换机决定。控制器控制转发路径的手段包括两个方面:一是处理 OpenFlow 的 Packet-In 消息,二是下发流表项。此处正好涉及 OpenFlow 协议下控制器和网络的交互方式(控制平面),即收发消息(Message)。</p></description>
<content:encoded><![CDATA[<p>原计划一面实验,一面总结,然而直到七月底都杂务缠身,只好现在才腾出手来,做一点回忆式的记录。</p><p>这一篇在于实现软件控制数据包的转发路径,路径的动态变化由控制器决定,而不是由交换机决定。控制器控制转发路径的手段包括两个方面:一是处理 OpenFlow 的 Packet-In 消息,二是下发流表项。此处正好涉及 OpenFlow 协议下控制器和网络的交互方式(控制平面),即收发消息(Message)。</p><span id="more"></span><h2 id="控制器与交换机的通信">控制器与交换机的通信</h2><h3 id="SDN-中的控制器和交换机">SDN 中的控制器和交换机</h3><p>从 SDN 网络的角度,控制器和交换机是一些独立的设备。其中控制器的可编程空间大,可以视作通用计算机;而交换机比较刻板,只能按规则首发一些固定的消息,并按照流表匹配的结果转发数据包。</p><p>在一个传统的网络中,典型交换机会固定地将 A 网口的数据转发到 B (C, D, ...) 网口,是二层设备;典型的路由器则维护有路由表,能部分或全局地感知网络的拓扑信息,按照数据包的目的端决定转发的端口,是三层设备, SDN 网络则不然。在 OpenFlow 协议中,“交换机”的称呼更多是沿用习惯,实际上它兼具传统网络中交换机和路由器的特性。从本文的视角看, OpenFlow 交换机的特点是:</p><ul><li>不了解、不保存全局拓扑信息。</li><li>能够解读数据包的头部,获知其二层、三层协议的控制信息,决定转发端口。</li><li>存储有<strong>流表</strong>,每一项分为<strong>匹配域</strong> <em>Match Field</em> 和<strong>动作</strong> <em>Action</em> 两部分。转发行为依赖于数据包头的信息与流表项的匹配结果,对数据包执行最先匹配成功的流表项所记录的动作。</li></ul><p>在 OpenFlow 协议中,逻辑交换机亦称 Datapath ,以 Datapath ID 作为唯一标识。</p><p>交换机启动时,流表是空的,控制器初始化时一般会向所有交换机下发一条流表项:发往控制器<sup><a href="#1">[1]</a></sup>。这一项的所有匹配域都是通配 (wild card) ,因此最初交换机唯一会做的就是把数据包转交给控制器。</p><h3 id="OpenFlow-的-Packet-In-消息">OpenFlow 的 Packet-In 消息</h3><p>OpenFlow 协议下,控制器与交换机之间的交互都依赖消息传递,不同的消息代表不同的控制信息,可以携带不同的数据。当交换机将数据包发给控制器时,采用的形式就是一个 Packet-In 消息。对于 Ryu 控制器,这个消息的格式如下<sup><a href="#2">[2]</a></sup>:</p><table><thead><tr><th>Attribute</th><th>Description</th></tr></thead><tbody><tr><td>buffer_id</td><td>ID assigned by datapath</td></tr><tr><td>total_len</td><td>Full length of frame</td></tr><tr><td>reason</td><td>Reason packet is being sent.<br>OFPR_NO_MATCH<br>OFPR_ACTION<br>OFPR_INVALID_TTL</td></tr><tr><td>table_id</td><td>ID of the table that was looked up</td></tr><tr><td>cookie</td><td>Cookie of the flow entry that was looked up</td></tr><tr><td>match</td><td>Instance of <code>OFPMatch</code></td></tr><tr><td>data</td><td>Ethernet frame</td></tr></tbody></table><p>本次实验中,只关注<code>reason</code>, <code>match</code>, <code>data</code>域。交换机上流表匹配失败,则<code>reason</code>域为 OFPR_NO_MATCH 。实验并不引入其他原因的 Packet-In ,进而可以直接认为只要是 Packet-In 就是流表匹配失败,不检查<code>reason</code>域。</p><h3 id="Ryu-控制器消息处理">Ryu 控制器消息处理</h3><p>OpenFlow 的消息主要分成三类:</p><ul><li>控制器到交换机消息 <em>Controller-to-Switch Messages</em>:这是控制器主动发送给交换机的消息,最常见的就是修改流表项的 Modify Flow Entry 。</li><li>异步消息 <em>Asynchronous Messages</em>:这是交换机发给控制器的消息。</li><li>对称消息 <em>Symmetric Messages</em>:可以互发的消息,例如 Hello, Echo Request/Reply 。</li></ul><p>Packet-In 属于异步消息,此类消息是异步处理的。</p><p>Ryu 控制器将所有到来的消息放入消息队列,每次取出一个消息调用处理函数,处理消息时可以继续接收消息。每次<code>ryu-manager</code>加载一个 Ryu 应用,它就启动一个<strong>协程</strong><sup><a href="#3">[3]</a></sup>运行消息循环,开始接收消息。</p><p>继承<code>RyuApp</code>基类,后,对方法使用<code>set_ev_cls</code>装饰器可以注册其为消息的处理函数,这个方法只需要一个参数<code>ev</code>,即事件对象。</p><figure><div class="code-wrapper"><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token decorator annotation punctuation">@set_ev_cls</span><span class="token punctuation">(</span>ofp_event<span class="token punctuation">.</span>EventOFPPacketIn<span class="token punctuation">,</span> MAIN_DISPATCHER<span class="token punctuation">)</span><span class="token keyword">def</span> <span class="token function">_packet_in_handler</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> ev<span class="token punctuation">)</span><span class="token punctuation">:</span> msg <span class="token operator">=</span> ev<span class="token punctuation">.</span>msg datapath <span class="token operator">=</span> msg<span class="token punctuation">.</span>datapath ofproto <span class="token operator">=</span> datapath<span class="token punctuation">.</span>ofproto parser <span class="token operator">=</span> datapath<span class="token punctuation">.</span>ofproto_parser<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div></figure><p><code>ev.msg</code>对应 OpenFlow 协议中的 Message ,不同在于多了描述交换机实例的<code>datapath</code>和描述 OpenFlow 协议的<code>ofproto</code>属性。如果希望控制交换机发送数据包,就要用<code>datapath.send_msg</code>方法。</p><h2 id="实现切换路径">实现切换路径</h2><p>建立一个简单的拓扑:</p><p><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="710px" height="287px" viewBox="-0.5 -0.5 710 287" content="<mxfile host="Electron" modified="2021-08-08T05:36:35.251Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.6.13 Chrome/89.0.4389.128 Electron/12.0.7 Safari/537.36" etag="_Wjk401kU-2ZceZNA98j" version="14.6.13" type="device"><diagram id="_PdI4dcSc9nUDlH0FPhw" name="第 1 页">7Zpdc5s4FIZ/jS/LAOLLl/5Im9nZ7mTGF22vMgQU0BQQFXKM99evAMmASGtiO0C7+CLhHIkjofeRBAcWYBPnn4ibhp+xD6OFrvr5AmwXum47OvtbOI6Vw9TUyhEQ5FcurXbs0L+QO0W1PfJh1qpIMY4oSttODycJ9GjL5xKCD+1qzzhqt5q6Aew4dp4bdb1fkE9D7tWsZV1wD1EQ8qYd3a4KYldU5leSha6PDw0XuFuADcGYVkdxvoFRMXZiXKrzPv6k9NQxAhPa54TV0+dk+axRknwNtODHw19YJR94lBc32vML5p2lRzECrN9pcRjnQaGw4qHMw0p2QNQLYaYEMIEEeY8ZfqaVcwHWIY0jdorGDlOMEgrJ3QvrZsZ9vpuFsOiWyoxnFEUbHGFSNgfUpWmaa+bPKMHfYaNkvdo4W3AqEXKwgVy/QEIRE+1v9wlGDzhDFOGElT1hSnHcqLCKUFAUUJwyr8stDxZdZA68pxFKWKMCprKDOKGNbtyVP+bvCiBGkzUG84aLC/IJ4hhScmRVeOkJ86Owzco+1KyZvErYoMzm1VxOd3CKXAPADjgDb+BBn3kYkwcg8aAJeywewMzDmDwYxsR4MGYexuTBNCa2X5gzD6PuF3abB6COvD5Yl/Lg4Tjds0HMHt3Ef0wZEmkIiRtlSurNQPRfIKT1Qe/yYDuv8KC+Ew/2zMOoPFiOvGE44wLhnAcCJv6qeHJnVoIT2FabDQU5fuUDVxrfCkMxhbnNm4XbY9N6YBixyyiUqJw5omUsxQHcbERjVh2sMI4NQw7VgUldG1tr9SsVM7wnHjy/mFKXBJD+oh6PB/1WKqPLxJlNQfgIjFyKXtoJkNdA4C08FBOwRk6zJOQMiaXquvlZzYSFHEhiF8iBqoHpBCq5PF325agur0WV06U10XozWDcF/j0oFVm5c5Tqk6L0dKck4JJXvL6UAmnLBfqwlIonwqsxVZVli1Rlqf9/YTUnBasur4TWpbACKZA2MKw98s1vX1Mv2K4nD6reE1QwKVANee+/dFU15cT40Ktqj0T4iKBqU8HU7ImpMSlMTevMnt0XU0vGVOb9vTHtkZ8fBNP3YAv8EWzpy1uxNfDjj9Yj1z/wEqho1kS3a6Mnq/akWDXkhyD5drA3qzL05sCsvpZ3tiLKk2staK0feywKPmTlBycrVkGz0rxEQJSzo6D4f6+JUKxrVbSqoDMbKMxpewoQyBpwn8oKBXrunuKqybK4kx6Us4gx8v3i5CrJWY6luV6YWymryedeG2zuJHif+HUulPWff2RTzqXC/ujGKCqE2zC1EeuHrv4DD7fJP9o/WQwbfINX+Jb345ulH7VuQnqn/a5CNpew8ndOUFn9Gwh8kkoo7HQTzMMq3M0w77ob16xw/3eMksKa033nPKzC3cTsDswKX/GViTkxhUXgpsLGrPAVc1hOINpjK9zNr+zMWeErXvzLb/7HvtPSu6mJ+3kfvupWejCFmVl/l149o9Uf94O7/wA=</diagram></mxfile>" style="background-color: rgb(255, 255, 255);"><defs/><g><rect x="161" y="106" width="50" height="75" fill="none" stroke="none" pointer-events="all"/><path d="M 161 181 L 161 113.92 L 200.79 113.92 L 200.79 181 Z M 172.84 106 L 161 113.92 L 200.79 113.92 L 211 106 Z M 211 170.43 L 211 106 L 200.79 113.92 L 200.79 181 Z" fill="#09555b" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 176.6 126.07 L 194.34 126.07 L 194.34 128.18 L 176.6 128.18 L 176.6 131.35 L 168.53 127.13 L 176.6 122.38 Z M 187.89 144.56 L 170.15 144.56 L 170.15 141.92 L 187.89 141.92 L 187.89 138.75 L 195.95 142.98 L 187.89 147.73 Z" fill="#ffffff" stroke="none" pointer-events="all"/><rect x="261" y="1" width="50" height="75" fill="none" stroke="none" pointer-events="all"/><path d="M 261 76 L 261 8.92 L 300.79 8.92 L 300.79 76 Z M 272.84 1 L 261 8.92 L 300.79 8.92 L 311 1 Z M 311 65.43 L 311 1 L 300.79 8.92 L 300.79 76 Z" fill="#09555b" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 276.6 21.07 L 294.34 21.07 L 294.34 23.18 L 276.6 23.18 L 276.6 26.35 L 268.53 22.13 L 276.6 17.38 Z M 287.89 39.56 L 270.15 39.56 L 270.15 36.92 L 287.89 36.92 L 287.89 33.75 L 295.95 37.98 L 287.89 42.73 Z" fill="#ffffff" stroke="none" pointer-events="all"/><rect x="391" y="1" width="50" height="75" fill="none" stroke="none" pointer-events="all"/><path d="M 391 76 L 391 8.92 L 430.79 8.92 L 430.79 76 Z M 402.84 1 L 391 8.92 L 430.79 8.92 L 441 1 Z M 441 65.43 L 441 1 L 430.79 8.92 L 430.79 76 Z" fill="#09555b" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 406.6 21.07 L 424.34 21.07 L 424.34 23.18 L 406.6 23.18 L 406.6 26.35 L 398.53 22.13 L 406.6 17.38 Z M 417.89 39.56 L 400.15 39.56 L 400.15 36.92 L 417.89 36.92 L 417.89 33.75 L 425.95 37.98 L 417.89 42.73 Z" fill="#ffffff" stroke="none" pointer-events="all"/><rect x="491" y="106" width="50" height="75" fill="none" stroke="none" pointer-events="all"/><path d="M 491 181 L 491 113.92 L 530.79 113.92 L 530.79 181 Z M 502.84 106 L 491 113.92 L 530.79 113.92 L 541 106 Z M 541 170.43 L 541 106 L 530.79 113.92 L 530.79 181 Z" fill="#09555b" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 506.6 126.07 L 524.34 126.07 L 524.34 128.18 L 506.6 128.18 L 506.6 131.35 L 498.53 127.13 L 506.6 122.38 Z M 517.89 144.56 L 500.15 144.56 L 500.15 141.92 L 517.89 141.92 L 517.89 138.75 L 525.95 142.98 L 517.89 147.73 Z" fill="#ffffff" stroke="none" pointer-events="all"/><rect x="321" y="191" width="50" height="75" fill="none" stroke="none" pointer-events="all"/><path d="M 321 266 L 321 198.92 L 360.79 198.92 L 360.79 266 Z M 332.84 191 L 321 198.92 L 360.79 198.92 L 371 191 Z M 371 255.43 L 371 191 L 360.79 198.92 L 360.79 266 Z" fill="#09555b" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 336.6 211.07 L 354.34 211.07 L 354.34 213.18 L 336.6 213.18 L 336.6 216.35 L 328.53 212.13 L 336.6 207.38 Z M 347.89 229.56 L 330.15 229.56 L 330.15 226.92 L 347.89 226.92 L 347.89 223.75 L 355.95 227.98 L 347.89 232.73 Z" fill="#ffffff" stroke="none" pointer-events="all"/><rect x="1" y="111" width="78" height="70" fill="none" stroke="none" pointer-events="all"/><rect x="1" y="111" width="0" height="0" fill="none" stroke="#bac8d3" stroke-width="2" pointer-events="all"/><path d="M 67.86 170.93 L 67.86 158.2 L 1 158.2 L 1 170.93 Z" fill="#09555b" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 47.16 165.09 L 64.13 165.09" fill="none" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 79 147.59 L 12.14 147.59 L 1 158.2 L 67.86 158.2 Z" fill="#09555b" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 79 158.73 L 79 147.59 L 67.86 158.2 L 67.86 170.93 Z M 56.17 181 L 56.17 178.34 L 62.02 169.86 L 62.02 175.16 Z M 56.17 178.34 L 56.17 181 L 2.59 181 L 2.59 178.34 Z M 56.17 178.34 L 2.59 178.34 L 8.42 169.86 L 62.02 169.86 Z M 58.83 151.84 L 58.83 118.43 L 13.19 118.43 L 13.19 151.84 Z" fill="#09555b" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 16.92 125.84 C 16.92 122.14 20.1 121.61 20.1 121.61 C 20.1 121.61 47.69 121.61 50.87 121.61 C 55.12 121.61 55.12 125.84 55.12 125.84 C 55.12 125.84 55.12 142.29 55.12 144.93 C 55.12 147.07 51.4 148.12 51.4 148.12 C 51.4 148.12 23.81 148.12 20.63 148.12 C 17.97 148.12 16.92 144.93 16.92 144.93 Z" fill="none" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 66.79 111 L 20.63 111 L 13.19 118.43 L 58.83 118.43 Z" fill="#09555b" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 66.79 144.93 L 66.79 111 L 58.83 118.43 L 58.83 151.84 Z" fill="#09555b" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><rect x="631" y="109" width="78" height="70" fill="none" stroke="none" pointer-events="all"/><rect x="631" y="109" width="0" height="0" fill="none" stroke="#bac8d3" stroke-width="2" pointer-events="all"/><path d="M 697.86 168.93 L 697.86 156.2 L 631 156.2 L 631 168.93 Z" fill="#09555b" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 677.16 163.09 L 694.13 163.09" fill="none" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 709 145.59 L 642.14 145.59 L 631 156.2 L 697.86 156.2 Z" fill="#09555b" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 709 156.73 L 709 145.59 L 697.86 156.2 L 697.86 168.93 Z M 686.17 179 L 686.17 176.34 L 692.02 167.86 L 692.02 173.16 Z M 686.17 176.34 L 686.17 179 L 632.59 179 L 632.59 176.34 Z M 686.17 176.34 L 632.59 176.34 L 638.42 167.86 L 692.02 167.86 Z M 688.83 149.84 L 688.83 116.43 L 643.19 116.43 L 643.19 149.84 Z" fill="#09555b" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 646.92 123.84 C 646.92 120.14 650.1 119.61 650.1 119.61 C 650.1 119.61 677.69 119.61 680.87 119.61 C 685.12 119.61 685.12 123.84 685.12 123.84 C 685.12 123.84 685.12 140.29 685.12 142.93 C 685.12 145.07 681.4 146.12 681.4 146.12 C 681.4 146.12 653.81 146.12 650.63 146.12 C 647.97 146.12 646.92 142.93 646.92 142.93 Z" fill="none" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 696.79 109 L 650.63 109 L 643.19 116.43 L 688.83 116.43 Z" fill="#09555b" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 696.79 142.93 L 696.79 109 L 688.83 116.43 L 688.83 149.84 Z" fill="#09555b" stroke="#bac8d3" stroke-width="2" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 65.74 146 L 161 143.5" fill="none" stroke="#0b4d6a" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 211 106 L 261 38.5" fill="none" stroke="#0b4d6a" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 206.5 175 L 321 228.5" fill="none" stroke="#0b4d6a" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 311 38.5 L 391 38.5" fill="none" stroke="#0b4d6a" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 371 228.5 L 491 181" fill="none" stroke="#0b4d6a" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 441 38.5 L 491 108.5" fill="none" stroke="#0b4d6a" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 541 143.5 L 643.48 144" fill="none" stroke="#0b4d6a" stroke-miterlimit="10" pointer-events="stroke"/><rect x="21" y="181" width="30" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 191px; margin-left: 36px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 16px; font-family: Courier New; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; "><font style="font-size: 16px">H1</font></div></div></div></foreignObject><text x="36" y="196" fill="#000000" font-family="Courier New" font-size="16px" text-anchor="middle">H1</text></switch></g><rect x="171" y="179" width="30" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 189px; margin-left: 186px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 16px; font-family: Courier New; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">S1</div></div></div></foreignObject><text x="186" y="194" fill="#000000" font-family="Courier New" font-size="16px" text-anchor="middle">S1</text></switch></g><rect x="271" y="76" width="30" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 86px; margin-left: 286px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 16px; font-family: Courier New; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">S2</div></div></div></foreignObject><text x="286" y="91" fill="#000000" font-family="Courier New" font-size="16px" text-anchor="middle">S2</text></switch></g><rect x="401" y="76" width="30" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 86px; margin-left: 416px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 16px; font-family: Courier New; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">S3</div></div></div></foreignObject><text x="416" y="91" fill="#000000" font-family="Courier New" font-size="16px" text-anchor="middle">S3</text></switch></g><rect x="331" y="266" width="30" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 276px; margin-left: 346px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 16px; font-family: Courier New; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">S4</div></div></div></foreignObject><text x="346" y="281" fill="#000000" font-family="Courier New" font-size="16px" text-anchor="middle">S4</text></switch></g><rect x="501" y="181" width="30" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 191px; margin-left: 516px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 16px; font-family: Courier New; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">S5</div></div></div></foreignObject><text x="516" y="196" fill="#000000" font-family="Courier New" font-size="16px" text-anchor="middle">S5</text></switch></g><rect x="651" y="181" width="30" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 191px; margin-left: 666px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 16px; font-family: Courier New; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">H2</div></div></div></foreignObject><text x="666" y="196" fill="#000000" font-family="Courier New" font-size="16px" text-anchor="middle">H2</text></switch></g></g><switch><g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/><a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank"><text text-anchor="middle" font-size="10px" x="50%" y="100%">Viewer does not support full SVG 1.1</text></a></switch></svg></p><p>从 H1 主机到 H2 主机,有两条路径:</p><ul><li><code>H1->S1->S2->S3->S5->H2</code></li><li><code>H1->S4->S5->H2</code></li></ul><p>实现路径切换的原理是:用新的流表项替换旧的,从而改变数据包的转发端口;对应到具体的实验上,还需要让控制器感知网络的某种变化,确定在何时下发新的流表项。</p><p>流表项的下发、修改都是通过发送 Flow Entry Modify 消息完成的,用 Ryu API <code>OFPFlowMod</code> 构造。构造一个 Flow Entry Modify 消息至少应该指定:</p><ul><li>属于哪个交换机 (Datapath)</li><li>优先级(决定流表项匹配时的顺序,数值大者优先进行匹配)</li><li>匹配域(匹配何种内容的数据包头)</li><li>超时时间:流表项定时删除,分为软超时(一定时间没有数据包则删除)、硬超时(一定时间后删除)两种。</li><li>指令:完成何种动作</li></ul><p><code>OFPFlowMod</code>将返回一个包装好的消息对象,此时调用某个<code>datapath</code>实例的<code>send_msg</code>方法发送即可。</p><figure><div class="code-wrapper"><pre class="line-numbers language-python" data-language="python"><code class="language-python">mod <span class="token operator">=</span> parser<span class="token punctuation">.</span>OFPFlowMod<span class="token punctuation">(</span>datapath<span class="token operator">=</span>datapath<span class="token punctuation">,</span> priority<span class="token operator">=</span>priority<span class="token punctuation">,</span> idle_timeout<span class="token operator">=</span>idle_timeout<span class="token punctuation">,</span> hard_timeout<span class="token operator">=</span>hard_timeout<span class="token punctuation">,</span> instructions<span class="token operator">=</span>inst<span class="token punctuation">)</span>datapath<span class="token punctuation">.</span>send_msg<span class="token punctuation">(</span>mod<span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div></figure><p>若这个交换机上之前存在匹配域一致的流表项,则此项将覆盖之,完成修改。</p><h3 id="设定流表项超时实现周期性切换">设定流表项超时实现周期性切换</h3><p>下发流表项时设定相同的硬超时,例如 5s ,则整个路径上所有的流表项 5s 后全部删除,就会发生 Table-miss 使交换机发送 Packet-In 消息到控制器。</p><p>因此,周期性切换可以用 Packet-In 消息作为触发条件:当一个非 LLDP 的数据包<sup><a href="#4">[4]</a></sup>被发送到控制器时,就意味着交换机上发生了 Table-miss ,之前的流表项已经失效了。检查逻辑大致如下:(ARP 数据包单独处理是为了记录 IP 地址、MAC 地址、交换机端口之间的对应关系)</p><figure><div class="code-wrapper"><pre class="line-numbers language-python" data-language="python"><code class="language-python">pkt <span class="token operator">=</span> packet<span class="token punctuation">.</span>Packet<span class="token punctuation">(</span>msg<span class="token punctuation">.</span>data<span class="token punctuation">)</span>eth <span class="token operator">=</span> pkt<span class="token punctuation">.</span>get_protocols<span class="token punctuation">(</span>ethernet<span class="token punctuation">.</span>ethernet<span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span>pkt_arp <span class="token operator">=</span> pkt<span class="token punctuation">.</span>get_protocol<span class="token punctuation">(</span>arp<span class="token punctuation">.</span>arp<span class="token punctuation">)</span>pkt_ipv4 <span class="token operator">=</span> pkt<span class="token punctuation">.</span>get_protocol<span class="token punctuation">(</span>ipv4<span class="token punctuation">.</span>ipv4<span class="token punctuation">)</span><span class="token keyword">if</span> eth<span class="token punctuation">.</span>ethertype <span class="token operator">==</span> ether_types<span class="token punctuation">.</span>ETH_TYPE_LLDP<span class="token punctuation">:</span><span class="token keyword">return</span><span class="token keyword">if</span> pkt_arp<span class="token punctuation">:</span> <span class="token comment"># Process the ARP packet, recording the corresponding IP, MAC address and tuple (datapath id, port id).</span> <span class="token keyword">return</span><span class="token keyword">if</span> pkt_ipv4<span class="token punctuation">:</span> <span class="token comment"># Find a new path from the source end to the destination end, install it.</span> <span class="token comment"># Re-send the packet to the next hop on the new path.</span> <span class="token keyword">return</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div></figure><p>按照这个逻辑来写代码,然后就会出错<del>此处应有狗头</del>。</p><p>逻辑是理想的,我所设想的工作过程很美好:</p><ol><li>流表项失效,数据包发生 Table-miss ,Packet-In 消息发送到控制器</li><li>控制器搜索路径、下发新的流表项、重新发送这个数据包到新路径上的下一跳</li><li>下一个数据包正常发送</li></ol><p>现实的冷水泼在脸上,三步中后两步都是<strong>没有保证</strong>的。</p><ul><li><p>控制器想要修改流表项,并不像给变量重新赋值这样简单。</p><ul><li>构造 Flow Entry Modify 消息之后,调用<code>send_msg</code>时只不过将这个消息<strong>送入发送队列</strong>,不保证立即发送给交换机。</li><li>从控制器发往不同交换机的消息途经网络链路,其到达时间是没有保证的,<strong>不一定先发送先到达</strong>。</li></ul><p>这意味着,Packet-Out 消息(用来重发数据包)可能比某些 Flow Entry Modify 消息到达更早。</p></li><li><p>交换机转发数据包动作迅速,不会等待流表项全部就位才转发。</p></li></ul><p>上述设想的工作过程,实际上依赖于特定的<strong>时序</strong>保证,然而网络运作时并没有如此良好的时序规则。</p><p>用比较简单的办法:记录上次搜索路径的时间,认为在每次搜索新路径后,短时间内(比如 1s)的 Packet-In 都是流表项没有全部就位的问题,不重新搜索路径,只作转发处理。</p><figure><div class="code-wrapper"><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">if</span> pkt_ipv4<span class="token punctuation">:</span> current <span class="token operator">=</span> datetime<span class="token punctuation">.</span>datetime<span class="token punctuation">.</span>now<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>current <span class="token operator">-</span> self<span class="token punctuation">.</span>last<span class="token punctuation">[</span><span class="token punctuation">(</span>src_dpid<span class="token punctuation">,</span> dst_dpid<span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">.</span>seconds <span class="token operator">></span> <span class="token number">1</span><span class="token punctuation">:</span> <span class="token comment"># Find a new path from the source end to the destination end, install it.</span> self<span class="token punctuation">.</span>last<span class="token punctuation">[</span><span class="token punctuation">(</span>src_dpid<span class="token punctuation">,</span> dst_dpid<span class="token punctuation">)</span><span class="token punctuation">]</span> <span class="token operator">=</span> current <span class="token comment"># Re-send the packet to the next hop on the new path.</span> <span class="token keyword">return</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div></figure><p>寻找新路径很容易实现,我用了模拟栈实现非递归深搜来寻找路径,得到的结果和原先的路径比较是否相同即可。路径的存储格式是<code>(dpid, port)</code>二元组的<code>list</code>,依次表示数据包经过的每一个端口。</p><p>测试时,让 <code>H1</code>连续 ping <code>H2</code> 20次:</p><img src="/SDN%E5%AE%9E%E9%AA%8C%EF%BC%88%E4%BA%8C%EF%BC%89%EF%BC%9A%E5%8A%A8%E6%80%81%E5%8F%98%E6%9B%B4%E8%BD%AC%E5%8F%91%E8%B7%AF%E5%BE%84/dynamic-ping.png" class=""><p>每次时延显著变长,就是流表项超时,控制器在下发流表项、进行转发。而在控制器端,也可以看到对应的路径切换:</p><img src="/SDN%E5%AE%9E%E9%AA%8C%EF%BC%88%E4%BA%8C%EF%BC%89%EF%BC%9A%E5%8A%A8%E6%80%81%E5%8F%98%E6%9B%B4%E8%BD%AC%E5%8F%91%E8%B7%AF%E5%BE%84/dynamic-route.png" class=""><h3 id="路径失效即时切换">路径失效即时切换</h3><p>在 Mininet 控制台,可以执行<code>link up/down</code>命令,激活或禁用某条链路,模拟物理网络中链路的连接和断开。在 OpenFlow 的视角下,链路的连接、断开反映为端口 <em>Port</em> 的状态变化,这同样会导致交换机向控制器发送消息。 Ryu 对此的封装有两种形式:</p><ul><li><code>OFPPortStatus</code>消息,直接对应于 OpenFlow 的同名消息。</li><li><code>EventPortAdd/Delete/Modify</code>事件分别表示端口的增加、删除、修改;<code>EventPortStatus</code>事件,表示端口状态的变化。</li></ul><p>上述的消息和事件都可以用<code>set_ev_cls</code>装饰器注册处理函数。简单起见,本次实验中只要端口状态变化,就认为是链路失效,删掉之前的所有流表项。这只需要注册<code>EventPortStatus</code>事件,在处理函数中删除保存的路径对应的流表项即可完成。</p><div class="note note-info"> <p>删除流表项需要发送<code>OFPFlowMod</code>消息,指定<code>command</code>参数为<code>ofproto.OFPFC_DELETE</code>。需要注意的是,还必须指定<code>out_port</code>参数,否则消息可以被发送但删除并不会发生。这里直接令<code>out_port=ofproto.OFPP_ANY</code>表示删除任何满足条件的流表项。</p> </div><p>类似地,流表项被删除后下一个数据包将导致 Table-miss ,进而导致 Packet-In ,此时用最短路算法找跳数最少的路径并下发新的流表项。测试时在 Mininet 控制台执行<code>link s1 s4 down/up</code>就可以看到控制器端输出的路径变化情况了。</p><h2 id="说明">说明</h2><p>这是对校内实验的一些记录和想法,实验指导书、代码框架由老师和助教编写,涉及著作权问题,故不在此展示。这里附上自己编写的一部分代码,除此之外,至少还需要处理<code>EventOFPSwitchFeathures</code>和<code>EventOFPPacketIn</code>;并在拓扑发生变化时(<code>Switch/Port/Link</code>的<code>Add/Delete/Modify</code>事件)重新获取全局拓扑。</p><ul><li>搜索并保存新路径</li></ul><figure><div class="code-wrapper"><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">get_new_path</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> src<span class="token punctuation">,</span> dst<span class="token punctuation">)</span><span class="token punctuation">:</span> <span class="token keyword">if</span> src <span class="token operator">==</span> dst<span class="token punctuation">:</span> <span class="token keyword">return</span> <span class="token punctuation">[</span><span class="token punctuation">]</span> stack <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token comment"># use this list as a stack for DFS</span> visited <span class="token operator">=</span> defaultdict<span class="token punctuation">(</span><span class="token keyword">lambda</span><span class="token punctuation">:</span> <span class="token boolean">False</span><span class="token punctuation">)</span> <span class="token comment"># record the children of a certain datapath when searching</span> <span class="token comment"># {dpid: [dpid, ...]}</span> children <span class="token operator">=</span> defaultdict<span class="token punctuation">(</span><span class="token keyword">lambda</span><span class="token punctuation">:</span> <span class="token builtin">list</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token comment"># record the previous port of a certain datapath</span> <span class="token comment"># {dpid: (prev_dpid, prev_port, dpid, port)}</span> result <span class="token operator">=</span> defaultdict<span class="token punctuation">(</span><span class="token keyword">lambda</span><span class="token punctuation">:</span> defaultdict<span class="token punctuation">(</span><span class="token keyword">lambda</span><span class="token punctuation">:</span> <span class="token boolean">None</span><span class="token punctuation">)</span><span class="token punctuation">)</span> stack<span class="token punctuation">.</span>append<span class="token punctuation">(</span>src<span class="token punctuation">)</span> <span class="token keyword">while</span> <span class="token builtin">len</span><span class="token punctuation">(</span>stack<span class="token punctuation">)</span> <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">:</span> current <span class="token operator">=</span> stack<span class="token punctuation">[</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">]</span> visited<span class="token punctuation">[</span>current<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token boolean">True</span> <span class="token keyword">if</span> current <span class="token operator">==</span> dst <span class="token keyword">and</span> stack <span class="token operator">!=</span> self<span class="token punctuation">.</span>pathnodes<span class="token punctuation">[</span><span class="token punctuation">(</span>src<span class="token punctuation">,</span> dst<span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">:</span> <span class="token keyword">break</span> found <span class="token operator">=</span> <span class="token boolean">False</span> <span class="token keyword">for</span> <span class="token punctuation">(</span>temp_src<span class="token punctuation">,</span> temp_dst<span class="token punctuation">)</span> <span class="token keyword">in</span> self<span class="token punctuation">.</span>src_links<span class="token punctuation">[</span>current<span class="token punctuation">]</span><span class="token punctuation">:</span> <span class="token keyword">if</span> visited<span class="token punctuation">[</span>temp_dst<span class="token punctuation">]</span> <span class="token operator">==</span> <span class="token boolean">False</span><span class="token punctuation">:</span> temp_src_port <span class="token operator">=</span> self<span class="token punctuation">.</span>src_links<span class="token punctuation">[</span>current<span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token punctuation">(</span>temp_src<span class="token punctuation">,</span> temp_dst<span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> temp_dst_port <span class="token operator">=</span> self<span class="token punctuation">.</span>src_links<span class="token punctuation">[</span>current<span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token punctuation">(</span>temp_src<span class="token punctuation">,</span> temp_dst<span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span> result<span class="token punctuation">[</span>temp_dst<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token punctuation">(</span>temp_src<span class="token punctuation">,</span> temp_src_port<span class="token punctuation">,</span> temp_dst<span class="token punctuation">,</span> temp_dst_port<span class="token punctuation">)</span> stack<span class="token punctuation">.</span>append<span class="token punctuation">(</span>temp_dst<span class="token punctuation">)</span> children<span class="token punctuation">[</span>current<span class="token punctuation">]</span><span class="token punctuation">.</span>append<span class="token punctuation">(</span>temp_dst<span class="token punctuation">)</span> found <span class="token operator">=</span> <span class="token boolean">True</span> <span class="token keyword">break</span> <span class="token keyword">if</span> found <span class="token operator">==</span> <span class="token boolean">False</span><span class="token punctuation">:</span> stack<span class="token punctuation">.</span>pop<span class="token punctuation">(</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span> <span class="token keyword">for</span> child <span class="token keyword">in</span> children<span class="token punctuation">[</span>current<span class="token punctuation">]</span><span class="token punctuation">:</span> visited<span class="token punctuation">[</span>child<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token boolean">False</span> path <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token keyword">if</span> dst <span class="token keyword">not</span> <span class="token keyword">in</span> result<span class="token punctuation">:</span> <span class="token keyword">return</span> <span class="token boolean">None</span> self<span class="token punctuation">.</span>pathnodes<span class="token punctuation">[</span><span class="token punctuation">(</span>src<span class="token punctuation">,</span> dst<span class="token punctuation">)</span><span class="token punctuation">]</span> <span class="token operator">=</span> stack <span class="token keyword">while</span> <span class="token punctuation">(</span>dst <span class="token keyword">in</span> result<span class="token punctuation">)</span> <span class="token keyword">and</span> <span class="token punctuation">(</span>result<span class="token punctuation">[</span>dst<span class="token punctuation">]</span> <span class="token keyword">is</span> <span class="token keyword">not</span> <span class="token boolean">None</span><span class="token punctuation">)</span><span class="token punctuation">:</span> path <span class="token operator">=</span> <span class="token punctuation">[</span>result<span class="token punctuation">[</span>dst<span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">:</span><span class="token number">4</span><span class="token punctuation">]</span><span class="token punctuation">]</span> <span class="token operator">+</span> path path <span class="token operator">=</span> <span class="token punctuation">[</span>result<span class="token punctuation">[</span>dst<span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">:</span><span class="token number">2</span><span class="token punctuation">]</span><span class="token punctuation">]</span> <span class="token operator">+</span> path dst <span class="token operator">=</span> result<span class="token punctuation">[</span>dst<span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> <span class="token keyword">return</span> path<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div></figure><ul><li>处理链路状态变化</li></ul><figure><div class="code-wrapper"><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">delete_flow</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> datapath<span class="token punctuation">,</span> priority<span class="token punctuation">,</span> <span class="token keyword">match</span><span class="token punctuation">,</span> idle_timeout<span class="token operator">=</span><span class="token number">0</span><span class="token punctuation">,</span> hard_timeout<span class="token operator">=</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">:</span> ofproto <span class="token operator">=</span> datapath<span class="token punctuation">.</span>ofproto parser <span class="token operator">=</span> datapath<span class="token punctuation">.</span>ofproto_parser mod <span class="token operator">=</span> parser<span class="token punctuation">.</span>OFPFlowMod<span class="token punctuation">(</span>datapath<span class="token operator">=</span>datapath<span class="token punctuation">,</span> priority<span class="token operator">=</span>priority<span class="token punctuation">,</span> idle_timeout<span class="token operator">=</span>idle_timeout<span class="token punctuation">,</span> hard_timeout<span class="token operator">=</span>hard_timeout<span class="token punctuation">,</span> <span class="token keyword">match</span><span class="token operator">=</span><span class="token keyword">match</span><span class="token punctuation">,</span> out_port<span class="token operator">=</span>ofproto<span class="token punctuation">.</span>OFPP_ANY<span class="token punctuation">,</span> out_group<span class="token operator">=</span>ofproto<span class="token punctuation">.</span>OFPG_ANY<span class="token punctuation">,</span> command<span class="token operator">=</span>ofproto<span class="token punctuation">.</span>OFPFC_DELETE<span class="token punctuation">)</span> datapath<span class="token punctuation">.</span>send_msg<span class="token punctuation">(</span>mod<span class="token punctuation">)</span><span class="token decorator annotation punctuation">@set_ev_cls</span><span class="token punctuation">(</span>ofp_event<span class="token punctuation">.</span>EventOFPPortStatus<span class="token punctuation">,</span> <span class="token punctuation">[</span>CONFIG_DISPATCHER<span class="token punctuation">,</span> MAIN_DISPATCHER<span class="token punctuation">,</span> DEAD_DISPATCHER<span class="token punctuation">,</span> HANDSHAKE_DISPATCHER<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token keyword">def</span> <span class="token function">_port_status_handler</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> ev<span class="token punctuation">)</span><span class="token punctuation">:</span> datapath <span class="token operator">=</span> ev<span class="token punctuation">.</span>msg<span class="token punctuation">.</span>datapath ofproto <span class="token operator">=</span> datapath<span class="token punctuation">.</span>ofproto parser <span class="token operator">=</span> datapath<span class="token punctuation">.</span>ofproto_parser <span class="token keyword">if</span> self<span class="token punctuation">.</span>path <span class="token keyword">is</span> <span class="token boolean">None</span><span class="token punctuation">:</span> <span class="token keyword">return</span> <span class="token keyword">for</span> i <span class="token keyword">in</span> <span class="token builtin">range</span><span class="token punctuation">(</span><span class="token builtin">len</span><span class="token punctuation">(</span>self<span class="token punctuation">.</span>path<span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">,</span> <span class="token operator">-</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">:</span> datapath_path <span class="token operator">=</span> self<span class="token punctuation">.</span>datapaths<span class="token punctuation">[</span>self<span class="token punctuation">.</span>path<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">]</span> <span class="token comment"># delete the flow entry from src to dst</span> <span class="token keyword">match</span> <span class="token operator">=</span> parser<span class="token punctuation">.</span>OFPMatch<span class="token punctuation">(</span>in_port<span class="token operator">=</span>self<span class="token punctuation">.</span>path<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">,</span> eth_src<span class="token operator">=</span>self<span class="token punctuation">.</span>eth_src<span class="token punctuation">,</span> eth_dst<span class="token operator">=</span>self<span class="token punctuation">.</span>eth_dst<span class="token punctuation">,</span> eth_type<span class="token operator">=</span><span class="token number">0x0800</span><span class="token punctuation">,</span> ipv4_src<span class="token operator">=</span>self<span class="token punctuation">.</span>ipv4_src<span class="token punctuation">,</span> ipv4_dst<span class="token operator">=</span>self<span class="token punctuation">.</span>ipv4_dst<span class="token punctuation">)</span> self<span class="token punctuation">.</span>delete_flow<span class="token punctuation">(</span>datapath_path<span class="token punctuation">,</span> <span class="token number">100</span><span class="token punctuation">,</span> <span class="token keyword">match</span><span class="token punctuation">)</span> <span class="token comment"># delete the flow entry from dst to src</span> <span class="token keyword">match</span> <span class="token operator">=</span> parser<span class="token punctuation">.</span>OFPMatch<span class="token punctuation">(</span>in_port<span class="token operator">=</span>self<span class="token punctuation">.</span>path<span class="token punctuation">[</span>i<span class="token operator">+</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">,</span> eth_src<span class="token operator">=</span>self<span class="token punctuation">.</span>eth_dst<span class="token punctuation">,</span> eth_dst<span class="token operator">=</span>self<span class="token punctuation">.</span>eth_src<span class="token punctuation">,</span> eth_type<span class="token operator">=</span><span class="token number">0x0800</span><span class="token punctuation">,</span> ipv4_src<span class="token operator">=</span>self<span class="token punctuation">.</span>ipv4_dst<span class="token punctuation">,</span> ipv4_dst<span class="token operator">=</span>self<span class="token punctuation">.</span>ipv4_src<span class="token punctuation">)</span> self<span class="token punctuation">.</span>delete_flow<span class="token punctuation">(</span>datapath_path<span class="token punctuation">,</span> <span class="token number">100</span><span class="token punctuation">,</span> <span class="token keyword">match</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div></figure><h2 id="注释">注释</h2><p><a name="#1">[1]</a>按照 <a href="https://opennetworking.org/wp-content/uploads/2014/10/openflow-switch-v1.3.5.pdf#subsection.5.4">OpenFlow v1.3 标准文档 5.4 Table-miss</a>,所有流表必须至少包含一条 Table-miss 流表项(由控制器下发),优先级为最低(0),即指定数据包匹配失败时的处理动作。最常见的动作是发往控制器,这也是 Ryu 控制器下发的 table-miss 流表项。若为 OpenFlow v1.0/1.1/1.2 标准,则无需包含 table-miss 流表项(也不需要控制器下发),匹配失败会自动发往控制器。</p><p><a name="#2">[2]</a><a href="https://ryu.readthedocs.io/en/latest/ofproto_v1_3_ref.html#asynchronous-messages">Packet-In Message, Asynchronous Messages, OpenFlow v1.3 Messages and Structures, Ryu documentation</a></p><p><a name="#3">[3]</a>虽然经常叫作 Thread,但 Ryu Manager 启动的不是经典意义上的线程,而是第三方库 eventlet 实现的协程。二者主要差别在于协程不能抢占,必须等待其他协程主动让出 CPU 才能执行。这一点对本次实验并无影响。</p><p><a name="#4">[4]</a>LLDP 数据包是用于发现交换机、探查网络拓扑的数据包,被设定为<strong>一跳</strong>之后转发给控制器,因此会引发大量的 Packet-In 消息。如果不需要主动处理拓扑信息,LLDP 数据包可以直接丢弃。本次实验中,Packet-In 的频率远远高于流表项超时的频率,大部分都是无用的 LLDP 数据包,须加以分辨。</p>]]></content:encoded>
<category domain="https://greyishsong.ink/categories/%E7%BC%96%E7%A8%8B/">编程</category>
<category domain="https://greyishsong.ink/tags/SDN/">SDN</category>
<category domain="https://greyishsong.ink/tags/%E8%BD%AF%E4%BB%B6%E5%AE%9A%E4%B9%89%E7%BD%91%E7%BB%9C/">软件定义网络</category>
<category domain="https://greyishsong.ink/tags/Mininet/">Mininet</category>
<category domain="https://greyishsong.ink/tags/Ryu/">Ryu</category>
<comments>https://greyishsong.ink/SDN%E5%AE%9E%E9%AA%8C%EF%BC%88%E4%BA%8C%EF%BC%89%EF%BC%9A%E5%8A%A8%E6%80%81%E5%8F%98%E6%9B%B4%E8%BD%AC%E5%8F%91%E8%B7%AF%E5%BE%84/#disqus_thread</comments>
</item>
<item>
<title>夏日不争艳</title>
<link>https://greyishsong.ink/%E5%A4%8F%E6%97%A5%E4%B8%8D%E4%BA%89%E8%89%B3/</link>
<guid>https://greyishsong.ink/%E5%A4%8F%E6%97%A5%E4%B8%8D%E4%BA%89%E8%89%B3/</guid>
<pubDate>Thu, 03 Jun 2021 05:08:43 GMT</pubDate>
<description><p>疲惫时,总是觉得有些一言难尽的事情,却不是太想说。大家越来越忙,我亦如是,于是谈天说地与切磋琢磨都少了很多。孤云独去闲,不见众鸟,就自己做几朵花来看。春日里气候甚佳,于是神完气足,一个个的争奇斗艳;夏天燥热起来,都蔫下去,雨后也只有一些苦菜花与寥寥的蒲公英开在楼下。</p>
<p>要说闲工夫也不多,不过有些小想法蹦出来,算是做了一点有意思的东西。不想旁的麻烦太多,拖延足有两月才记下来。</p></description>
<content:encoded><![CDATA[<p>疲惫时,总是觉得有些一言难尽的事情,却不是太想说。大家越来越忙,我亦如是,于是谈天说地与切磋琢磨都少了很多。孤云独去闲,不见众鸟,就自己做几朵花来看。春日里气候甚佳,于是神完气足,一个个的争奇斗艳;夏天燥热起来,都蔫下去,雨后也只有一些苦菜花与寥寥的蒲公英开在楼下。</p><p>要说闲工夫也不多,不过有些小想法蹦出来,算是做了一点有意思的东西。不想旁的麻烦太多,拖延足有两月才记下来。</p><span id="more"></span><h2 id="茉莉胸花">茉莉胸花</h2><p>已经忘了是去年何时看到的做法:以拧麻花的方式把皱纹纸条拧半圈,然后从中间拧细处对折,再用来裁剪花瓣或叶子。 以对折处为尖端,则越靠近尖端纸张越厚,可以模仿多肉的叶子。脑海里浮现了小小的白色花,恰好有这样的花瓣。我没有多想具体是哪一种,根据印象,姑且算作茉莉——品种多样,总有差不多的。</p><p>裁约摸拇指宽度的纸条,比平时用的窄很多。我起先报废了不少练手的材料,基本是拧过头,从中间断掉了。这样做出的花瓣必然不平整,所以这次也没准备模板,随手下剪刀。铁丝太细,粘起来不易,往往花瓣粘上了一半,整个一朵掉了下来。我不得不动作慢一点,让胶固化得彻底些。随后又草率地做了几片叶子,这时想起来去找茉莉的照片,按着其中两片对生的样子扎到花梗上。</p><img src="/%E5%A4%8F%E6%97%A5%E4%B8%8D%E4%BA%89%E8%89%B3/1.jpg" class=""><img src="/%E5%A4%8F%E6%97%A5%E4%B8%8D%E4%BA%89%E8%89%B3/2.jpg" class=""><p>成形之后,先拿一摞便签纸垫着,转圈拍了几张照片,找找感觉。</p><p>起先,我想做盆栽,花做成后却发现:下半部分只占全高的三分之一,难以固定在花盆这样浅的容器中。</p><p>钟休所提的建议是西服上的胸花,然而我更想做成静物,而非饰品。另有材料性能的限制:纸花的花梗和叶子都埋有铁丝,因而较硬且缺乏弹性,成品宜静不宜动。</p><p>另一个建议是做个画框,确实与我的意思更相近。但我不愿意把四边封住、把花草框起来——那像是做押花,有些压抑,不够自由。如果去掉边框,那么这就是一张卡片,于是我又把思路向卡片发散:做一张打孔的卡片,穿上线绳固定花梗;将来做得多了,只要多打些孔便能一并容纳在同一张卡片上。</p><p>这般的想法走马灯似的闪过去了,我才想起拿着花和纸张比划一下。花总是从平面上支棱起来,压平又不现实(有叶子全掉的风险),思路又窄了。</p><p>手边有一张用来练笔的空白明信片,有一小片笔迹。拿在手里翻来覆去,不如用上。</p><p>若折起卡片的一角,岂不是正好充作西服的衣领?进而又想到,若用原色的明信片折角,则正好用白色的垫在下面作白衬衫。找了些西服照片,将比例夸张几分,做出了这么一张东西:</p><img src="/%E5%A4%8F%E6%97%A5%E4%B8%8D%E4%BA%89%E8%89%B3/3.jpg" class=""><p>后来添上了的领带。这卡片不足一乍长,然而纸花却是同真的胸花尺寸相仿的。按照原本插胸花的位置和角度摆放,则头重脚轻。为此我打孔时稍偏几分,把花摆得更接近中轴,以免整体重心不稳。</p><img src="/%E5%A4%8F%E6%97%A5%E4%B8%8D%E4%BA%89%E8%89%B3/4.jpg" class=""><p>最后的成品倒是出乎意料地让我满意了。</p><h2 id="未成的向日葵">未成的向日葵</h2><p>大概是自己之前离毕业太遥远,对毕业典礼总是没有印象;而今年临近大四,对学长的毕业典礼便兼具好奇和怯意。每日见到拍毕业照的同学不过二三次,已感到为毕业的氛围所淹没。</p><p>拍毕业照的学长学姐往往捧花,多的是向日葵、勿忘我、尤加利叶,也有些单头或多头玫瑰。勿忘我果然多见用水晶草充数的,奇怪的是很合气氛的非洲菊不见出场。几日过去,向日葵大都被堆在图书馆钱老的雕像面前,只一天就被西安的烈日脱了水,堆出干枯的阳光颜色来。</p><p>远方朋友的学校情况相仿,受我所托解剖了一朵向日葵。我就着发来的照片裁剪模板,尝试动手。中间的花序是长纸条裁成流苏状,再将每一条搓成绳子,耗费不少苦工,成果尚可;外围的花序则做成了雏菊的样子,实在失败。</p><img src="/%E5%A4%8F%E6%97%A5%E4%B8%8D%E4%BA%89%E8%89%B3/5.jpg" class=""><img src="/%E5%A4%8F%E6%97%A5%E4%B8%8D%E4%BA%89%E8%89%B3/6.jpg" class=""><img src="/%E5%A4%8F%E6%97%A5%E4%B8%8D%E4%BA%89%E8%89%B3/7.jpg" class=""><p>正面来看,外圈花瓣只从中心伸出一半来,宽且短;反面则可见重叠处不对,明明是两层花瓣,看起来却像交叠的一层。</p><p>第二次吸取教训,将花瓣的模板修整得更细;基部额外留出一截,以便连接中心。如此确实将花瓣分层的效果显现出来,却发现依然太宽。</p><img src="/%E5%A4%8F%E6%97%A5%E4%B8%8D%E4%BA%89%E8%89%B3/8.jpg" class=""><img src="/%E5%A4%8F%E6%97%A5%E4%B8%8D%E4%BA%89%E8%89%B3/9.jpg" class=""><p>向日葵的花瓣本是瘦的,很多簇拥成一圈,显得繁复灿烂。这样的成品不伦不类,像是穿了向日葵衣服的雏菊,一气之下差点扔了。也试着添上萼片,两番尝试成果也不佳,只能说比花瓣稍强。</p><img src="/%E5%A4%8F%E6%97%A5%E4%B8%8D%E4%BA%89%E8%89%B3/10.jpg" class=""><img src="/%E5%A4%8F%E6%97%A5%E4%B8%8D%E4%BA%89%E8%89%B3/11.jpg" class=""><p>最后也没能告成,只匆匆而去,自西安返乡。</p>]]></content:encoded>
<category domain="https://greyishsong.ink/categories/%E6%89%8B%E5%B7%A5%E8%89%BA/">手工艺</category>
<category domain="https://greyishsong.ink/tags/%E7%94%9F%E6%B4%BB/">生活</category>
<category domain="https://greyishsong.ink/tags/%E7%BA%B8%E8%8A%B1/">纸花</category>
<comments>https://greyishsong.ink/%E5%A4%8F%E6%97%A5%E4%B8%8D%E4%BA%89%E8%89%B3/#disqus_thread</comments>
</item>
<item>
<title>二零二一:春已归来</title>
<link>https://greyishsong.ink/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/</link>
<guid>https://greyishsong.ink/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/</guid>
<pubDate>Mon, 05 Apr 2021 08:29:54 GMT</pubDate>
<description><p>今年西安的春日反复无常,忽地就下雨,忽地就放晴。走着走着,总是想起那本稼轩词中的第一首:《汉宫春·立春日》。昔日我想着要读完稼轩词,然而最终倒在了第一首上。这就好像大家传来传去的笑话:背单词一年,一提起来还是 abnormal。</p>
<p>“春已归来,望美人头上,袅袅春幡。无端风雨,未肯收尽馀寒。”</p>
<p>猛然的升温之后,就是一阵倒春寒,感冒足有三周。最冷的几天里又正是感冒严重,只好围巾口罩,瑟缩起来。然而终于又洒下来的暖阳,还是让我摘了围巾,不然挂不上相机啊。</p></description>
<content:encoded><![CDATA[<p>今年西安的春日反复无常,忽地就下雨,忽地就放晴。走着走着,总是想起那本稼轩词中的第一首:《汉宫春·立春日》。昔日我想着要读完稼轩词,然而最终倒在了第一首上。这就好像大家传来传去的笑话:背单词一年,一提起来还是 abnormal。</p><p>“春已归来,望美人头上,袅袅春幡。无端风雨,未肯收尽馀寒。”</p><p>猛然的升温之后,就是一阵倒春寒,感冒足有三周。最冷的几天里又正是感冒严重,只好围巾口罩,瑟缩起来。然而终于又洒下来的暖阳,还是让我摘了围巾,不然挂不上相机啊。</p><span id="more"></span><blockquote><p>友情提示:本站服务器带宽有限,快速下滑大概会加载不出图片来。</p></blockquote><p>其实彭康背后有几枝紫叶桃花,还是今年回来之后取外卖时才注意到。这条小路大多只作通往外卖骑手之路,而我点外卖增多,正是随着近两月来骤然忙碌开始的。以往除了冬日里惫懒,想要吃点热腾腾的冒菜,通常是很少点外卖的。纵然如此,还是有些奇怪——毕竟是紫红的叶子,且桃树那对折的狭长叶片是少见的好认,竟完全没有印象了?</p><img src="/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/DSC_1643.jpg" class=""><img src="/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/DSC_1644.jpg" class=""><p>道是紫叶桃花,当年在南海街口也看过,还曾驻足许久——却不曾认得也不曾去查找,还是这一次才知晓大名。</p><img src="/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/DSC_1645.jpg" class=""><p>桃花恰好有深浅二色,交相掩映之下颇显风姿。这回也不像南海街口那次是晚春,少了那种冷艳的气象,明快多了。虽说如此,深红浅红紫红褐红的枝条,就是有那么一种不太娇媚的风范,深红色尤其有一点梅花的味道。想来想去,大概是叶子不明显,像是融入枝条去了。</p><img src="/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/DSC_1658.jpg" class=""><p>小道边上,大约是空间较高中开阔了许多,荠菜足长到二三十公分高,只蹲下去便能拍到近乎平视的场景,再不必像高中那般伏在地上贴地拍摄。</p><img src="/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/DSC_1659.jpg" class=""><p>要说园林绿化的师傅们也是随便得很,修剪下来的松枝就这么丢在草地上——事实上,连是否为人工修剪都不清楚,只不过西安少有大风,我觉得不像是风吹雨打掉下来的。蓝色小花与松枝还挺相和,一查却是一入侵物种,本应消灭的。不过阿拉伯婆婆纳主要危害农作物,这里也不是西农,没有实验田,放过去就放过去罢。</p><img src="/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/DSC_1663.jpg" class=""><p>栅栏边上的紫荆。小时候看一本介绍野花野草的书(现在仍在家中书柜里),名字已记不清,有一篇“不分家的紫荆花”,虽然故事讲得不算很有趣,却总是能记得。一直以来,都有疑惑:香港特别行政区区旗上的“紫荆花”,到底是哪里与这簇生于枝条上的小花相似了?再去查找才知道旗上是洋紫荆,又闹了雷锋跟雷峰塔的笑话。</p><img src="/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/DSC_1667.jpg" class=""><p>钱院南边房屋林立,八九点的时候,也没有其他地方天光大亮的感觉。阳光斜斜地穿过这棵桃树的纸条,打在伸出的一条侧枝上,花瓣多少是透光的,这样以来透亮得恰到好处,我以为胜过诸般玉石。</p><img src="/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/DSC_1668.jpg" class=""><img src="/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/DSC_1669.jpg" class=""><img src="/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/DSC_1671.jpg" class=""><p>这几张曝光都短,花瓣的反射率是我没有料到的高,又或者是很久没有拍过,把之前习惯的东西忘掉了。</p><img src="/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/DSC_1673.jpg" class=""><p>交大的樱花还是有名气的,不过名气是樱花道上日本晚樱的事情,和宿舍楼旁边的东京樱花是没有多大关系。它更像原变种些,没有重瓣,单一朵不起眼。</p><img src="/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/DSC_1679.jpg" class=""><img src="/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/DSC_1681.jpg" class=""><p>不过这一棵树却比北边樱花道上的远亲繁茂太多了,独自立与此处,就已经如同一片云霞落地,围在老宿舍楼边。如果努力地把底下这一张照片拉高亮度,能看到窗户里我的倒影也说不定。</p><img src="/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/DSC_1682.jpg" class=""><p>迎春花早开过了,如今在一大丛叶子里找到藏起来的一朵,有点惊喜。</p><p>这边的坡度着实太大,留个花池都要砖砌起来才能整平,还凭空高出地面许多。砖墙围起来的入口处,路灯下逼仄的角落乃至砖缝里,都还是有着花草,身架自是小巧不必说。</p><img src="/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/DSC_1688.jpg" class=""><img src="/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/DSC_1689.jpg" class=""><img src="/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/DSC_1691.jpg" class=""><p>日本晚樱已然盛开,那么樱花道上恐怕少不了行人。当下我并不想多见人,也就不去北边了,只在这花池里走走就好。</p><img src="/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/DSC_1692.jpg" class=""><p>花池里甚至不能称为小道,只算是小径,或许本来也只是为了方便园林师傅进来修剪花木。我这样一米七不到的小个子,也少不了弯腰闪躲头上垂下来的花。</p><img src="/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/DSC_1693.jpg" class=""><img src="/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/DSC_1696.jpg" class=""><img src="/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/DSC_1697.jpg" class=""><img src="/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/DSC_1698.jpg" class=""><p>相机对低亮度的反射光似乎比我的眼睛敏感些,于是这几张都只有很短的曝光。比起全图都明晃晃的,这样还好看些。</p><p>看看时间还早,至少也留一个小时给自己闲逛吧?那么往西边去看看好了。</p><p>思源附近亦是有紫荆的,且属于示人的门面,培养修剪看起来远比栅栏边上放养的勤快。</p><img src="/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/DSC_1704.jpg" class=""><img src="/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/DSC_1706.jpg" class=""><p>其实自觉这一张构图并不如何,光线也把握得不好。但紫荆树花远多于叶,远看很像是一大棵珊瑚摆在那里,伸展开来。真有这么大的珊瑚,估计也是罕见的大型礁盘上才能找到了。</p><p>取道宪梓堂往回绕,又和一只乌鸦对上了眼:</p><img src="/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/DSC_1710.jpg" class=""><p>阳光之下,乌鸦也不是全黑的,金黄的喙和深褐色的羽毛看起来倒仿佛很小个头的猛禽;而这个立在柱子上的姿势,顿时就联想到古埃及守护法老的雄鹰。只来得及留下一张英姿,它就跳下去一蹦一蹦跑远了,马上没了雄鹰的味道。</p><img src="/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/DSC_1717.jpg" class=""><p>这边的一片草地上婆婆纳更多些,其他的草却极少,几乎是物种荒漠了。</p><p>差不多了,回去继续翻资料吧。</p><blockquote><p>春已归来,望美人头上,袅袅春幡。</p><p>可惜山高水长,我望不到北京。</p><p>却笑东风从此,便薰梅染柳,更没些闲。</p><p>看起来我要与东风一较长短了,同样闲不下来啊。</p></blockquote><hr><p>博客上的图片是我压缩过的,想要原图的同学,可以邮件联系我。</p>]]></content:encoded>
<category domain="https://greyishsong.ink/categories/%E9%9A%8F%E7%AC%94/">随笔</category>
<category domain="https://greyishsong.ink/tags/%E7%94%9F%E6%B4%BB/">生活</category>
<category domain="https://greyishsong.ink/tags/%E6%8B%8D%E6%91%84/">拍摄</category>
<comments>https://greyishsong.ink/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%EF%BC%9A%E6%98%A5%E5%B7%B2%E5%BD%92%E6%9D%A5/#disqus_thread</comments>
</item>
<item>
<title>SDN实验(一):Mininet的安装问题与Fat-Tree的构建</title>
<link>https://greyishsong.ink/SDN%E5%AE%9E%E9%AA%8C%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9AMininet%E7%9A%84%E5%AE%89%E8%A3%85%E9%97%AE%E9%A2%98%E4%B8%8EFat-Tree%E7%9A%84%E6%9E%84%E5%BB%BA/</link>
<guid>https://greyishsong.ink/SDN%E5%AE%9E%E9%AA%8C%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9AMininet%E7%9A%84%E5%AE%89%E8%A3%85%E9%97%AE%E9%A2%98%E4%B8%8EFat-Tree%E7%9A%84%E6%9E%84%E5%BB%BA/</guid>
<pubDate>Fri, 26 Mar 2021 15:38:14 GMT</pubDate>
<description><p>最初因为大创项目,接触了 OpenStack 相关的知识。但重点不在网络,所以对于其中可以随便定义、删除的网络只是有点好奇,没有多想。恰逢软件定义网络课程实验之要求,上手尝试 SDN 的相关实验,试着折腾这个不怕玩坏的网络。</p>
<p>然而尝试之初不免遇到问题,在此简短记述以备查阅,并可作同道者参考。如有问题,欢迎斧正。</p></description>
<content:encoded><![CDATA[<p>最初因为大创项目,接触了 OpenStack 相关的知识。但重点不在网络,所以对于其中可以随便定义、删除的网络只是有点好奇,没有多想。恰逢软件定义网络课程实验之要求,上手尝试 SDN 的相关实验,试着折腾这个不怕玩坏的网络。</p><p>然而尝试之初不免遇到问题,在此简短记述以备查阅,并可作同道者参考。如有问题,欢迎斧正。</p><span id="more"></span><h2 id="Mininet">Mininet</h2><p>按照<a href="http://mininet.org/">官网</a>上的描述,Mininet 是一个强大的 SDN 实验环境,可以在单机上完成各种网络拓扑的测试。然而如果把他的代码仓库 clone 下来,会发现代码量真的不是很大——不像是一个大型框架的样子。</p><p>大致阅读一下源码,或者将其在<code>debug</code>级别下启动,就会发现 Mininet 是通过包装 SDN 工具命令来实现构建拓扑的,所以其核心管理功能并不包含软件定义网络的数据平面;而重要的控制节点,也使用外部的<code>ovs-testcontroller</code>或者<code>pox</code>、<code>ryu</code>等等。本质上,Mininet 用于构建网络的操作,也<strong>都是可以手动完成</strong>的。不过 Mininet 用一个进程来模拟一个 host,让每个 host 可以调用宿主机上几乎所有的程序,确实带来了非常大的方便。</p><h3 id="安装">安装</h3><p><a href="http://mininet.org/download/">官方文档</a>已经给出了各种安装方案,截至目前,各种方案的版本和环境如下:</p><ul><li>虚拟机镜像:官方已迁移至 Ubuntu 20.04 LTS,在 GitHub Releases 上可以下载。</li><li>包管理器:Ubuntu 18.04/20.04 LTS 下,均更新到2.2版本。</li><li>源码安装:最新的稳定版本为2.3.0版本。</li></ul><p>需要注意的是,即使是最新的 Ubuntu 发行版,软件源中也没有最新版的 Mininet 。考虑到 Mininet 是大学开发的教学实验用软件,我认为在新系统下,追最新版本是没问题的。当前 Mininet 官方已经完成了向 Python3 的迁移,也鼓励用户迁移到 Python3,不过 POX 控制器仍然只能在 Python2 下运行。</p><p>我本机的环境是 Kubuntu 20.04 LTS, Mininet 2.3.0,附带安装了 POX, Ryu 和 Wireshark。很推荐源码安装时的<code>-w</code>选项,安装后 Wireshark 就能分清 OpenFlow 的不同 tag,从而能单独抓取某个虚拟交换机的数据。</p><h3 id="问题排查和解决">问题排查和解决</h3><p>其实这个问题出现得挺让我哭笑不得的。</p><p>安装之初,我按照文档说明,进行最简单的测试:</p><figure><div class="code-wrapper"><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">$ <span class="token function">sudo</span> mn <span class="token parameter variable">--test</span> pingall<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre></div></figure><p>只见每个节点之间 ping 的结果都正常,Mininet 的统计结果却非说节点都不通,还有报错:</p><figure><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">h1 -> h2*** Error: could not parse ping output: PING h2 (10.0.0.2) 56(84) bytes of data.64 比特,来自 h2 (10.0.0.2): icmp_seq=1 ttl=49 时间=35.4 毫秒--- h2 ping 统计 ---已发送 1 个包, 已接收 1 个包, 0% 包丢失, 耗时 0 毫秒rtt min/avg/max/mdev = 35.388/35.388/35.388/0.000 msX<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div></figure><p>翻遍了文档、GitHub Issue、StackOverflow,我也没找到什么解释。不服得很,遂翻找源码。源码组织得比较整齐,于是很快找到了解析 ping 输出的部分:就在<code>mininet/mininet/net.py</code>当中,是<code>Mininet</code>类的一个方法。</p><figure><div class="code-wrapper"><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token decorator annotation punctuation">@staticmethod</span><span class="token keyword">def</span> <span class="token function">_parsePing</span><span class="token punctuation">(</span> pingOutput <span class="token punctuation">)</span><span class="token punctuation">:</span> <span class="token string">"Parse ping output and return packets sent, received."</span> <span class="token comment"># Check for downed link</span> <span class="token keyword">if</span> <span class="token string">'connect: Network is unreachable'</span> <span class="token keyword">in</span> pingOutput<span class="token punctuation">:</span> <span class="token keyword">return</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token number">0</span> r <span class="token operator">=</span> <span class="token string">r'(\d+) packets transmitted, (\d+)( packets)? received'</span> m <span class="token operator">=</span> re<span class="token punctuation">.</span>search<span class="token punctuation">(</span> r<span class="token punctuation">,</span> pingOutput <span class="token punctuation">)</span> <span class="token keyword">if</span> m <span class="token keyword">is</span> <span class="token boolean">None</span><span class="token punctuation">:</span> error<span class="token punctuation">(</span> <span class="token string">'*** Error: could not parse ping output: %s\n'</span> <span class="token operator">%</span> pingOutput <span class="token punctuation">)</span> <span class="token keyword">return</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token number">0</span> sent<span class="token punctuation">,</span> received <span class="token operator">=</span> <span class="token builtin">int</span><span class="token punctuation">(</span> m<span class="token punctuation">.</span>group<span class="token punctuation">(</span> <span class="token number">1</span> <span class="token punctuation">)</span> <span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token builtin">int</span><span class="token punctuation">(</span> m<span class="token punctuation">.</span>group<span class="token punctuation">(</span> <span class="token number">2</span> <span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token keyword">return</span> sent<span class="token punctuation">,</span> received<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div></figure><p>一看之下,直笑过去了。本以为是调了哪个系统级的 lib 之类,拿着返回值在解析,谁知是直接把<code>ping</code>命令的输出文字拿来正则匹配了,真有点儿倒拔垂杨柳式的蛮力。</p><p>问题其实已经明了:我的 Linux 是双系统,打算长期使用的,因此配置了中文环境、中文输入法种种,<code>ping</code>命令的输出<strong>也被本地化成了中文</strong>。这样一来,用来匹配英文输出的正则永远失败,Mininet 就认为<code>ping</code>都不通。</p><p>我考虑的解决方法是设置临时环境变量。根据以前对 Linux 的了解,本地化工作应该受<code>LANG</code>这个环境变量控制,于是改变运行 Mininet 的方式:</p><figure><div class="code-wrapper"><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">$ <span class="token function">sudo</span> <span class="token assign-left variable"><span class="token environment constant">LANG</span></span><span class="token operator">=</span>en_US mn <span class="token parameter variable">--test</span> pingall<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre></div></figure><p>以期 Linux 按照<code>en_US</code>来本地化输出,避免输出中文。结果是成功的,不过总这样有点麻烦,可以设置别名:</p><figure><div class="code-wrapper"><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token builtin class-name">alias</span> <span class="token assign-left variable">sudo</span><span class="token operator">=</span><span class="token string">'sudo '</span><span class="token builtin class-name">alias</span> <span class="token assign-left variable">mn</span><span class="token operator">=</span><span class="token string">'LANG=en_US mn'</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre></div></figure><p>第一条让<code>sudo</code>命令也查找别名(默认是不查找的),第二条就是设置语言环境。这两个别名要全局生效,可以放在<code>/etc/profile</code>或者<code>/etc/bash.bashrc</code>当中。</p><blockquote><p>装系统只为用来实验、语言环境是英文的同学,完全不会有这个问题——这就是各种资料上找不到的原因。</p></blockquote><h2 id="Fat-Tree-拓扑的搭建">Fat-Tree 拓扑的搭建</h2><blockquote><p>关于 Fat-Tree,我参考的主要是这篇文章:<a href="https://blog.csdn.net/baidu_20163013/article/details/110004560">Fat-tree:A Scalable, Commodity Data Center Network Architecture 解读</a></p></blockquote><p>在构建 Fat-Tree 时,我是用自底向上的顺序连接节点,将每个 Pod 都存放在一个 Object 里。具体代码就不贴了,想来思路上大同小异,不会有太多的差别。</p><p>在实现了一个<code>FatTreeTopo</code>类之后,我用一个小函数来测试:</p><figure><div class="code-wrapper"><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">doTest</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span> topo <span class="token operator">=</span> FatTreeTopo<span class="token punctuation">(</span>k<span class="token operator">=</span><span class="token number">4</span><span class="token punctuation">)</span> net <span class="token operator">=</span> Mininet<span class="token punctuation">(</span>topo<span class="token punctuation">)</span> net<span class="token punctuation">.</span>start<span class="token punctuation">(</span><span class="token punctuation">)</span> net<span class="token punctuation">.</span>waitConnected<span class="token punctuation">(</span><span class="token punctuation">)</span> CLI<span class="token punctuation">(</span>net<span class="token punctuation">)</span> net<span class="token punctuation">.</span>stop<span class="token punctuation">(</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div></figure><p>然而在解决了上面的一切问题后,还是所有的<code>ping</code>都不通,且这次是真的不通了,<code>h1 ping -c1 h4</code>这样的命令显示完全是访问不到的。于是先尝试着建一个 k = 2 的 Fat-Tree,每个 Pod 只有两台交换机,方便调试。奇怪的是,在 k = 2 的情况下,一切都工作正常。</p><p>在没有头绪的情况下,我只好重新建起 k = 4 的拓扑,再打开 Wireshark 抓包。这里让<code>h1</code>连续尝试<code>ping</code>另一个 Pod 的主机<code>h6</code>,让 Wireshark 抓取连接了<code>h1</code>的边缘交换机上通过的数据包,过滤规则是<code>icmp</code>。当我在<code>h1</code>上发起<code>ping</code>时,发现根本抓不到想要的 ICMPv4 数据包,只有疯狂刷屏的 ICMPv6 包,<code>h1</code>发出的 ARP 包完全淹没在其中,<code>h6</code>连 ARP 应答都没有作出。</p><p>又是一阵好找,找着找着摸到一点线索:k = 4 和 k = 2 情况最大的不同,应该是拓扑中<strong>出现了环路</strong>。这时,用来探测网络连接、更新路由等信息的数据包可能形成广播风暴,让网络处于极度拥塞的状态下。最终搜出了官方 Wiki 上的一条解答:</p><blockquote><p><a href="https://github.com/mininet/mininet/wiki/FAQ#ethernet-loops">Why does my controller, which implements an Ethernet bridge or learning switch, not work with my network which has loops in it? I can't ping anything!</a></p></blockquote><p>其中提到,在这种情况下,应该为网络开启生成树协议(STP),这样可以明确转发路径,进而避免数据包在一个环上来回转发。对应到实现上,需要修改的是<code>addSwitch</code>部分,为其增加一些参数:</p><figure><div class="code-wrapper"><pre class="line-numbers language-python" data-language="python"><code class="language-python">switchOpts <span class="token operator">=</span> <span class="token punctuation">{</span><span class="token string">'cls'</span><span class="token punctuation">:</span> OVSBridge<span class="token punctuation">,</span><span class="token string">'stp'</span><span class="token punctuation">:</span> <span class="token number">1</span><span class="token punctuation">}</span>addSwitch<span class="token punctuation">(</span><span class="token string">'sxx'</span><span class="token punctuation">,</span> <span class="token operator">**</span>switchOpts<span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre></div></figure><p>这里指定使用<code>OVSBridge</code>类型的交换机,<code>stp=1</code>开启 STP 协议。另外,开启 STP 后需要等待交换机连接,所以脚本中</p><figure><div class="code-wrapper"><pre class="line-numbers language-python" data-language="python"><code class="language-python">net<span class="token punctuation">.</span>waitConnected<span class="token punctuation">(</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre></div></figure><p>是必不可少的,在连接上之前,网络都是不通的。或者也可以在 CLI 下调用这个方法:(启动时创建的<code>Mininet</code>实例一般都叫做<code>net</code>)</p><figure><div class="code-wrapper"><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">mininet<span class="token operator">></span> py net.waitConnected<span class="token punctuation">(</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre></div></figure><p>再启动拓扑进行测试,一切就都符合预期了。</p><h2 id="一些其他用途">一些其他用途</h2><p>Mininet 用来当网络测试工具也不错。在启动时,加上 NAT 选项,就可以让 host 接入外部网络:</p><figure><div class="code-wrapper"><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">$ <span class="token function">sudo</span> mn <span class="token parameter variable">--nat</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre></div></figure><p>这个网络环境非常干净、简单(没防火墙、没拥塞,甚至没多少流量),还比用多台虚拟机组网轻量许多。通过 Xterm 或者 SSH 连接到 host 之后,网络实验中的 ARP 攻击、DNS 攻击等等都可以在这里尝试,并且<strong>完全没有后遗症</strong>。<code>sudo mn -c</code>之后,一切灰飞烟灭不留痕迹,纵然南朝四百八十寺,再无楼台烟雨中。</p><blockquote><p>一点小问题:host 的 DNS 解析跟随宿主机设置,所以如果系统中有<code>systemd-resolved</code>这个服务,需要暂时将<code>/etc/resolv.conf</code>这个文件中的<code>nameserver</code>项修改成一个可用的 DNS 服务器地址。原先的<code>nameserver</code>一般是<code>127.0.0.53</code>,这是<code>systemd-resolved</code>维护的一个本地 DNS,经过它指向真正的 DNS 服务器,但是 Mininet 并不能使用这个东西。注意玩完之后,还是把这一项该回来为好,虽说哪怕不改重启后也会恢复。</p></blockquote><p>另外,在<code>addLink</code>时,Mininet 提供了限制带宽的选项。如果想要观察拥塞之类的现象,完全可以在添加连接时就把带宽限得很低,本机上的网络环境总是比复杂的外网要好下手许多。</p>]]></content:encoded>
<category domain="https://greyishsong.ink/categories/%E7%BC%96%E7%A8%8B/">编程</category>
<category domain="https://greyishsong.ink/tags/SDN/">SDN</category>
<category domain="https://greyishsong.ink/tags/%E8%BD%AF%E4%BB%B6%E5%AE%9A%E4%B9%89%E7%BD%91%E7%BB%9C/">软件定义网络</category>
<category domain="https://greyishsong.ink/tags/Mininet/">Mininet</category>
<comments>https://greyishsong.ink/SDN%E5%AE%9E%E9%AA%8C%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9AMininet%E7%9A%84%E5%AE%89%E8%A3%85%E9%97%AE%E9%A2%98%E4%B8%8EFat-Tree%E7%9A%84%E6%9E%84%E5%BB%BA/#disqus_thread</comments>
</item>
<item>
<title>连接回忆的断层(二)</title>
<link>https://greyishsong.ink/%E8%BF%9E%E6%8E%A5%E5%9B%9E%E5%BF%86%E7%9A%84%E6%96%AD%E5%B1%82%EF%BC%88%E4%BA%8C%EF%BC%89/</link>
<guid>https://greyishsong.ink/%E8%BF%9E%E6%8E%A5%E5%9B%9E%E5%BF%86%E7%9A%84%E6%96%AD%E5%B1%82%EF%BC%88%E4%BA%8C%EF%BC%89/</guid>
<pubDate>Wed, 20 May 2020 05:01:24 GMT</pubDate>
<description><p>这一篇是看过一些关于人文主义、早期自然科学、分析法发展过程的解释之后记下来的所见所想。下笔时,不由感慨自己居于当今时代、工程学学生的身份,视角受限不浅,思维导向也真是难以动摇。因此而展现的种种顽固,也许会博明日之我一笑,不过今日之我的思维既然如此,就值得记录下来,并向读者讨教。</p></description>
<content:encoded><![CDATA[<p>这一篇是看过一些关于人文主义、早期自然科学、分析法发展过程的解释之后记下来的所见所想。下笔时,不由感慨自己居于当今时代、工程学学生的身份,视角受限不浅,思维导向也真是难以动摇。因此而展现的种种顽固,也许会博明日之我一笑,不过今日之我的思维既然如此,就值得记录下来,并向读者讨教。</p><span id="more"></span><h2 id="人文主义者的骄傲与资产阶级的美德">人文主义者的骄傲与资产阶级的美德</h2><p>研究自然和它的演进,这是从人类启蒙伊始就存在的活动,这种活动在文艺复兴的年代得到了空前的发展,甚至从闲时的消遣成为社会的主流。</p><p>这一活动的兴盛通常被简单地描述为:人文主义的兴起让神学在思想领域作出了巨大的让步,自然科学在很大程度上填补了这种空隙。但是我所看到的情况比这更复杂。</p><p><br><br></p><h3 id="人文主义者的骄傲?">人文主义者的骄傲?</h3><p>书中有一段话,作者阐述了他对16世纪欧洲人自我地位认知的见解:</p><blockquote><p>他也并没有做到现代教科书中所说的:‘科学使人认识到自己的渺小和卑微;哥白尼把人从宇宙的中心拉开;达尔文把人降到和动物同等的地位;弗洛伊德把理性拉下宝座,代之以本能。’<br>关于后两句话,以后会作评论。第一句话是未经思考的轻率之语:过去的人自认为是可怜的罪人,害怕震怒的上帝会用瘟疫、饥馑和地震来惩罚他们;他们相信撒旦‘狂暴地在世界上任意游荡’,把受害者打入地狱永世不得翻身;他们为了求得圣人和圣物的保佑而经历千辛万苦,又是去朝圣,又是自贬自卑;处于这种状况的人有什么骄傲可言呢?人文主义者的确感受到了人的尊严,那是因为人的力量创造出了奇迹,而不是因为人在宇宙中的位置。不管托勒密或哥白尼的理论如何,人还是在上帝之下。蒙田认为人并没有感到骄傲的理由。说中世纪和近代早期的人自谓:‘我是宇宙的中心,这是多么的辉煌啊!’这完全是几世纪之后唯科学主义的杜撰。</p></blockquote><p>从人自身的情感角度,我完全赞同作者的论断。如果一个骄傲的人赖以骄傲的理由破灭,那么他失魂落魄——或者歇斯底里,应该是比较正常的反应。前者导致崩溃,后者催生报复。但是哥白尼、伽利略、布鲁诺时代的教会和基督教既没有因此没落,也没有因此报复。这些早期科学工作者给教会带来的威胁,还没有新教各宗的冲击大;教会判处这些人以刑罚,也不是因为他们的种种发现,而是因为持神秘主义观点(异端)、相信魔法(异教)等等其实相当传统的罪名。恰恰相反的是,他们的研究经常受到天主教人士的支持(通常是在资金和政治上)。</p><p>但是从客观情况的角度,我实在不能同意“人文主义者之前的人,没有骄傲可言”这样的说法。我国的法律判处刑罚的首要依据是犯罪者的行为而不是动机,例如过失伤人者,并不能、也不应该逃脱伤人所需承担的法律责任(当然,会因为没有动机而减轻一些)。同样的,看待那个时代的人有没有骄傲,更应该看重的难道不是他们的行为是否表现出下意识的傲慢吗?</p><p>基督教的信仰强调人的原罪,要求信徒谦卑,这是从信徒的主观心理来看的;然而,并不是如圣经所说,“上帝按照他的形象造人”,相反:人按照自己的形象造神。在一切神的故事中,神的选民永远是人,人以外的一切生物不仅从构造上被放在人以下,还要被人的道德划分到人以下,这甚至普遍到了被忽略的地步。这不是一种骄傲吗?人的谦卑是献给神的,人的恐惧是交给魔鬼的,人的不屑是投向万物的——然而物质世界中存在的只有万物,这就非常有趣了。</p><p>因此,古代人的骄傲,不是主观上的心理感受,而是下意识的行为。人以神魔解释世界,神魔终究是人主观意识的直接拓展,那么人实则以自己为中心解释世界,并赋予世界以“意义”。这不是最大的骄傲吗?人不过是对自己的观念世界谦卑。这种谦卑在拥有一致共识的社群中会表现出来,因为这观念世界并不是某一个人的而是整个社群的;然而面对相冲的观念世界时就表现出了它本质的傲慢。</p><p>大航海时代,欧洲的殖民者对其他民族的看法正如此。远征的基督徒面对同样具有“自我中心”世界观的亚洲文明,始终认为自己高人一等;而面对持泛灵论信仰的美洲和非洲土著,常常干脆视之为尚未脱离本能的动物。恰恰是“骄傲”的人文主义者(同样有许多是基督徒),为其他民族作为“人”的地位辩护,或者更进一步地研究其他文明的价值观念和对外认识。</p><p>不过,也很难说,“人文主义是推进科学的第一力量”。无论如何,早期的人文主义者常与罗马教廷有所冲突,但新教加尔文宗和路德宗对自然科学都没有多少兴趣,加尔文宗这样的分支甚至会反对科学研究。相反的是,继承了经院哲学的研究传统和大部分已有的自然认识的天主教会却经常性地成为了研究者的支持者。</p><p>不过这种思想潮流中,确实产生了两个变化,对科学工作影响不小。其一是世俗化带来的资产阶级道德观,其二是学界传播风气的兴起。</p><p><br><br></p><h3 id="文化潮流对科学研究的影响">文化潮流对科学研究的影响</h3><p>在世俗化的浪潮中,手工业、商业和城市迎来了兴盛,在市民中产生了之后的资产阶级。这些人的某些作风,似乎在科学工作者身上遗传了下来。书中写道:</p><blockquote><p>回顾过去,可以看到商人或银行家的某些习惯对科学工作者也是有用的。对细节一丝不苟,注意小数字,要求资料精确,这些并不是贵族的特征,而是卑贱的生意人的特征。<br>……<br>我们可以毫不夸张地说,一个沉浸在研究中的科学家是资产阶级美德的楷模。</p></blockquote><p>对此我并没有深入的了解,所以没有什么可多说的。不过有一点有趣的推断:那个年代里,许多著名的科学家并非商人,而是传统贵族的继承人——比如说,天才的实验物理学家卡文迪许<sup><a href="#link1">[1]</a></sup>。那么如果作者所言无误,资产阶级的生活方式和道德习惯已经在相当程度上影响到了“传统的”贵族家庭。后来的科学家们中,除了贵族继承人之外,富商、律师这样市民阶层的孩子也屡见不鲜,从法国的拉瓦锡<sup><a href="#link1">[2]</a></sup>到英国的牛顿,科学工作者更多地拥有商业时代才产生的出身。</p><p>另一种风潮是各种小册子的流行。在早些的时候还没有期刊这种概念,但是随着印刷术的普及,印刷的成本大幅度降低,各路作者能够快速、小批量地印制自己的作品,这些不那么正式的小册子成为了一种交流的时尚形式。书中提到:</p><blockquote><p>在交流的最初阶段,出版当然是一种手段,哥白尼、伽利略、培根、笛卡尔、波意耳等人的著作就是最好的例子。</p></blockquote><p>对于进入到公共讨论领域的出版物而言,成为相互批驳乃至攻讦的载体并不稀奇。但是无论如何这是一种巨大的进步,因为新的形式与传统的公共演讲、小型交流会相比大大增加了信息交流的带宽。甚至未必不可以说,空前便利的观点交锋地改变了知识界对交流的态度。</p><blockquote><p>炼金术士都是在密室里操作,……,他是不愿意同别人分享荣耀和利益的。医生们对自己的医术也秘而不宣。……从17世纪起,科学家和几何学家开始采用相反的思维和行为方式。他们从经验中认识到,伟大的真理是一点一点地发现的(培根指出了这一点),互相审查和纠正对大家都有好处。</p></blockquote><p><br><br></p><h2 id="埋藏在田园沃土中的利剑">埋藏在田园沃土中的利剑</h2><blockquote><p>大概希腊人的先祖实在不会想到,他们的测地术会在后来开辟了一条从未存在于世上的道路——geometry。</p></blockquote><p>在大概16到18世纪的数学和自然科学研究中,几何学所处的地位非常奇特。按照今人的理念,几何是不过是数学的一个分支,轮不到它来挥动引领数学与自然科学发展的红旗。但在那个年代,事实就是如此。在高中数学和物理教材中分别提到,代数学和力学的公理化过程都是参照几何学的公理化体系进行的。并且那个年代的分析学也带有非常浓厚的几何味道。</p><p>比如说,从数学上推导开普勒第二定律(面积定律)这件工作,我过去一直误以为牛顿是用微积分完成,直到大学的物理老师讲到这段历史,才知道他用的是几何推导而不是自己发明的流数术(微积分)。高数(也许是线代)老师也讲到过,牛顿和莱布尼茨年代的微积分和今天的有很大不同,论证中含有更多几何的成分,而不像今天这样代数化。</p><p>几何在早年能够成为数学和自然科学中“唯一可靠的部分”(这个形容不是我说的),是因为它的思维方式是独创的:首先从实物中抽象出概念,再以一组假定的概念作为出发点,将一切几何问题都归结到用直接用这些概念作出判断。</p><p>作者举了一个例子,以说明几何的抽象特征:</p><blockquote><p>三角形的木框子并不被看成一个三角形;……教科书上印的三角形图形也不是几何三角形;它只是提示了三角形的定义和从中可以推理出来的特征。</p></blockquote><p>而数学和自然科学处理问题的分析法(analysis),</p><blockquote><p>分析是抽象的一种形式,因为它把每个研究对象都视作一座钟,由不同的部件组成,它们与其他同类的部件别无二致。拿上述的例子来说,所有的犯罪、所有的离婚、所有的居民和所有的肺癌都是相同的单位。</p></blockquote><p>再看看帕斯卡对人思维的划分是“几何思维”<sup><a href="#link3">[3]</a></sup>和“本能思维”<sup><a href="#link4">[4]</a></sup>,以及作者对此的解释:</p><blockquote><p>帕斯卡所谓的几何,指的是研究科学或数学中精确定义和抽象概念时的思维方式,而所谓本能,指的是考虑没有确切定义的思想和概念时的思维方式。</p></blockquote><p>就让我感到,几何的特殊地位,根本上来说在于它是分析的起源。</p><blockquote><p>这种方法到底是什么?是仔细认真地研究任何一个问题,把它分成不同的部分,然后分别处理各个部分,这比一下子处理整个问题要容易得多。最后,再把各部分重新组合起来,并且要保证不遗漏其中任何一部分。<br>……<br>简而言之,这种方法是分析(analysis),这是个希腊词,意思是‘拆散’。它是科学的理想方法,不仅因为它是标准化的,而且也是因为它认定任何一个研究对象都是由不同的部分组成的,是一种机制。</p></blockquote><p>几何本身的起源也是生活化的,geometry是个源自希腊语的词,本意是“测地术”,几何就是测量大地的学科。但是从田园的沃土中,人们发掘出了这把完全不同的利剑。</p><p>一直到16世纪,自然科学(自然哲学)都不否认自然本身是有意义的,并且都接受一些赋予自然的意义:</p><blockquote><p>16世纪的哲学家,不管是传统派还是激进派,脑子里都装满了从古人那里继承下来的思想。人文主义者都熟读普林尼那部观察与幻想混淆在一起的大作《自然史》。</p></blockquote><p>然而分析法之于自然科学,不仅是从几何学中引进了一种思维,更引入了这种思维的根基,也就是抽象的概念。</p><blockquote><p>科学如果要从以往的研究中升华出来,必须弄懂并且完全接受一个奇怪的想法,那就是物体是纯物质的东西,没有任何特征,这样才可以量化。</p></blockquote><p>为了将物质世界中的对象抽象成观念世界中的概念,必然抹去赋予物质的意义——一个兼做炼金术士的化学家,无论他怎么认为金是神圣的而铁是低贱的,只要他采纳原子论来研究这两种元素,就把这两种东西放在了同样高的平台上,从而在事实上抹去了他自己主观赋予它的意义。他当然可以加以说明,这两种原子也是高低有别的、是有贵贱的。可如果他进一步地拆分和抽象他的研究对象,看向更加普遍的质子、中子和电子,就又破坏了自己的说明。</p><p>可见应用分析法的过程也会是排除物质自身意义的过程,随着这种过程的推进,</p><blockquote><p>早期的一些概念不够几何化,过于诗情画意。它们清楚地但是象征性地反映了宇宙,也就是说充满了含义;而纯物质的东西是没有含义的,它就是它。</p></blockquote><p>这样的想法就会自然地产生。于是自然科学从哲学中脱离出来,挥起这把埋藏在田园沃土中的利剑,斩断了自己连在“温情脉脉的意义”上的脐带,发出了宣言:</p><blockquote><p>自然本身没有审慎、没有做作,也没有智慧。我们以为它有这些特点是因为我们根据自己特有的能力和思维方式去对自然中天赐的东西进行判断。<sup><a href="#link5">[5]</a></sup><br>……<br>亚里士多德的物理学家依赖于目的原理,认为任何事物都有其最终的目的和意义。与其相反的假设产生的才是科学的真理,没有向着目标的推进,只有无目的的运动。</p></blockquote><p><br><br></p><h2 id="后记">后记</h2><p>提及分析法和自然科学时,我会不由自主地表现出赞美的意味。这应该就是所学知识和所在视角的影响了。倒不是要自我否定,但科学本来不是万能,对它的赞美却有让我有“下意识地把分析法当作锤子,把任何事物当作钉子”的危险,值得警惕。下一篇想要梳理的就是关于科学式的思维、理性主义和“本能思维”的问题。</p><p><br><br><br></p><h2 id="参考和注释">参考和注释</h2><p><a name="link1" id="link1">[1]</a>以测量万有引力常量的扭秤实验而著称,一生孤僻低调,许多成果死后才被发现。他所在的卡文迪许家族有德文郡公爵的世袭爵位。</p><p><a name="link2" id="link2">[2]</a>成功地分解了不少原先认为的“基本物质”,发现了数种元素单质的存在,被誉为现代化学之父。</p><p><a name="link3" id="link3">[3]</a>帕斯卡对此的描述是:“一种是严格的、固定的”。</p><p><a name="link4" id="link4">[4]</a>帕斯卡对此的描述是:“而另一种是有弹性的”。</p><p><a name="link5" id="link5">[5]</a>原书引述自威廉·哈维(William Harvey,见<a href="https://zh.wikipedia.org/wiki/%E5%A8%81%E5%BB%89%C2%B7%E5%93%88%E7%BB%B4">维基百科页面</a>或<a href="https://baike.baidu.com/item/%E5%A8%81%E5%BB%89%C2%B7%E5%93%88%E7%BB%B4">百度百科页面</a>)。</p>]]></content:encoded>
<category domain="https://greyishsong.ink/categories/%E9%9A%8F%E7%AC%94/">随笔</category>
<category domain="https://greyishsong.ink/tags/%E9%98%85%E8%AF%BB/">阅读</category>
<category domain="https://greyishsong.ink/tags/%E5%8E%86%E5%8F%B2/">历史</category>
<comments>https://greyishsong.ink/%E8%BF%9E%E6%8E%A5%E5%9B%9E%E5%BF%86%E7%9A%84%E6%96%AD%E5%B1%82%EF%BC%88%E4%BA%8C%EF%BC%89/#disqus_thread</comments>
</item>
</channel>
</rss>