-
Notifications
You must be signed in to change notification settings - Fork 26
/
11-控制结构.texi
1511 lines (1040 loc) · 69 KB
/
11-控制结构.texi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
\input texinfo @c -*- texinfo -*-
@c %**start of header
@setfilename 11-控制结构.info
@settitle
@documentencoding UTF-8
@documentlanguage en
@c %**end of header
@finalout
@titlepage
@title
@author zunrong
@end titlepage
@contents
@ifnottex
@node Top
@top
@end ifnottex
@menu
* 11 控制结构::
@detailmenu
--- The Detailed Node Listing ---
11 控制结构
* 11.1 测序: 111 测序.
* 11.2 条件: 112 条件.
* 11.3 组合条件的构造: 113 组合条件的构造.
* 11.4 模式匹配条件: 114 模式匹配条件.
* 11.5 迭代: 115 迭代.
* 11.6 生成器: 116 生成器.
* 11.7 非本地出口: 117 非本地出口.
11.4 模式匹配条件
* 11.4.1 pcase宏: 1141 pcase宏.
* 11.4.2 扩展 pcase: 1142 扩展 pcase.
* 11.4.3 反引号样式模式: 1143 反引号样式模式.
* 11.4.4 解构 pcase模式: 1144 解构 pcase模式.
11.7 非本地出口
* 11.7.1 显式非本地出口: catch和 throw: 1171 显式非本地出口: catch和 throw.
* 11.7.2 示例 catch和 throw: 1172 示例 catch和 throw.
* 11.7.3 错误: 1173 错误.
* 11.7.4 清理非本地出口: 1174 清理非本地出口.
@end detailmenu
@end menu
@node 11 控制结构
@chapter 11 控制结构
Lisp 程序由一组表达式或表单组成(请参阅表单种类)。我们通过将它们封装在控制结构中来控制这些表单的执行顺序。控制结构是特殊形式,它控制何时、是否或多少次执行它们所包含的表单。
最简单的执行顺序是顺序执行:首先形成 a,然后形成 b,依此类推。当您在函数体中或在 Lisp 代码文件的顶层连续编写多个表单时,就会发生这种情况——表单按照编写的顺序执行。我们称之为文本顺序。例如,如果函数体由 a 和 b 两种形式组成,则函数的求值首先求值 a,然后求值 b。计算 b 的结果成为函数的值。
显式控制结构使执行顺序成为可能,而不是顺序。
Emacs Lisp 提供了多种控制结构,包括其他种类的排序、条件、迭代和(受控)跳转——所有这些都将在下面讨论。内置控制结构是特殊形式,因为它们的子形式不一定要评估或不按顺序评估。您可以使用宏来定义自己的控制结构构造(请参阅宏)。
@menu
* 11.1 测序: 111 测序.
* 11.2 条件: 112 条件.
* 11.3 组合条件的构造: 113 组合条件的构造.
* 11.4 模式匹配条件: 114 模式匹配条件.
* 11.5 迭代: 115 迭代.
* 11.6 生成器: 116 生成器.
* 11.7 非本地出口: 117 非本地出口.
@end menu
@node 111 测序
@section 11.1 测序
按表单出现的顺序评估表单是控制从一种表单传递到另一种表单的最常见方式。在某些情况下,例如在函数体中,这会自动发生。在其他地方,您必须使用控制结构结构来执行此操作:progn,Lisp 最简单的控制结构。
progn 特殊形式如下所示:
@lisp
(progn a b c …)
@end lisp
它说按顺序执行表格 a、b、c 等等。这些形式称为 progn 形式的主体。正文中最后一个形式的值成为整个 progn 的值。(progn) 返回零。
在 Lisp 的早期,progn 是连续执行两个或多个表单并使用最后一个的值的唯一方法。但是程序员发现他们经常需要在函数体中使用 progn,而(当时)只允许一种形式。因此,函数的主体被制作成隐式的 progn:允许使用多种形式,就像在实际 progn 的主体中一样。许多其他控制结构同样包含一个隐含的 progn。因此,progn 的使用量不如多年前。现在最常在 unwind-protect 中使用它,并且,或,或 if 的 then 部分。
@lisp
Special Form: progn forms… ¶
@end lisp
@lisp
(progn (print "The first form")
(print "The second form")
(print "The third form"))
-| "The first form"
-| "The second form"
-| "The third form"
⇒ "The third form"
@end lisp
这种特殊形式以文本顺序评估所有形式,返回最终形式的结果。
另外两个构造同样评估一系列形式,但返回不同的值:
@lisp
Special Form: prog1 form1 forms… ¶
@end lisp
这种特殊的形式以文本顺序计算 form1 和所有的形式,返回 form1 的结果。
@lisp
(prog1 (print "The first form")
(print "The second form")
(print "The third form"))
-| "The first form"
-| "The second form"
-| "The third form"
⇒ "The first form"
@end lisp
这是一种从变量 x 的列表中删除第一个元素,然后返回前一个元素的值的方法:
@lisp
(prog1 (car x) (setq x (cdr x)))
@end lisp
@lisp
Special Form: prog2 form1 form2 forms… ¶
@end lisp
这种特殊形式按文本顺序计算 form1、form2 和以下所有形式,并返回 form2 的结果。
@lisp
(prog2 (print "The first form")
(print "The second form")
(print "The third form"))
-| "The first form"
-| "The second form"
-| "The third form"
⇒ "The second form"
@end lisp
@node 112 条件
@section 11.2 条件
条件控制结构在备选方案中进行选择。Emacs Lisp 有五种条件形式:if,这和其他语言很相似; when 和 unless,它们是 if 的变体; cond,这是一个广义的案例陈述; 和 pcase,它是 cond 的概括(参见 Pattern-Matching Conditional)。
@lisp
Special Form: if condition then-form else-forms… ¶
@end lisp
if 根据条件的值在 then-form 和 else-forms 之间进行选择。如果评估的条件不为 nil,则评估 then-form 并返回结果。否则,else-forms 将按文本顺序进行评估,并返回最后一个的值。(if 的 else 部分是隐式 progn 的一个示例。请参阅 Sequencing。)
如果 condition 的值为 nil,并且没有给出 else-forms,则 if 返回 nil。
if 是一种特殊形式,因为未选择的分支永远不会被评估——它被忽略。因此,在此示例中,不会打印 true ,因为从不调用 print:
@lisp
(if nil
(print 'true)
'very-false)
⇒ very-false
@end lisp
@lisp
Macro: when condition then-forms… ¶
@end lisp
这是 if 的一种变体,其中没有 else-forms,并且可能有几个 then-forms。尤其,
@lisp
(when condition a b c)
@end lisp
完全等同于
@lisp
(if condition (progn a b c) nil)
@end lisp
@lisp
Macro: unless condition forms… ¶
@end lisp
这是没有 then 形式的 if 的变体:
@lisp
(unless condition a b c)
@end lisp
完全等同于
@lisp
(if condition nil
a b c)
@end lisp
@lisp
Special Form: cond clause… ¶
@end lisp
cond 在任意数量的备选方案中进行选择。cond 中的每个子句都必须是一个列表。此列表的 CAR 是条件; 其余的元素,如果有的话,身体形式。因此,一个子句如下所示:
@lisp
(condition body-forms…)
@end lisp
cond 通过评估每个子句的条件,按文本顺序尝试子句。如果条件的值为非零,则该子句成功; 然后 cond 评估它的 body-forms,并返回最后一个 body-forms 的值。任何剩余的子句都将被忽略。
如果 condition 的值为 nil,则该子句失败,因此 cond 转到下一个子句,尝试其条件。
子句也可能如下所示:
@lisp
(condition)
@end lisp
然后,如果条件在测试时不为零,则 cond 形式返回条件的值。
如果每个条件的计算结果都为 nil,因此每个子句都失败,则 cond 返回 nil。
以下示例有四个子句,分别测试 x 的值是数字、字符串、缓冲区和符号的情况:
@lisp
(cond ((numberp x) x)
((stringp x) x)
((bufferp x)
(setq temporary-hack x) ; multiple body-forms
(buffer-name x)) ; in one clause
((symbolp x) (symbol-value x)))
@end lisp
当前面的子句都没有成功时,我们经常希望执行最后一个子句。为此,我们使用 t 作为最后一个子句的条件,如下所示:(t body-forms)。形式 t 计算为 t,它永远不会是 nil,所以这个子句永远不会失败,只要 cond 得到它。例如:
@lisp
(setq a 5)
(cond ((eq a 'hack) 'foo)
(t "default"))
⇒ "default"
@end lisp
如果 a 的值为 hack,则此 cond 表达式返回 foo,否则返回字符串 @code{default} 。
任何条件构造都可以用 cond 或 if 表示。因此,它们之间的选择是风格问题。例如:
@lisp
(if a b c)
≡
(cond (a b) (t c))
@end lisp
@node 113 组合条件的构造
@section 11.3 组合条件的构造
本节描述了经常与 if 和 cond 一起使用来表达复杂条件的结构。结构和和或也可以单独用作多种条件结构。
@lisp
Function: not condition ¶
@end lisp
此功能测试条件的虚假性。如果条件为 nil,则返回 t,否则返回 nil。函数 not 与 null 相同,如果您正在测试空列表,我们建议使用名称 null。
@lisp
Special Form: and conditions… ¶
@end lisp
和特殊形式测试是否所有条件都为真。它通过按写入的顺序一一评估条件来工作。
如果任何条件的计算结果为 nil,则 and 的结果必须为 nil,而不管其余条件如何; 所以并立即返回 nil ,忽略其余条件。
如果所有条件都非零,那么最后一个条件的值将成为 and 形式的值。Just (and),没有条件,返回 t,因为所有条件都非零。(想想看,哪个没有?)
这是一个例子。第一个条件返回整数 1,它不是 nil。同样,第二个条件返回整数 2,它不是 nil。第三个条件为 nil,因此永远不会评估剩余的条件。
@lisp
(and (print 1) (print 2) nil (print 3))
-| 1
-| 2
⇒ nil
@end lisp
这是一个更实际的使用 and 的例子:
@lisp
(if (and (consp foo) (eq (car foo) 'x))
(message "foo is a list starting with x"))
@end lisp
请注意,如果 (consp foo) 返回 nil,则不执行 (car foo),从而避免错误。
and 表达式也可以使用 if 或 cond 来编写。就是这样:
@lisp
(and arg1 arg2 arg3)
≡
(if arg1 (if arg2 arg3))
≡
(cond (arg1 (cond (arg2 arg3))))
@end lisp
@lisp
Special Form: or conditions… ¶
@end lisp
或特殊形式测试至少一个条件是否为真。它通过按写入的顺序一一评估所有条件来工作。
如果任何条件的计算结果为非零值,则 or 的结果必须为非零; so or 立即返回,忽略其余条件。它返回的值是刚刚评估的条件的非零值。
如果所有条件都为 nil,则 or 表达式返回 nil。Just (or),没有条件,返回 nil,因为所有条件都变成 nil。(想想看,哪个没有?)
例如,这个表达式测试 x 是 nil 还是整数零:
@lisp
(or (eq x nil) (eq x 0))
@end lisp
像 and 构造,or 可以写成 cond。例如:
@lisp
(or arg1 arg2 arg3)
≡
(cond (arg1)
(arg2)
(arg3))
@end lisp
你几乎可以用 if 来写或写,但不完全是:
@lisp
(if arg1 arg1
(if arg2 arg2
arg3))
@end lisp
这并不完全等效,因为它可以计算 arg1 或 arg2 两次。相比之下, (或 arg1 arg2 arg3) 从不多次评估任何参数。
@lisp
Function: xor condition1 condition2 ¶
@end lisp
此函数返回条件 1 和条件 2 的布尔异或。也就是说,如果两个参数都为 nil,或者两者都不是 nil,则 xor 返回 nil。否则,它返回非零参数的值。
请注意,与 or 相比,两个参数总是被评估。
@node 114 模式匹配条件
@section 11.4 模式匹配条件
除了四种基本的条件形式之外,Emacs Lisp 还有一个模式匹配条件形式,pcase 宏,是 cond 和 cl-case 的混合体(参见 Common Lisp Extensions 中的条件),它克服了它们的限制并引入了模式匹配编程风格. pcase 克服的限制是:
cond 形式通过评估其每个子句的谓词条件来在备选方案中进行选择(请参阅条件)。主要限制是条件中的变量对子句的主体形式不可用。
另一个烦恼(与其说是限制,不如说是不便)是,当一系列条件谓词实现相等测试时,会出现大量重复代码。(cl-case 解决了这个不便。)
cl-case 宏通过评估其第一个参数与一组特定值的相等性来在备选方案中进行选择。
它的局限性有两个:
相等性测试使用 eql。
这些值必须事先知道并写入。
这些使 cl-case 不适用于字符串或复合数据结构(例如,列表或向量)。(cond 没有这些限制,但它有其他限制,见上文。)
从概念上讲,pcase 宏借用了 cl-case 的 first-arg 焦点和 cond 的子句处理流程,将 condition 替换为作为模式匹配变体的等式测试的泛化,并添加了设施,以便您可以简洁地表达子句的谓词,并安排在子句的谓词和正文形式之间共享 let 绑定。
谓词的简洁表达称为模式。当调用第一个参数的值的谓词返回非零时,我们说 @code{模式匹配值} (或有时 @code{值匹配模式} )。
@menu
* 11.4.1 pcase宏: 1141 pcase宏.
* 11.4.2 扩展 pcase: 1142 扩展 pcase.
* 11.4.3 反引号样式模式: 1143 反引号样式模式.
* 11.4.4 解构 pcase模式: 1144 解构 pcase模式.
@end menu
@node 1141 pcase宏
@subsection 11.4.1 pcase宏
有关背景,请参阅模式匹配条件。
@lisp
Macro: pcase expression &rest clauses ¶
@end lisp
子句中的每个子句都具有以下形式:(模式主体形式@dots{})。
计算表达式以确定它的值,expval。在模式与 expval 匹配的子句中查找第一个子句,并将控制权传递给该子句的主体形式。
如果匹配,则 pcase 的值是成功子句中最后一个 body-forms 的值。否则,pcase 的计算结果为 nil。
每个模式都必须是一个 pcase 模式,它可以使用下面定义的核心模式之一,或者通过 pcase-defmacro 定义的模式之一(请参阅扩展 pcase)。
本小节的其余部分描述了不同形式的核心模式,提供了一些示例,并以使用某些模式形式提供的 let-binding 工具的重要警告作为结尾。核心模式可以有以下形式:
@lisp
_
@end lisp
匹配任何 expval。这也称为无关或通配符。
@lisp
'val
@end lisp
如果 expval 等于 val,则匹配。比较是通过 equal 来完成的(参见 Equality Predicates)。
@lisp
keyword
@end lisp
@lisp
integer
@end lisp
@lisp
string
@end lisp
如果 expval 等于文字对象,则匹配。这是上面 'val 的一个特例,可能是因为这些类型的字面量对象是自引用的。
@lisp
symbol
@end lisp
匹配任何 expval,另外让 let-binds 符号与 expval 匹配,这样该绑定可用于 body-forms(请参阅动态绑定)。
如果symbol 是排序模式seqpat 的一部分(例如,通过使用and,下面),则绑定也可用于seqpat 出现在symbol 之后的部分。这种用法有一些注意事项,请参阅注意事项。
要避免的两个符号是 t,它的行为类似于 _(上图)并且已被弃用,以及 nil,它表示错误。同样,绑定关键字符号也没有任何意义(请参阅永不更改的变量)。
@lisp
(cl-type type)
@end lisp
如果 expval 是 type 类型,则匹配,这是 cl-typep 接受的类型描述符(请参阅 Common Lisp Extensions 中的类型谓词)。例子:
@lisp
(cl-type integer)
(cl-type (integer 0 10))
@end lisp
@lisp
(pred function)
@end lisp
如果谓词函数在 expval 上调用时返回非零,则匹配。可以使用语法 (pred (not function)) 来否定测试。谓词函数可以具有以下形式之一:
@lisp
function name (a symbol)
@end lisp
使用一个参数 expval 调用命名函数。
示例:整数 p
拉姆达表达式
@lisp
@end lisp
使用一个参数 expval 调用匿名函数(请参阅 Lambda 表达式)。
示例: (lambda (n) (= 42 n))
@lisp
function call with n args
@end lisp
使用 n 个参数(其他元素)和一个附加的第 n+1 个参数(即 expval)调用函数(函数调用的第一个元素)。
示例:(= 42)
本例中,函数为=,n为1,实际函数调用变为:(= 42 expval)。
@lisp
(app function pattern)
@end lisp
如果在 expval 上调用的函数返回与模式匹配的值,则匹配。函数可以采用上面为 pred 描述的形式之一。然而,与 pred 不同的是,app 根据模式而不是布尔真值测试结果。
@lisp
(guard boolean-expression)
@end lisp
如果 boolean-expression 计算结果为非 nil,则匹配。
@lisp
(let pattern expr)
@end lisp
评估 expr 以获取 exprval,如果 exprval 匹配模式则匹配。(之所以称为 let,是因为模式可以使用符号将符号绑定到值。)
排序模式(也称为 seqpat)是一种按顺序处理其子模式参数的模式。pcase 有两个:and 和 or。它们的行为方式与共享其名称的特殊形式类似(请参阅组合条件的构造),但它们不是处理值,而是处理子模式。
@lisp
(and pattern1…)
@end lisp
尝试按顺序匹配 pattern1@dots{},直到其中一个无法匹配。在这种情况下,同样无法匹配,其余的子模式不会被测试。如果所有子模式都匹配,则匹配。
@lisp
(or pattern1 pattern2…)
@end lisp
尝试按顺序匹配 pattern1、pattern2、@dots{},直到其中一个成功。在那种情况下,或者同样匹配,其余的子模式不会被测试。
为了向 body-forms 呈现一致的环境(参见评估简介)(从而避免匹配时的评估错误),模式绑定的变量集是每个子模式绑定的变量的并集。如果一个变量没有被匹配的子模式绑定,那么它被绑定为 nil。
@lisp
(rx rx-expr…)
@end lisp
将字符串与正则表达式 rx-expr@dots{} 匹配,使用 rx 正则表达式表示法(请参阅 rx 结构化正则表达式表示法),就像通过字符串匹配一样。
除了通常的 rx 语法,rx-expr… 可以包含以下结构:
@lisp
(let ref rx-expr…)
@end lisp
将符号 ref 绑定到匹配 rx-expr@dots{}. 的子匹配。 ref 以 body-forms 绑定到子匹配或 nil 的字符串,但也可以在 backref 中使用。
@lisp
(backref ref)
@end lisp
与标准的 backref 结构类似,但这里的 ref 也可以是前一个 (let ref @dots{}) 结构引入的名称。
示例: 优于 cl-case
这是一个示例,它突出了 pcase 相对于 cl-case 的一些优势(请参阅 Common Lisp Extensions 中的条件)。
@lisp
(pcase (get-return-code x)
;; string
((and (pred stringp) msg)
(message "%s" msg))
;; symbol
('success (message "Done!"))
('would-block (message "Sorry, can't do it now"))
('read-only (message "The shmliblick is read-only"))
('access-denied (message "You do not have the needed rights"))
;; default
(code (message "Unknown return code %S" code)))
@end lisp
使用 cl-case,您需要显式声明一个局部变量 code 来保存 get-return-code 的返回值。cl-case 也很难与字符串一起使用,因为它使用 eql 进行比较。
示例:使用和
一个常见的习惯用法是编写一个以 and 开头的模式,其中一个或多个符号子模式提供与随后的子模式(以及主体形式)的绑定。例如,以下模式匹配一位整数。
@lisp
(and
(pred integerp)
n ; bind n to expval
(guard (<= -9 n 9)))
@end lisp
首先,如果 (integerp expval) 的计算结果为非零,则 pred 匹配。接下来,n 是一个匹配任何东西并将 n 绑定到 expval 的符号模式。最后,如果布尔表达式 (<= -9 n 9)(注意对 n 的引用)的计算结果为非零,则防护匹配。如果所有这些子模式都匹配,则匹配。
示例:使用 pcase 重新表述
这是另一个示例,展示了如何将简单的匹配任务从其传统实现(函数 grok/traditional)重新表述为使用 pcase(函数 grok/pcase)的匹配任务。这两个函数的文档字符串是: @code{如果 OBJ 是 ~key:NUMBER} 形式的字符串,则返回 NUMBER(字符串)。否则,返回列表( @code{149} 默认)。~ 一、传统实现(见正则表达式):
@lisp
(defun grok/traditional (obj)
(if (and (stringp obj)
(string-match "^key:\\([[:digit:]]+\\)$" obj))
(match-string 1 obj)
(list "149" 'default)))
(grok/traditional "key:0") ⇒ "0"
(grok/traditional "key:149") ⇒ "149"
(grok/traditional 'monolith) ⇒ ("149" default)
@end lisp
重新表述演示了符号绑定以及 or、and、pred、app 和 let。
@lisp
(defun grok/pcase (obj)
(pcase obj
((or ; line 1
(and ; line 2
(pred stringp) ; line 3
(pred (string-match ; line 4
"^key:\\([[:digit:]]+\\)$")) ; line 5
(app (match-string 1) ; line 6
val)) ; line 7
(let val (list "149" 'default))) ; line 8
val))) ; line 9
(grok/pcase "key:0") ⇒ "0"
(grok/pcase "key:149") ⇒ "149"
(grok/pcase 'monolith) ⇒ ("149" default)
@end lisp
grok/pcase 的大部分是 pcase 形式的单个子句,第 1-8 行的模式,第 9 行的(单个)主体形式。模式是 or,它尝试依次匹配其参数子模式,首先是 and(第 2-7 行),然后是 let(第 8 行),直到其中一个成功。
与前面的示例一样(参见示例 1),并以 pred 子模式开始,以确保以下子模式与正确类型的对象(在本例中为字符串)一起工作。如果 (stringp expval) 返回 nil,则 pred 失败,因此也失败了。
下一个 pred(第 4-5 行)计算 (string-match RX expval) 并在结果为非 nil 时进行匹配,这意味着 expval 具有所需的形式:key:NUMBER。再一次,失败了,pred 失败了,and 也失败了。
最后(在这一系列和子模式中),app 评估 (match-string 1 expval)(第 6 行)以获取临时值 tmp(即 @code{NUMBER} 子字符串)并尝试将 tmp 与模式 val(行7)。由于这是一个符号模式,它无条件匹配并且另外将 val 绑定到 tmp。
现在该应用程序已匹配,所有和子模式都已匹配,所以和匹配。同样,一旦和已经匹配,或者匹配并且不继续尝试子模式 let(第 8 行)。
让我们考虑一下 obj 不是字符串,或者它是字符串但格式错误的情况。在这种情况下,pred 之一(第 3-5 行)无法匹配,因此(第 2 行)无法匹配,因此或(第 1 行)继续尝试子模式 let(第 8 行)。
首先,让计算 (list "149" 'default) 得到 ("149" default) exprval,然后尝试将 exprval 与模式 val 匹配。由于这是一个符号模式,它无条件匹配并且另外将 val 绑定到 exprval。现在 let 已经匹配,或者匹配。
注意 and 和 let 子模式是如何以相同的方式完成的:通过在进程绑定 val 中尝试(总是成功)匹配符号模式 val。因此,or 总是匹配并且控制总是传递给 body 表单(第 9 行)。因为这是成功匹配的 pcase 子句中的最后一个主体形式,所以它是 pcase 的值,也是 grok/pcase 的返回值(参见什么是函数?)。
排序模式中符号的注意事项
前面的示例都使用了以某种方式包含符号子模式的排序模式。以下是有关该用法的一些重要细节。
当 symbol 在 seqpat 中多次出现时,第二次和后续的出现不会扩展为重新绑定,而是使用 eq 扩展为相等测试。
以下示例具有一个 pcase 形式,其中包含两个子句和两个 seqpat,A 和 B。A 和 B 都首先检查 expval 是否是一对(使用 pred),然后将符号绑定到 expval 的 car 和 cdr(每个使用一个 app )。
对于 A,因为符号 st 被提及两次,第二次提及成为使用 eq 的相等性测试。另一方面,B 使用两个单独的符号 s1 和 s2,它们都成为独立的绑定。
@lisp
(defun grok (object)
(pcase object
((and (pred consp) ; seqpat A
(app car st) ; first mention: st
(app cdr st)) ; second mention: st
(list 'eq st))
((and (pred consp) ; seqpat B
(app car s1) ; first mention: s1
(app cdr s2)) ; first mention: s2
(list 'not-eq s1 s2))))
(let ((s "yow!"))
(grok (cons s s))) ⇒ (eq "yow!")
(grok (cons "yo!" "yo!")) ⇒ (not-eq "yo!" "yo!")
(grok '(4 2)) ⇒ (not-eq 4 (2))
@end lisp
副作用代码引用符号未定义。避免。例如,这里有两个类似的函数。都使用和,符号和守卫:
@lisp
(defun square-double-digit-p/CLEAN (integer)
(pcase (* integer integer)
((and n (guard (< 9 n 100))) (list 'yes n))
(sorry (list 'no sorry))))
(square-double-digit-p/CLEAN 9) ⇒ (yes 81)
(square-double-digit-p/CLEAN 3) ⇒ (no 9)
(defun square-double-digit-p/MAYBE (integer)
(pcase (* integer integer)
((and n (guard (< 9 (incf n) 100))) (list 'yes n))
(sorry (list 'no sorry))))
(square-double-digit-p/MAYBE 9) ⇒ (yes 81)
(square-double-digit-p/MAYBE 3) ⇒ (yes 9) ; WRONG!
@end lisp
区别在于保护中的布尔表达式:CLEAN 简单直接地引用 n,而 MAYBE 在表达式 (incf n) 中引用具有副作用的 n。当整数为 3 时,会发生以下情况:
第一个 n 将其绑定到 expval,即计算 (* 3 3) 或 9 的结果。
评估布尔表达式:
@lisp
start: (< 9 (incf n) 100)
becomes: (< 9 (setq n (1+ n)) 100)
becomes: (< 9 (setq n (1+ 9)) 100)
becomes: (< 9 (setq n 10) 100)
; side-effect here!
becomes: (< 9 n 100) ; n now bound to 10
becomes: (< 9 10 100)
becomes: t
@end lisp
因为评估的结果是非零,所以保护匹配和匹配,并且控制传递到该子句的主体形式。
除了断言 9 是一个两位数的整数在数学上的错误之外,MAYBE 还有另一个问题。主体形式再次引用 n,但我们根本看不到更新后的值 10。这是怎么回事?
总而言之,最好完全避免对符号模式的副作用引用,不仅在 boolean-expression(在 guard 中),而且在 expr(在 let)和 function(在 pred 和 app)中。
在匹配时,子句的主体形式可以引用模式 let-binds 的符号集。当 seqpat 是 and 时,这个集合是所有符号的并集,每个符号的子模式 let-binds。这是有道理的,因为为了匹配,所有子模式都必须匹配。
当 seqpat 为 or 时,情况有所不同: or 匹配第一个匹配的子模式; 其余的子模式被忽略。每个子模式让绑定不同的符号集是没有意义的,因为主体形式无法区分哪个子模式匹配并在不同的集合中进行选择。例如,以下内容无效:
@lisp
(require 'cl-lib)
(pcase (read-number "Enter an integer: ")
((or (and (pred cl-evenp)
e-num) ; bind e-num to expval
o-num) ; bind o-num to expval
(list e-num o-num)))
Enter an integer: 42
error→ Symbol’s value as variable is void: o-num
Enter an integer: 149
error→ Symbol’s value as variable is void: e-num
@end lisp
评估正文形式(list e-num o-num)表示错误。为了区分子模式,您可以使用另一个符号,在所有子模式中名称相同但值不同。重写上面的例子:
@lisp
(require 'cl-lib)
(pcase (read-number "Enter an integer: ")
((and num ; line 1
(or (and (pred cl-evenp) ; line 2
(let spin 'even)) ; line 3
(let spin 'odd))) ; line 4
(list spin num))) ; line 5
Enter an integer: 42
⇒ (even 42)
Enter an integer: 149
⇒ (odd 149)
@end lisp
第 1 行用 and 和符号 @code{分解} 了 expval 绑定(在本例中为 num)。在第 2 行,或以与之前相同的方式开始,但不是绑定不同的符号,而是使用 let 两次(第 3-4 行)在两个子模式中绑定相同的符号自旋。spin 的值区分子模式。正文形式引用了这两个符号(第 5 行)。
@node 1142 扩展 pcase
@subsection 11.4.2 扩展 pcase
pcase 宏支持多种模式(请参阅模式匹配条件)。您可以使用 pcase-defmacro 宏添加对其他类型模式的支持。
宏:pcase-defmacro name args [doc] &rest body ¶
为 pcase 定义一种新的模式,以 (name actual-args) 调用。pcase 宏将此扩展为一个评估 body 的函数调用,它的工作是将调用的模式重写为其他模式,在 args 绑定到实际参数的环境中。
此外,安排与 pcase 的文档字符串一起显示文档。按照惯例,doc 应该使用 EXPVAL 来代表计算表达式的结果(第一个 arg 到 pcase)。
通常,body 会重写调用的模式以使用更基本的模式。尽管所有的模式最终都归结为核心模式,但身体不需要立即使用核心模式。下面的示例定义了两个模式,分别命名为小于和整数小于。
@lisp
(pcase-defmacro less-than (n)
"Matches if EXPVAL is a number less than N."
`(pred (> ,n)))
(pcase-defmacro integer-less-than (n)
"Matches if EXPVAL is an integer less than N."
`(and (pred integerp)
(less-than ,n)))
@end lisp
请注意,文档字符串以通常的方式提到了 args(在这种情况下,只有一个:n),并且按照惯例也提到了 EXPVAL。第一次重写(即,小于的主体)使用一个核心模式:pred。第二种使用两个核心模式:and 和 pred,以及新定义的模式 less-than。两者都使用单个反引号结构(请参阅反引号)。
@node 1143 反引号样式模式
@subsection 11.4.3 反引号样式模式
本小节描述了反引号样式模式,这是一组简化结构匹配的内置模式。有关背景,请参阅模式匹配条件。
反引号样式模式是一组功能强大的 pcase 模式扩展(使用 pcase-defmacro 创建),可以轻松地将 expval 与其结构规范进行匹配。
例如,要匹配 expval 必须是两个元素的列表,其中第一个元素是特定字符串,第二个元素是任何值,您可以编写一个核心模式:
@lisp
(and (pred listp)
ls
(guard (= 2 (length ls)))
(guard (string= "first" (car ls)))
(let second-elem (cadr ls)))
@end lisp
或者您可以编写等效的反引号样式模式:
@lisp
`("first" ,second-elem)
@end lisp
反引号样式的模式更简洁,类似于 expval 的结构,并且避免了绑定 ls。
反引号样式的模式具有 `qpat 形式,其中 qpat 可以具有以下形式:
@lisp
[qpat1 qpat2 … qpatm]
@end lisp
如果 expval 是其 car 与 qpat1 匹配且 cdr 与 qpat2 匹配的 cons 单元格,则匹配。这很容易推广到 (qpat1 qpat2 @dots{}) 中的列表。
@lisp
symbol
@end lisp
如果 expval 是长度为 m 的向量,其第 0..(m-1) 个元素分别匹配 qpat1、qpat2 @dots{} qpatm,则匹配。
@lisp
keyword
@end lisp
@lisp
number
@end lisp
@lisp
string
@end lisp
如果 expval 的对应元素等于指定的文字对象,则匹配。
@lisp
,pattern
@end lisp
如果 expval 的相应元素与模式匹配,则匹配。请注意,模式是 pcase 支持的任何类型。(在上面的例子中,second-elem 是一个符号核心模式;因此它匹配任何东西,并且 let-binds second-elem。)
对应的元素是 expval 中与反引号样式模式中 qpat 的结构位置相同的部分。(在上面的例子中,second-elem 的对应元素是 expval 的第二个元素。)
这是一个使用 pcase 为小表达式语言实现简单解释器的示例(请注意,这需要对 fn 子句中的 lambda 表达式进行词法绑定以正确捕获 body 和 arg(请参阅词法绑定):
@lisp
(defun evaluate (form env)
(pcase form
(`(add ,x ,y) (+ (evaluate x env)
(evaluate y env)))
(`(call ,fun ,arg) (funcall (evaluate fun env)
(evaluate arg env)))
(`(fn ,arg ,body) (lambda (val)
(evaluate body (cons (cons arg val)
env))))
((pred numberp) form)
((pred symbolp) (cdr (assq form env)))
(_ (error "Syntax error: %S" form))))
@end lisp
前三个子句使用反引号样式的模式。`(add ,x ,y) 是一种模式,它检查表单是否是以文字符号 add 开头的三元素列表,然后提取第二个和第三个元素并将它们分别绑定到符号 x 和 y。子句主体评估 x 和 y 并添加结果。同样,call 子句实现函数调用,fn 子句实现匿名函数定义。
其余子句使用核心模式。(pred numberp) 如果 form 是数字,则匹配。在比赛中,身体评估它。(pred symbolp) 如果 form 是一个符号,则匹配。匹配时,主体在 env 中查找符号并返回其关联。最后, _ 是匹配任何东西的包罗万象的模式,因此它适用于报告语法错误。
以下是一些用这种小语言编写的示例程序,包括它们的评估结果:
@lisp
(evaluate '(add 1 2) nil) ⇒ 3
(evaluate '(add x y) '((x . 1) (y . 2))) ⇒ 3
(evaluate '(call (fn x (add 1 x)) 2) nil) ⇒ 3
(evaluate '(sub 1 2) nil) ⇒ error
@end lisp
@node 1144 解构 pcase模式
@subsection 11.4.4 解构 pcase模式
Pcase 模式不仅表达了它们可以匹配的对象形式的条件,而且它们还可以提取这些对象的子字段。例如,我们可以使用以下代码从变量 my-list 的值的列表中提取 2 个元素:
@lisp
(pcase my-list
(`(add ,x ,y) (message "Contains %S and %S" x y)))
@end lisp
这不仅会提取 x 和 y,还会额外测试 my-list 是否是一个恰好包含 3 个元素且其第一个元素是符号 add 的列表。如果这些测试中的任何一个失败,pcase 将立即返回 nil 而不会调用 message。
提取存储在对象中的多个值称为解构。使用 pcase 模式允许执行解构绑定,这类似于局部绑定(请参阅局部变量),但通过从兼容结构的对象中提取这些值来为变量的多个元素提供值。
本节中描述的宏使用 pcase 模式来执行解构绑定。对象具有兼容结构的条件意味着对象必须匹配模式,因为只有这样才能提取对象的子字段。例如:
@lisp
(pcase-let ((`(add ,x ,y) my-list))
(message "Contains %S and %S" x y))
@end lisp
与前面的示例相同,只是它直接尝试从 my-list 中提取 x 和 y,而无需首先验证 my-list 是否是具有正确数量的元素并且将 add 作为其第一个元素的列表。当对象实际上与模式不匹配时的精确行为是未定义的,尽管主体不会被静默地跳过:要么发出错误信号,要么运行主体,其中一些变量可能绑定到任意值,如 nil。
对解构绑定有用的 pcase 模式通常是反引号样式模式中描述的那些模式,因为它们表达了将匹配的对象结构的规范。
有关解构绑定的替代工具,请参阅 seq-let。
@lisp
Macro: pcase-let bindings body… ¶
@end lisp
根据绑定对变量进行解构绑定,然后对body求值。
bindings 是 (pattern exp) 形式的绑定列表,其中 exp 是要计算的表达式,而 pattern 是 pcase 模式。
首先评估所有 exp,然后将它们与各自的模式匹配,引入新的变量绑定,然后可以在 body 中使用。变量绑定是通过解构 pattern 元素与计算的 exp 的相应元素的值的绑定来产生的。
@lisp
Macro: pcase-let* bindings body… ¶
@end lisp
根据绑定对变量进行解构绑定,然后对body求值。
bindings 是 (pattern exp) 形式的绑定列表,其中 exp 是要计算的表达式,而 pattern 是 pcase 模式。变量绑定是通过解构 pattern 元素与计算的 exp 的相应元素的值的绑定来产生的。
与 pcase-let 不同,但与 let* 类似,每个 exp 在处理绑定的下一个元素之前与其对应的模式匹配,因此在每个绑定中引入的变量绑定在它后面的绑定的 exp 中可用,除了身体可用。
@lisp
Macro: pcase-dolist (pattern list) body… ¶
@end lisp
对 list 的每个元素执行一次 body ,在每次迭代时,将 pattern 中的变量解构绑定到 list 元素的相应子字段的值。绑定就像通过 pcase-let 一样执行。当 pattern 是一个简单变量时,这最终等同于 dolist(请参阅迭代)。
@lisp
Macro: pcase-setq pattern value… ¶
@end lisp
以 setq 形式为变量赋值,根据其各自的模式解构每个值。
@node 115 迭代
@section 11.5 迭代
迭代意味着重复执行程序的一部分。例如,您可能希望对列表的每个元素重复一次计算,或者对从 0 到 n 的每个整数重复一次。您可以在 Emacs Lisp 中使用特殊形式执行此操作,同时:
@lisp
Special Form: while condition forms… ¶
@end lisp
而首先评估条件。如果结果非零,它会按文本顺序评估表单。然后它重新评估条件,如果结果是非零,它再次评估表格。重复此过程,直到条件评估为零。
可能发生的迭代次数没有限制。循环将继续,直到任一条件评估为 nil 或直到错误或 throw 跳出它(请参阅非本地退出)。
@lisp
(setq num 0)
⇒ 0
(while (< num 4)
(princ (format "Iteration %d." num))
(setq num (1+ num)))
-| Iteration 0.
-| Iteration 1.
-| Iteration 2.
-| Iteration 3.
⇒ nil
@end lisp
要编写一个 repeat-until 循环,它将在每次迭代中执行某些操作,然后进行结束测试,请将主体后跟结束测试作为 while 的第一个参数放在 progn 中,如下所示:
@lisp
(while (progn
(forward-line 1)
(not (looking-at "^$"))))
@end lisp
这将向前移动一行并继续逐行移动,直到到达空行。奇怪的是,while 没有主体,只是结束测试(它也完成了移动点的实际工作)。
dolist 和 dotimes 宏提供了方便的方法来编写两种常见的循环。
@lisp
Macro: dolist (var list [result]) body… ¶
@end lisp
此构造对列表的每个元素执行一次 body,在本地绑定变量 var 以保存当前元素。然后它返回评估结果的值,如果省略结果,则返回 nil。例如,以下是如何使用 dolist 定义反向函数:
@lisp
(defun reverse (list)
(let (value)
(dolist (elt list value)
(setq value (cons elt value)))))
@end lisp
@lisp
Macro: dotimes (var count [result]) body… ¶
@end lisp
此构造对从 0(包括)到 count(不包括)的每个整数执行一次 body,将变量 var 绑定到当前迭代的整数。然后它返回评估结果的值,如果省略结果,则返回 nil。不推荐使用结果。下面是一个使用 dotimes 做某事 100 次的例子:
@lisp
(dotimes (i 100)
(insert "I will not obey absurd orders\n"))
@end lisp
@node 116 生成器
@section 11.6 生成器
生成器是一个产生潜在无限值流的函数。每次函数产生一个值时,它都会挂起自己并等待调用者请求下一个值。
@lisp
Macro: iter-defun name args [doc] [declare] [interactive] body… ¶
@end lisp
iter-defun 定义了一个生成器函数。生成器函数与普通函数具有相同的签名,但工作方式不同。生成器函数在调用时不会执行主体,而是返回一个迭代器对象。该迭代器运行 body 以生成值,发出一个值并在出现 iter-yield 或 iter-yield-from 的地方暂停。当 body 正常返回时,iter-next 以 body 的结果作为其条件数据发出 iter-end-of-sequence 信号。
任何类型的 Lisp 代码在正文中都是有效的,但 iter-yield 和 iter-yield-from 不能出现在 unwind-protect 表单中。
@lisp
Macro: iter-lambda args [doc] [interactive] body… ¶
@end lisp
iter-lambda 生成一个未命名的生成器函数,其工作方式与使用 iter-defun 生成的生成器函数一样。
@lisp
Macro: iter-yield value ¶
@end lisp
当它出现在生成器函数中时,iter-yield 表示当前迭代器应该暂停并从 iter-next 返回值。iter-yield 计算为下一次调用 iter-next 的 value 参数。
@lisp
Macro: iter-yield-from iterator ¶
@end lisp
iter-yield-from 产生迭代器生成的所有值,并计算为迭代器的生成器函数正常返回的值。虽然它有控制权,但迭代器使用 iter-next 接收发送给迭代器的值。
要使用生成器函数,首先正常调用它,生成一个迭代器对象。迭代器是生成器的特定实例。然后使用 iter-next 从这个迭代器中检索值。当没有更多值可以从迭代器中提取时,iter-next 会使用迭代器的最终值引发 iter-end-of-sequence 条件。
重要的是要注意生成器函数体仅在对 iter-next 的调用内部执行。对使用 iter-defun 定义的函数的调用会产生一个迭代器; 您必须使用 iter-next 驱动此迭代器,以使任何有趣的事情发生。对生成器函数的每次调用都会产生一个不同的迭代器,每个迭代器都有自己的状态。
@lisp
Function: iter-next iterator value ¶
@end lisp