-
Notifications
You must be signed in to change notification settings - Fork 9
/
chapter-12.tex
658 lines (544 loc) · 56.7 KB
/
chapter-12.tex
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
\chapter{Они назвали его Lisp неспроста: обработка списков}
\label{ch:12}
Списки в языке Lisp играют важную роль как по историческим, так и по практическим
причинам. Изначально списки были основным составным типом данных в языке Lisp, и в течении
десятилетий они были единственным составным типом данных. В наши дни, программист на
Common Lisp может использовать как типы vector, hash table, самостоятельно определённые
типы и структуры, так и списки.
Списки остаются в языке потому, что они являются прекрасным решением для определённого
рода проблем. Одна из них~--- проблема представления кода, как данных для трансформации и
генерации кода в макросах~--- является специфичной для языка Lisp, и это объясняет то, как
другие языки обходятся без списков в стиле Lisp. Вообще говоря, списки~--- прекрасная
структура данных для представления любых неоднородных и/или иерархических данных. Кроме
того они достаточно легковесны и поддерживают функциональный стиль программирования~--- ещё
одна важная часть наследия Lisp.
Итак, вы должны понять списки в их собственных терминах; после того как вы достигнете
хорошего понимания как списки работают, вы сможете лучше оценивать стоит ли их
использовать или нет.
\section{Списков нет}
Мальчик с ложкой: Не пытайся согнуть список. Это невозможно. Вместо этого... попытайся
понять истину.
Нео: Какую истину?
Мальчик с ложкой: Списков нет.
Нео: Списков нет?
Мальчик с ложкой: И тогда ты поймёшь: это не списки, которые сгибаются; это только ты
сам. \footnote{Перифраз из к/ф <<Матрица>>
\href{http://us.imdb.com/Quotes?0133093}{Оригинальная цитата}}
Ключом к пониманию списков, является осознание того, что они, по большей части, иллюзия,
построенная на основе объектов более примитивных типов данных. Эти простые объекты~--- пары
значений, называемые cons-ячейкой, от имени функции \code{CONS}, используемой для их
создания.
\code{CONS} принимает 2 аргумента и возвращает новую cons-ячейку, содержащую 2 значения
\footnote{\code{CONS} сокращения для глагола construct~--- конструировать}. Эти значения
могут быть ссылками на объект любого типа. Если второе значение не \code{NIL} и не другая
cons-ячейка, то ячейка печатается как два значения в скобках, разделённые точкой (так
называемая точечная пара).
\begin{lstlisting}
(cons 1 2) ==> (1 . 2)
\end{lstlisting}
Два значения в cons-ячейке называются \code{CAR} и \code{CDR}~--- по имени функций,
используемых для доступа к ним. На заре времён эти имена были мнемониками, по крайней мере
для людей, работающих над первой реализацией Lisp на IBM 704. Но даже тогда они были всего
лишь позаимствованы из мнемоник ассемблера, используемых для реализации этих операций. Тем
не менее, это не так уж плохо, что названия этих функций несколько бессмысленны,
применительно к конкретной cons-ячейке лучше думать о них просто как о произвольной паре
значений без какого-либо особого смысла. Итак:
\begin{lstlisting}
(car (cons 1 2)) ==> 1
(cdr (cons 1 2)) ==> 2
\end{lstlisting}
Оба \code{CAR} и \code{CDR} могут меняться функцией \code{SETF}: если дана cons-ячейка, то
возможно присвоить значение обеим её составляющим. \footnote{Когда место переданное в
\code{SETF} это \code{CAR} или \code{CDR}, вызов \code{SETF} разворачивается в вызов
функции \code{RPLACA} или \code{RPLACD}; некоторые лисперы старой школы, те самые, что
до сих пор используют \code{SETQ}, будут использовать \code{RPLACA} или \code{RPLACD}
непосредственно, но современный стиль учит использовать \code{SETF} для \code{CAR} и
\code{CDR}}
\begin{lstlisting}
(defparameter *cons* (cons 1 2))
*cons* ==> (1 . 2)
(setf (car *cons*) 10) ==> 10
*cons* ==> (10 . 2)
(setf (cdr *cons*) 20) ==> 20
*cons* ==> (10 . 20)
\end{lstlisting}
Так как значения в cons-ячейке могут быть ссылками на любой объект, вы можете строить большие
структуры, связывая cons-ячейки между собой. Списки строятся связыванием cons-ячеек в
цепочку. Элементы списка содержатся в \code{CAR} cons-ячеек, а связи со следующими
cons-ячейками содержатся в их \code{CDR}. Последняя ячейка в цепочке имеет \code{CDR} со
значением \code{NIL}, которое, как я говорил в главе~\ref{ch:04}, представляет собой как
пустой список, так и булево значение ложь.
Такая компоновка отнюдь не уникальна для Lisp, она называется однонаправленный
список. Несколько языков, не входящих в семейство Lisp, предоставляют расширенную
поддержку для этого скромного типа данных.
Таким образом, когда я говорю, что какое-то значение является списком, я имею в виду, что
оно либо \code{NIL}, либо ссылка на cons-ячейку. \code{CAR} cons-ячейки является первым
элементом списка, а \code{CDR} является ссылкой на другой список (который в свою очередь
также является cons-ячейкой), содержащий остальные элементы, или значение
\code{NIL}. Lisp-печаталка понимает это соглашение и печатает такие цепочки cons-ячеек как
списки в скобках, нежели как точечные пары.
\begin{lstlisting}
(cons 1 nil) ==> (1)
(cons 1 (cons 2 nil)) ==> (1 2)
(cons 1 (cons 2 (cons 3 nil))) ==> (1 2 3)
\end{lstlisting}
Когда мы говорим о структурах из cons-ячеек, нам могут помочь несколько диаграмм. Эти
диаграммы изображают cons-ячейки как пары блоков:
%TODO: add picture
%{{one-cons-cell.png}}
Блок слева представляет \code{CAR}, а блок справа~--- \code{CDR}. Значения, хранимые в
ячейках также представлены в виде отдельных блоков или в виде стрелки выходящей из блока,
для представления ссылки на значение. \footnote{Как правило, простые объекты, такие как
числа, изображаются внутри соответствующего блока, а более сложные объекты~--- снаружи, со
стрелкой выходящей из блока, означающей ссылку. Это хорошо согласуется с тем, как
работают многие реализации Common Lisp: хотя все объекты концептуально хранятся как
ссылки, некоторые простые неизменяемые объекты могут храниться в cons-ячейках
непосредственно} Например, список \code{(1 2 3)}, который состоит из трёх cons-ячеек,
связанных вместе с помощью их \code{CDR}, может быть изображён так:
%TODO: add picture
%{{list-1-2-3.png}}
Однако, в основном при работе со списками вы не обязаны иметь дело с отдельными ячейками,
так как существуют функции, которые создают списки и используются для манипулирования
ими. Например, функция \code{LIST} строит cons-ячейки и связывает их вместе, следующие
\code{LISP}-выражения эквивалентны \code{CONS}-выражениям, приведённым выше:
\begin{lstlisting}
(list 1) ==> (1)
(list 1 2) ==> (1 2)
(list 1 2 3) ==> (1 2 3)
\end{lstlisting}
Аналогично, когда вы думаете в терминах списков, вы не должны использовать бессмысленные
имена \code{CAR} и \code{CDR}; вы должны использовать \code{FIRST} и \code{REST}~---
синонимы для \code{CAR} и \code{CDR} при рассмотрении cons-ячеек, как списков.
\begin{lstlisting}
(defparameter *list* (list 1 2 3 4))
(first *list*) ==> 1
(rest *list*) ==> (2 3 4)
(first (rest *list*)) ==> 2
\end{lstlisting}
Как и cons-ячейки, списки могут содержать значения любых типов.
\begin{lstlisting}
(list "foo" (list 1 2) 10) ==> ("foo" (1 2) 10)
\end{lstlisting}
Структура списка при этом выглядит так:
%TODO: add picture
%{{mixed-list.png}}
Так как элементами списка могут быть другие списки, то с помощью списков можно представлять
деревья произвольной глубины и сложности. Таким образом, они идеально подходят для
представления гетерогенных иерархических данных. Например, XML-процессоры, основанные на
Lisp, как правило представляют XML-документы в виде списков. Другим очевидным примером
данных с древовидной структурой является сам код на языке Lisp. В главах~\ref{ch:30} и
\ref{ch:31} вы
напишете библиотеку для генерации HTML-кода, которая использует списки списков для
представления кода, который необходимо сгенерировать. В следующей главе я расскажу больше
об использовании cons-ячеек для представления других типов данных.
Common Lisp предоставляет обширную библиотеку функций для работы со списками. В
разделах~\ref{sec:12-list-funcs} и~\ref{sec:12-map} вы узнаете о наиболее важных из
них. Данные функции будет проще понять в контексте идей, взятых из парадигмы
функционального программирования.
\section{Функциональное программирование и списки}
Суть функционального программирования в том, что программы состоят исключительно из
функций без побочных эффектов, которые вычисляют свои значения исключительно на основе
своих аргументов. Преимущество функционального стиля программирования в том, что он
облегчает понимание программы. Устранение побочных эффектов ведёт к устранению практически
всех возможностей удалённого воздействия. Так как результат функции определяется её
аргументами, поведение функции легче понять и протестировать. Например, когда вы видите
выражение \code{(+ 3 4)}, вы знаете, что результат целиком задаётся определением функции
\code{+} и переданными аргументами 3 и 4. Вам не надо беспокоиться о том, что могло
произойти в программе до этого, так как нет ничего, что могло бы изменить результат
вычисления выражения.
Функции, работающие с числами, естественным образом являются функциональными, так как числа
неизменяемы. Списки же, как вы видели раннее, могут меняться, при применении функции
\code{SETF} над ячейками списка. Тем не менее, списки могут рассматриваться как
функциональные типы данных, если считать, что их значение определяется элементами, которые
они содержат. Так, любой список вида \code{(1 2 3 4)} функционально эквивалентен любому
другому списку, содержащему эти четыре значения, независимо от того, какие cons-ячейки
используются для представления списка. И любая функция, которая принимает списки в
качестве аргументов и возвращает значение, основываясь исключительно на содержании списка,
могут считаться функциональными. Например, если функции \code{REVERSE} передать список
\code{(1 2 3 4)}, она всегда вернёт \code{(4 3 2 1)}. Различные вызовы \code{REVERSE} с
функционально-эквивалентными аргументами вернут функционально-эквивалентные
результаты. Другой аспект функционального программирования, который я рассматриваю в
разделе~\ref{sec:12-map}, это использование функций высших порядков: функций, которые
используют другие функции, как данные, принимая их в качестве аргументов или возвращая в
качестве результата.
Большинство функций Common Lisp для работы со списками написаны в функциональном
стиле. Позднее мы обсудим как совмещать функциональный подход к программированию с другими
подходами, но сначала вы должны понять некоторые тонкости функционального подхода
применительно к спискам.
Так как большинство функций для работы со списками написаны в функциональном стиле, они могут
возвращать результаты, которые имеют общие cons-ячейки с их аргументами. Возьмём
конкретный пример: функция \code{APPEND} принимает любое число списков в качестве
аргументов и возвращает новый список, содержащий все элементы списков-аргументов,
например:
\begin{lstlisting}
(append (list 1 2) (list 3 4)) ==> (1 2 3 4)
\end{lstlisting}
С точки зрения функционального подхода, задача функции \code{APPEND}~--- вернуть список
\code{(1 2 3 4)} не изменяя ни одну из cons-ячеек в списках-аргументах \code{(1 2)} и
\code{(3 4)}. Очевидно, что это можно сделать создав новый список, состоящий из четырёх
новых cons-ячеек. Однако в этом есть лишняя работа. Вместо этого \code{APPEND} на самом
деле создаёт только две новые cons-ячейки, чтобы хранить значения 1 и 2, соединяя их
вместе и делая ссылку из \code{CDR} второй ячейки на первый элемент последнего аргумента~---
списка \code{(3 4)}. После этого функция возвращает cons-ячейку содержащую 1. Ни одна из
входных cons-ячеек не была изменена, и результатом, как и требовалось, является список
\code{(1 2 3 4)}. Единственная хитрость в том, что результат, возвращаемый функцией
\code{APPEND} имеет общие cons-ячейки со списком \code{(3 4)}. Структура результата
выглядит так:
%TODO: add picture
%{{after-append.png}}
В общем случае, функция \code{APPEND} должна копировать все, кроме последнего,
аргументы-списки, и она может вернуть результат, который имеет общие структуры с последним
аргументом.
Другие функции также используют полезную особенность списков иметь общие
структуры. Некоторые, как и \code{APPEND}, всегда возвращают результаты, которые имеют
общие структуры со своими аргументами. Другим функциям позволено возвращать результаты с
общими структурами, но это зависит от их реализации.
\section{<<Разрушающие>> операции}
Если бы Common Lisp был строго функциональным языком, больше не о чем было бы
говорить. Однако, так как после создания cons-ячейки есть возможность изменить её значение
применив функцию \code{SETF} над её \code{CAR} и \code{CDR}, мы должны обсудить как
стыкуются побочные эффекты и общие структуры.
Из-за функционального наследия языка Lisp, операции, которые изменяют существующие объекты
называются разрушающими~--- в функциональном программировании изменение состояния объекта
<<разрушает>> его, так как объект больше не представляет того же значения, что до применения
функции. Однако использование одного и того же термина для обозначения всего класса
операций, изменяющих состояние существующих объектов, ведёт к некоторой путанице,
так как существует 2 сильно отличающихся класса таких операций:
операции-для-побочных-эффектов и утилизирующие операции. \footnote{Термин <<операция для
побочных эффектов>> используется в стандарте языка, название второго класса~--- моё
изобретение; большинство пособий по Lisp используют термин разрушающие операции для
обоих классов, я же пытаюсь развеять неразбериху}
Операции-для-побочных-эффектов это те, которые используются ради их эффектов. В этом
смысле, всякое использование \code{SETF} является разрушающим, как и использование
функций, которые вызывают \code{SETF} чтобы изменить состояние объектов, например
\code{VECTOR-PUSH} или \code{VECTOR-POP}. Но это несколько некорректно объявлять операции
разрушающими, так как они не предназначены для написания программ в функциональном стиле,
т.е. они не могут быть описаны с использованием терминов функциональной парадигмы. Тем не
менее, если вы смешиваете нефункциональные операции-для-побочных-эффектов с функциями
возвращающими результаты с общими структурами, то надо быть внимательным, чтобы случайно
не изменить эти общие структуры. Например, имеем:
\begin{lstlisting}
(defparameter *list-1* (list 1 2))
(defparameter *list-2* (list 3 4))
(defparameter *list-3* (append *list-1* *list-2*))
\end{lstlisting}
После вычисления, у вас три списка, но \code{list-3} и \code{list-2} имеют общую
структуру, как показано на предыдущей диаграмме.
\begin{lstlisting}
*list-1* ==> (1 2)
*list-2* ==> (3 4)
*list-3* ==> (1 2 3 4)
\end{lstlisting}
Посмотрим, что случится если мы изменим \code{list-2}:
\begin{lstlisting}
(setf (first *list-2*) 0) ==> 0
*list-2* ==> (0 4) ; как и ожидалось
*list-3* ==> (1 2 0 4) ; а вот этого возможно вы и не хотели
\end{lstlisting}
Из-за наличия общих структур, изменения в списке \code{list-2} привели к изменению списка
\code{list-3}: первая cons-ячейка в \code{list-2} является также третьей ячейкой в
\code{list-3}. Изменение значения \code{FIRST} списка \code{list-2} изменило значение
\code{CAR} в cons-ячейке, изменив оба списка.
Совсем по-другому обстоит дело с утилизирующими операциями, которые предназначены для
запуска в функциональном коде. Они используют побочные эффекты лишь для оптимизации. В
частности, они повторно используют некоторые cons-ячейки своих аргументов для получения
результатов. Но в отличие от таких функций, как \code{APPEND}, которые используют
cons-ячейки, включая их без изменений в результирующий список, утилизирующие операции
используют cons-ячейки как сырьё, изменяя их \code{CAR} и \code{CDR} для получения
желаемого результата. Таким образом утилизирующие операции спокойно могут быть
использованы, когда их аргументы не пригодятся после их выполнения.
Чтобы понять как работают функции утилизации сравним неразрушающую операцию
\code{REVERSE}, которая возвращает инвертированный аргумент, с функцией \code{NREVERSE},
которая является утилизирующей функцией и делает то же самое. Так как \code{REVERSE} не
меняет своих аргументов, она создаёт новую cons-ячейку для каждого элемента
списка-аргумента. Но предположим, вы напишите:
\begin{lstlisting}
(setf *list* (reverse *list*))
\end{lstlisting}
Присвоив результат вычисления переменной \code{*list*}, вы удалили ссылку на начальное
значение \code{*list*}. Если на cons-ячейки в начальном списке больше никто не ссылается,
то они теперь доступны для сборки мусора. Тем не менее в большинстве реализаций Lisp более
эффективным является повторно использовать эти ячейки, чем создавать новые, а старые
превращать в мусор.
\code{NREVERSE} именно это и делает. \code{N}~--- сокращение для <<не создающая>>, в том
смысле, что она не создаёт новых cons-ячеек. Конкретные побочные эффекты \code{NREVERSE}
явно не описываются, она может проводить любые модификации над \code{CAR} и \code{CDR}
любых cons-ячеек аргумента, но типичная реализация может быть такой: проход по списку с
изменением \code{CDR} каждой cons-ячейки так, чтобы она указывала на предыдущую
cons-ячейку, в конечном счёте результатом будет cons-ячейка, которая была последней в
списке аргументе, а теперь является головой этого списка. Никакие новые cons-ячейки при
этом не создаются и сборка мусора не производится.
Большинство утилизирующих функций, таких как \code{NREVERSE}, имеют своих неразрушающих
двойников, которые вычисляют тот же результат. В общем случае утилизирующие функции имеют
такое же имя, как их недеструктивные двойники с подставленной первой буквой N. Но есть и
исключения, например часто используемые: \code{NCONC}~--- утилизирующая версия
\code{APPEND} и \code{DELETE}, \code{DELETE-IF}, \code{DELETE-IF-NOT},
\code{DELETE-DUPLICATED}~--- версии семейства функций \code{REMOVE}.
В общем случае, вы можете использовать утилизирующие функции таким же образом, как их
недеструктивные аналоги, учитывая, что они безопасны, если их аргументы не будут
использованы после выполнения функций. Побочные эффекты большинства утилизирующих функций
не достаточно строго описаны, чтобы на них можно было полагаться.
Однако ещё большую неразбериху вносит небольшой набор утилизирующих функций со строго
определёнными побочными эффектами, на которые можно положиться. В этот набор входят
\code{NCONC}, утилизирующая версия \code{APPEND}, и \code{NSUBSTITUTE} и её \code{-IF} и
\code{-IF-NOT} варианты~--- утилизирующие версии группы функций \code{SUBSTITUTE}.
Как и \code{APPEND}, \code{NCONC} возвращает соединение своих аргументов, но строит такой
результат следующим образом: для каждого непустого аргумента-списка, \code{NCONC}
устанавливает в \code{CDR} его последней cons-ячейки ссылку на первую cons-ячейку
следующего непустого аргумента-списка. После этого она возвращает первый список, который
теперь является головой результата-соединения. Таким образом:
\begin{lstlisting}
(defparameter *x* (list 1 2 3))
(nconc *x* (list 4 5 6)) ==> (1 2 3 4 5 6)
*x* ==> (1 2 3 4 5 6)
\end{lstlisting}
На функцию \code{NSUBSTITUTE} и её варианты можно положиться в следующем её поведении: она
пробегает по списковой структуре аргумента-списка и устанавливает с помощью функции
\code{SETF} новое значения в \code{CAR} его cons-ячеек. После этого она возвращает
переданный ей аргумент-список, который теперь имеет то же значение, как если бы был
вычислен с помощью \code{SUBSTITUTE}.\footnote{Строковые функции
\code{NSTRING-CAPITALIZE}, \code{NSTRING-DOWNCASE}, и \code{NSTRING-UPCASE} действуют
также: они возвращают такой же результат, как их аналоги без \code{N}, но при этом
изменяют свои аргументы}
Ключевой момент, который необходимо запомнить о \code{NCONC} и \code{NSUBSTITUTE} состоит
в том, что они являются исключением из правила не полагаться на побочные эффекты
утилизирующих функций. Вполне допустимо, и даже желательно, игнорировать возможность
полагаться на побочные эффекты этих функций, и использовать их, как и любые другие
утилизирующие функции, только ради возвращаемых ими значений.
\section{Комбинирование утилизации с общими структурами}
Хотя вы можете спокойно использовать утилизирующие функции, если их аргументы не будут
использованы позднее, стоит заметить, что каждая утилизирующая функция~--- как заряженное
ружье направленное на собственную ногу, в том смысле, что если вы случайно примените
утилизирующую функцию к аргументу, который используете позднее, то наверняка потеряете
пару пальцев.
Всё усложняется тем, что функции использующие общие структуры и утилизирующие функции
работают над схожими задачами. Недеструктивные списочные функции возвращают списки,
которые имеют общие структуры со своими аргументами, основываясь на предположении, что
cons-ячейки этих общих структур никогда не поменяются, а утилизирующие функции работают с
нарушением этого допущения. Другими словами, использование общих структур основано на
предположении, что вам безразлично какие cons-ячейки составляют результат, утилизирующие
же функции требуют, чтобы вы точно знали, откуда берутся cons-ячейки.
На практике, утилизирующие функции используются в нескольких определённых
случаях. Наиболее частый случай~--- построение списка, который будет возвращён из функции
добавлением элементов в его конец (как правило с использованием функции \code{PUSH}), а
потом инвертирование данного списка. При этом список хранится в локальной для данной
функции переменной. \footnote{При анализе кода из Common Lisp Open Code Collection
(CLOCC), выясняется, что среди множества библиотек, написанных разными авторами,
сочетание \code{PUSH} и \code{NREVERSE} составляет около половины всех использований
утилизирующих функций}
Это является эффективным способом построения списка, потому что каждый вызов \code{PUSH}
создаёт только одну cons-ячейку, а вызов \code{NREVERSE} быстро пробегает по списку,
переназначая \code{CDR} ячеек. Так как список создаётся в локальной переменной внутри
функции, то нет никакой возможности, что какой-то код за пределами функции имеет общие
структуры с данным списком. Вот пример функции, которая использует такой приём для
построения списка, который содержит числа от 0 до n \footnote{Конечно же имеются и другие
пути достигнуть такого результата. Например, макрос \code{LOOP} делает это очень просто
и похоже генерирует код, который даже эффективнее связки \code{PUSH}/\code{NREVERSE}}:
\begin{lstlisting}
(defun upto (max)
(let ((result nil))
(dotimes (i max)
(push i result))
(nreverse result)))
(upto 10) ==> (0 1 2 3 4 5 6 7 8 9)
\end{lstlisting}
Другой часто встречающийся случай применения утилизирующих функций \footnote{Использования
этого случая насчитывает около 30\% от всех случаев использования утилизирующий функций
в CLOCC} - немедленное переприсваивание значения, возвращаемого утилизирующей функцией,
переменной, содержащей потенциально утилизированное значение. Например, вы часто будете
видеть такие выражения с использованием функций \code{DELETE} (утилизирующей версии
\code{REMOVE}):
\begin{lstlisting}
(setf foo (delete nil foo))
\end{lstlisting}
Эта конструкция присваивает переменной \code{foo} её старое значение с удалёнными
элементами, равными \code{NIL}. Однако, здесь утилизирующие функции должны применяться
осмотрительно: если \code{foo} имеет общие структуры с другими списками, то использование
\code{DELETE} вместо \code{REMOVE} может разрушить структуры этих списков. Например,
возьмём два списка \code{list-2} и \code{list-3}, рассмотренные раннее, они разделяют свои
последние 2 cons-ячейки.
\begin{lstlisting}
*list-2* ==> (0 4)
*list-3* ==> (1 2 0 4)
\end{lstlisting}
Вы можете удалить \code{4} из \code{list-3} так:
\begin{lstlisting}
(setf *list-3* (delete 4 *list-3*)) ==> (1 2 0)
\end{lstlisting}
Но \code{DELETE} скорее всего произведёт удаление тем, что установит \code{CDR} третьей
ячейки списка в \code{NIL}, отсоединив таким образом четвёртую ячейку от
списка. Так как третья ячейка в \code{list-3} является также первой ячейкой в \code{list-2},
этот код изменит также и \code{list-2}:
\begin{lstlisting}
*list-2* ==> (0)
\end{lstlisting}
Если вы используете \code{REMOVE} вместо \code{DELETE}, то результат будет построен с
помощью создания новых ячеек, не изменяя ячеек \code{list-3}. В этом случае \code{list-2}
не будет изменён.
Эти два случая использования вместе составляют около 80\% от общего числа случаев
использования утилизирующих функций. Другие случаи использования возможны, но требуют
внимательного отслеживания того возвращают ли функции списки с общими структурами или нет.
В общем случае, при работе со списками лучше писать код в функциональном стиле~--- ваши
функции должны полагаться на содержание их аргументов-списков и не должны менять
их. Следование этому правилу, разумеется, исключает использование разрушающих операций,
утилизирующих или нет~--- не имеет значения. После того, как вы получите работающий код и
профилирование покажет, что вам нужна оптимизация, вы можете заменить неразрушающие
операции со списками на их утилизирующие варианты, но только при условии, что аргументы
данных функций не используются где-то ещё.
Важный момент, на который следует обратить внимание: функции сортировки списков
\code{SORT}, \code{STABLE-SORT} и \code{MERGE} из главы~\ref{ch:11} также являются
утилизирующими функциями когда они применяются к спискам\footnote{\code{SORT} и
\code{STABLE-SORT} могут также применяться к векторам ради их побочных эффектов, но
так как они возвращают отсортированный вектор, то вы должны использовать эти функции только
ради возвращаемого ими значения из соображений единого подхода и логичности}. Эти
функции не имеют неразрушающих аналогов, так что если вам необходимо отсортировать списки
не разрушая их, вы должны передать в сортирующую функцию копию списка, сделанную с помощью
\code{COPY-LIST}. В любом случае, вы должны сохранить результат функции, так как исходный
аргумент-список будет разрушен. Например:
\begin{lstlisting}
CL-USER> (defparameter *list* (list 4 3 2 1))
*LIST*
CL-USER> (sort *list* #'<)
(1 2 3 4) ; кажется то, что надо
CL-USER> *list*
(4) ; упс!
\end{lstlisting}
\section{Функции для работы со списками}
\label{sec:12-list-funcs}
Теперь вы готовы взглянуть на библиотеку функций для работы со списками, которую
предоставляет Common Lisp.
Вы уже видели базовые функции для извлечения элементов списка: \code{FIRST} и
\code{REST}. Хотя вы можете получить любой элемент списка комбинируя вызовы \code{REST}
(для продвижения по списку) и \code{FIRST} (для выделения элемента), это может быть
утомительным занятием. Поэтому Lisp предоставляет функции от \code{SECOND} до \code{TENTH}
извлекающие соответствующие элементы списка. Более общая функция \code{NTH} принимает два
аргумента: индекс и список, и возвращает n-ый (начиная с нуля) элемент списка. Также
существует функция \code{NTHCDR}, принимающая индекс и список и возвращающая результат
n-кратного применения \code{REST} к списку. Таким образом, \code{(nthcdr 0 ...)} просто
возвращает исходный список, а \code{(nthcdr 1 ...)} эквивалентно вызову
\code{REST}. Имейте в виду, что ни одна из этих функций не является более производительной
по сравнению с эквивалентной комбинацией \code{FIRST} и \code{REST}, так как нет иного
способа получить n-ый элемент списка без n-кратного вызова \code{CDR}\footnote{\code{NTH}
приблизительно эквивалентна функции \code{ELT}, но работает только на списках. В
противоположность \code{ELT}, \code{NTH} принимает индекс как первый аргумент. Другая
разница между ними состоит в том, что \code{ELT} сигнализирует об ошибке при попытке
доступа к элементу в позиции, превышающей или равной длине списка, в то время как
\code{NTH} вернёт \code{NIL}}.
28 составных \code{CAR/CDR} функций составляют ещё одно семейство, которое вы можете время
от времени использовать. Имя каждой функции получается подстановкой до 4 букв A или D
между C и R, каждая A представляет вызов \code{CAR}, а каждая D~--- \code{CDR}. Таким
образом:
\begin{lstlisting}
(caar list) === (car (car list))
(cadr list) === (car (cdr list))
(cadadr list) === (car (cdr (car (cdr list))))
\end{lstlisting}
Обратите внимание, что многие из этих функций имеют смысл только применительно к спискам,
содержащим другие списки. Например, \code{CAAR} возвращает \code{CAR} от \code{CAR}
исходного списка, таким образом, этот список должен иметь своим первым элементом
список. Другими словами, эти функции для работы скорее с деревьями нежели со списками:
\begin{lstlisting}
(caar (list 1 2 3)) ==> ошибка
(caar (list (list 1 2) 3)) ==> 1
(cadr (list (list 1 2) (list 3 4))) ==> (3 4)
(caadr (list (list 1 2) (list 3 4))) ==> 3
\end{lstlisting}
В настоящее время, эти функции не используются так часто, как раньше. Даже упёртые
Lisp-хакеры старой закалки стремятся избегать этих длинных комбинаций. Однако они
присутствуют в старом Lisp-коде, поэтому необходимо представлять, как они
работают\footnote{В частности, они использовались для извлечения различных частей
выражения, переданного в макрос до того, как был изобретён неструктурированный список
параметров FIXME. Например, вы могли разложить выражение
\begin{lstlisting}
(when (> x 10) (print x))
\end{lstlisting}
таким образом:
\begin{lstlisting}
;; условие
(cadr '(when (> x 10) (print x))) ==> (> X 10)
;; тело как список
(cddr '(when (> x 10) (print x))) ==> ((PRINT X))
\end{lstlisting}
}.
Функции \code{FIRST}-\code{TENTH}, \code{CAR}, \code{CADR} и т.д. могут также быть
использованы как аргумент к \code{SETF}, если вы пишите в нефункциональном стиле.
\begin{figure}[tb]
\begin{tabular}{|c|p{110mm}|}
\hline
Функция& \multicolumn{1}{c|}{Описание}\\
\hline
\code{LAST} &Возвращает последнюю cons-ячейку в списке. Если вызывается с целочисленным аргументом n, возвращает n ячеек.\\
\code{BUTLAST} &Возвращает копию списка без последней cons-ячейки. Если вызывается с целочисленным аргументом n, исключает последние n ячеек.\\
\code{NBUTLAST} &Утилизирующая версия \code{BUTLAST}; может изменять переданный список-аргумент, не имеет строго заданных побочных эффектов.\\
\code{LDIFF} &Возвращает копию списка до заданной cons-ячейки.\\
\code{TAILP} &Возвращает \code{TRUE} если переданный объект является cons-ячейкой, которая является частью списка.\\
\code{LIST*} &Строит список, содержащий все переданные аргументы кроме последнего, после этого присваивает \code{CDR} последней cons-ячейки списка последнему аргументу. Т.е. смесь \code{LIST} и \code{APPEND}.\\
\code{MAKE-LIST}&Строит список из n элементов. Начальные элементы списка \code{NIL} или значение заданное аргументом :initial-element.\\
\code{REVAPPEND}&Комбинация \code{REVERSE} и \code{APPEND}; инвертирует первый аргумент как \code{REVERSE} и добавляет второй аргумент. Не имеет строго заданных побочных эффектов.\\
\code{NRECONC} &Утилизирующая версия предыдущей функции; инвертирует первый аргумент как это делает \code{NREVERSE} и добавляет второй аргумент. Не имеет строгих побочных эффектов.\\
\code{CONSP} &Предикат для тестирования является ли объект cons-ячейкой.\\
\code{ATOM} &Предикат для тестирования является ли объект не cons-ячейкой.\\
\code{LISTP} &Предикат для тестирования является ли объект cons-ячейкой или \code{NIL}\\
\code{NULL} &Предикат для тестирования является ли объект \code{NIL}. Функционально эквивалентен функции \code{NOT}, но стилистически лучше использовать \code{NULL} при тестировании является ли список \code{NIL}, \code{NOT} для проверки булевого выражения \code{FALSE} \\
\hline
\end{tabular}
\caption{Другие функции для работы со списками}
\label{table:12-other}
\end{figure}
\section{Отображение}
\label{sec:12-map}
Другой важный аспект функционального стиля программирования~--- это использование функций
высших порядков, т.е. функций, которые принимают функции в качестве аргументов или
возвращают функции. Вы видели несколько примеров функций высших порядков, таких как
\code{MAP}, в предыдущей главе. Хотя \code{MAP} может использоваться как со списками, так
и с векторами (т.е. с любым типом последовательностей), Common Lisp также предоставляет 6
функций отображения специально для списков. Разница между этими шестью функциями в том как
они строят свой результат и в том применяют ли они переданные функции к элементам списка
или к cons-ячейкам списка.
\code{MAPCAR} функция наиболее похожая на \code{MAP}. Так как она всегда возвращает список,
она не требует уточнения типа результата, как \code{MAP}. Вместо этого, первый аргумент
\code{MAPCAR}~--- функция, которую необходимо применить, а последующие аргументы~--- списки,
чьи элементы будут поставлять аргументы для этой функции. Другими словами \code{MAPCAR}
ведёт себя, как \code{MAP}: функция применяется к элементам аргументов-списков, беря от
каждого списка по элементу. Результат каждого вызова функции собирается в новый
список. Например:
\begin{lstlisting}
(mapcar #'(lambda (x) (* 2 x)) (list 1 2 3)) ==> (2 4 6)
(mapcar #'+ (list 1 2 3) (list 10 20 30)) ==> (11 22 33)
\end{lstlisting}
\code{MAPLIST} работает как \code{MAPCAR}, только вместо того, чтобы передавать функции
элементы списка, она передаёт cons-ячейки\footnote{Т.е. \code{MAPLIST} более примитивная
из двух функций: если у вас есть только \code{MAPLIST} вы можете построить на её основе
\code{MAPCAR}, но построить \code{MAPLIST} на основе \code{MAPCAR} невозможно}. Таким
образом, функция имеет доступ не только к значению каждого элемента списка (через функцию
\code{CAR}, применённую к cons-ячейке), но и к хвосту списка (через \code{CDR}).
\code{MAPCAN} и \code{MAPCON} работают как \code{MAPCAR} и \code{MAPLIST}, разница состоит
в том, как они строят свой результат. В то время как \code{MAPCAR} и \code{MAPLIST} строят
новый список, содержащий результаты вызова функций, \code{MAPCAN} и \code{MAPCON} строят
результат склеиванием результатов (которые должны быть списками), как это делает
\code{NCONC}. Таким образом, каждый вызов функции может предоставлять любое количество
элементов, для включения в результирующий список \footnote{В диалектах Lisp, которые не
имеют фильтрующей функции, такой как \code{REMOVE} возможным способом фильтрации списка
является применение \code{MAPCAN}:
\begin{lstlisting}
(mapcan #'(lambda (x) (if (= x 10) nil (list x))) list) === (remove 10 list)
\end{lstlisting}
}. \code{MAPCAN}, как \code{MAPCAR}, передаёт элементы списка отображающей функции, а
\code{MAPCON}, как \code{MAPLIST}, передаёт cons-ячейки.
Наконец, функции \code{MAPC} и \code{MAPL}~--- управляющие структуры, замаскированные под
функции, они просто возвращают свой первый аргумент-список, так что они используются
только из-за побочных эффектов передаваемой функции. \code{MAPC} действует как
\code{MAPCAR} и \code{MAPCAN}, а \code{MAPL} как \code{MAPLIST} и \code{MAPCON}.
\section{Другие структуры}
Хотя cons-ячейки и списки обычно трактуются как синонимы, это не всегда так~--- как я
говорил раннее, вы можете использовать списки списков для представления деревьев. Так же
как функции из этой главы позволяют вам интерпретировать структуры, построенные из
cons-ячеек, как списки, другие функции позволяют использовать cons-ячейки для
представления деревьев, множеств, и двух типов отображений ключ-значение. Мы рассмотрим
большинство этих функций в следующей главе.
%%% Local Variables:
%%% mode: latex
%%% TeX-master: "pcl-ru"
%%% TeX-open-quote: "<<"
%%% TeX-close-quote: ">>"
%%% End: