-
Notifications
You must be signed in to change notification settings - Fork 8
/
chapter-14.tex
863 lines (729 loc) · 79.4 KB
/
chapter-14.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
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
\chapter{Файлы и файловый ввод/вывод}
\label{ch:14}
\thispagestyle{empty}
Common Lisp предоставляет мощную библиотеку для работы с файлами. В~этой главе я
остановлюсь на нескольких элементарных задачах, относящихся к файловыми операциям: чтение,
запись, а также отображение структуры файловой системы. Средства Common Lisp,
предназначенные для ввода/вывода, аналогичны имеющимся в других языках
программирования. Common Lisp предоставляет потоковую абстракцию операций чтения/записи, а
для манипулирования файловыми объектами в независимом от операционной системы формате~---
абстракцию файловых путей. Кроме того, Common Lisp предоставляет некоторое
количество уникальных возможностей, такие как чтение и запись s-выражений.
\section{Чтение данных из файлов}
Самая фундаментальная задача ввода/вывода~--- чтение содержимого файла. Для того, чтобы
получить поток, из которого вы можете прочитать содержимое файла, используется функция
\lstinline{OPEN}. По умолчанию, \lstinline{OPEN} возвращает посимвольный поток ввода данных, который
можно передать множеству функций, считывающих один или несколько символов текста:
\lstinline{READ-CHAR} считывает одиночный символ; \lstinline{READ-LINE} считывает строку текста,
возвращая её как строку без символа конца строки; функция \lstinline{READ} считывает одиночное
s-выражение, возвращая объект Lisp. Когда работа с потоком завершена, вы можете закрыть
его с помощью функции \lstinline{CLOSE}.
Функция \lstinline{OPEN} требует имя файла как единственный обязательный аргумент. Как можно
увидеть в разделе~<<\nameref{ch14:file-names}>>, Common Lisp предоставляет два пути для представления
имени файла, но наиболее простой способ~--- использовать строку, содержащую имя в формате,
используемом в файловой системе. Так, предполагая, что \lstinline{/some/file/name.txt} это
файл, возможно открыть его следующим образом:
\begin{myverb}
(open "/some/file/name.txt")
\end{myverb}
Вы можете использовать объект, возвращаемый функцией, как первый аргумент любой функции,
осуществляющей чтение. Например, для того, чтобы напечатать первую строку файла, вы можете
комбинировать \lstinline{OPEN}, \lstinline{READ-LINE}, \lstinline{CLOSE} следующим образом:
\begin{myverb}
(let ((in (open "/some/file/name.txt")))
(format t "~a~%" (read-line in))
(close in))
\end{myverb}
Конечно, при открытии и чтении данных может произойти ряд ошибок. Файл может не
существовать, или вы можете непредвиденно достигнуть конца файла в процессе его чтения. По
умолчанию, \lstinline{OPEN} и \lstinline{READ-*} будут сигнализировать об ошибках в данных
ситуациях. В~главе~\ref{ch:19} я рассмотрю, как обработать эти ошибки. Сейчас же, однако, будем
использовать более легковесное решение: каждая из этих функций принимает аргументы,
которые изменяют её реакцию на исключительные ситуации.
Если вы хотите открыть файл, который возможно не существует, без генерации ошибки
функцией \lstinline{OPEN}, вы можете использовать аргумент \lstinline{:if-does-not-exists} для того,
чтобы указать другое поведение. Три различных значения допустимы для данного аргумента~---
\lstinline{:error}, по умолчанию; \lstinline{:create}, что указывает на необходимость создания файла
и повторное его открытие как существующего и \lstinline{NIL}, что означает возврат \lstinline{NIL}
(при неуспешном открытии) вместо потока. Итак, возможно изменить предыдущий пример таким
образом, чтобы обработать несуществующий файл.
\begin{myverb}
(let ((in (open "/some/file/name.txt" :if-does-not-exist nil)))
(when in
(format t "~a~%" (read-line in))
(close in)))
\end{myverb}
Все функции чтения~--- \lstinline{READ-CHAR}, \lstinline{READ-LINE}, \lstinline{READ}~--- принимают
опциональный аргумент, по умолчанию <<истина>>, который указывает должны ли они
сигнализировать об ошибке, если они достигли конца файла. Если этот аргумент установлен в
\lstinline{NIL}, то они возвращают значение их 3-го аргумента, который по умолчанию \lstinline{NIL},
вместо ошибки. Таким образом, вывести на печать все строки файла можно следующим способом:
\begin{myverb}
(let ((in (open "/some/file/name.txt" :if-does-not-exist nil)))
(when in
(loop for line = (read-line in nil)
while line do (format t "~a~%" line))
(close in)))
\end{myverb}
Среди трёх функций чтения, \lstinline{READ}~--- уникальна. Это~--- та самая функция, которая
представляет букву <<R>> в <<REPL>>, и которая используется для того, чтобы читать исходный
код Lisp. Во время вызова она читает одиночное s-выражение, пропуская пробельные символы и
комментарии, и возвращает объект Lisp, представляемый s-выражением. Например, предположим,
что файл \lstinline{/some/file/name.txt} содержит следующие строки:
\begin{myverb}
(1 2 3)
456
"строка" ; это комментарий
((a b)
(c d))
\end{myverb}
Другими словами, он содержит 4 s-выражения: список чисел, число, строку, и список
списков. Вы можете считать эти выражения следующим образом:
\begin{myverb}
CL-USER> (defparameter *s* (open "/some/file/name.txt"))
*S*
CL-USER> (read *s*)
(1 2 3)
CL-USER> (read *s*)
456
CL-USER> (read *s*)
"строка"
CL-USER> (read *s*)
((A B) (C D))
CL-USER> (close *s*)
T
\end{myverb}
Как было показано в главе~\ref{ch:03}, возможно использовать \lstinline{PRINT} для того, чтобы выводить
объекты Lisp на печать в удобочитаемой форме. Итак, когда необходимо хранить данные в
файлах, \lstinline{PRINT} и \lstinline{READ} предоставляют простой способ делать это без создания
специального формата данных и парсера для их прочтения. Вы даже можете использовать
комментарии без ограничений. И, поскольку s-выражения создавались для того, чтобы быть
редактируемыми людьми, то они так же хорошо подходят для использования в качестве формата
конфигурационных файлов\footnote{Заметим, однако, что процедура чтения Lisp, зная как
пропускать комментарии, полностью их пропускает. Поэтому, если вы считаете
конфигурационный файл, содержащий комментарии, при помощи \lstinline{READ}, а затем запишете
изменения при помощи \lstinline{PRINT}, то потеряете все комментарии.}\hspace{\footnotenegspace}.
\section{Чтение двоичных данных}
По умолчанию \lstinline{OPEN} возвращает символьные потоки, которые преобразуют байты в символы
в соответствии с конкретной схемой кодирования символов\footnote{По умолчанию \lstinline{OPEN}
использует кодировку, используемую в операционной системе, но возможно указать ключевой
параметер \lstinline{:external-format}, в котором передать используемую схему кодирования,
отличную от используемой в операционной системе. Символьные потоки также преобразуют
платформозависимые символы конца строки в символ \lstinline!#\Newline!.}\hspace{\footnotenegspace}.
Для чтения потока байтов необходимо передать функции \lstinline{OPEN} ключевой параметр
\lstinline{:element-type} со значением \lstinline{'(unsigned-byte 8)}\footnote{Тип
\lstinline{(unsigned-byte 8)} обозначает 8-битный беззнаковый байт; <<Байты>> в Common Lisp
могут иметь различный размер поскольку Lisp может выполняться на различных платформах с
размерами байтов от 6 до 9 бит, к примеру PDP-10, может адресовать битовые поля
различной длины от 1 до 36 бит.}\hspace{\footnotenegspace}. Полученный поток можно передать функции
\lstinline{READ-BYTE}, которая будет возвращать целое число от 0 до 255 во время каждого
вызова. \lstinline{READ-BYTE}, так же, как и функции, работающие с потоками символов, принимает
опциональные аргументы, которые указывают, должна ли она сигнализировать об ошибке, если
достигнут конец файла, и какое значение возвращать в противном случае. В~главе~\ref{ch:24}
мы построим библиотеку, которая позволит удобно читать структурированные бинарные данные,
используя \lstinline{READ-BYTE}\footnote{В~общем, поток может быть либо символьным, либо
бинарным, так что невозможно смешивать вызовы \lstinline{READ-BYTE} с \lstinline{READ-CHAR} и
другими символьными функциями. Однако, в некоторых реализациях, таких как Allegro,
поддерживаются так называемые бивалентные потоки, которые поддерживают как символьные,
так и байтовые операции ввода/вывода.}\hspace{\footnotenegspace}.
\section{Блочное чтение}
Последняя функция для чтения, \lstinline{READ-SEQUENCE}, работает с бинарными и символьными
потоками. Ей передаётся последовательность (обычно вектор) и поток, и она пытается
заполнить эту последовательность данными из потока. Функция возвращает индекс первого
элемента последовательности, который не был заполнен, либо её длину, если она была
заполнена полностью. Так же возможно передать ключевые аргументы \lstinline{:start} и
\lstinline{:end}, которые указывают на подпоследовательность, которая должна быть заполнена
вместо последовательности. Аргумент, определяющий последовательность должен быть типом,
который может хранить элементы, из которых состоит поток. Поскольку большинство
операционных систем поддерживают только одну какую-либо форму блочных операций
ввода/вывода, \lstinline{READ-SEQUENCE} скорее всего более эффективна чем чтение
последовательных данных несколькими вызовами \lstinline{READ-BYTE} или \lstinline{READ-CHAR}.
\section{Файловый вывод}
Для записи данных в файл необходим поток вывода, который можно получить вызовом функции
\lstinline{OPEN} с ключевым аргументом \lstinline{:direction} \lstinline{:output}. Когда файл открывается
для записи, \lstinline{OPEN} предполагает, что файл не должен существовать, и будет сообщать об
ошибке в противном случае. Однако, возможно изменить это поведение с помощью ключевого
аргумента \lstinline{:if-exists}. Передавая значение \lstinline{:supersede} можно вызвать замену
существующего файла. Значение :append позволяет осуществлять запись таким образом, что
новые данные будут помещены в конец файла, а значение \lstinline{:overwrite} возвращает поток,
который будет переписывать существующие данные с начала файла. Если же передать
\lstinline{NIL}, то \lstinline{OPEN} вернёт \lstinline{NIL} вместо потока, если файл уже
существует. Характерное использование \lstinline{OPEN} для вывода данных выглядит следующим
образом:
\begin{myverb}
(open "/some/file/name.txt" :direction :output :if-exists :supersede)
\end{myverb}
Common Lisp также предоставляет некоторые функции для записи данных: \lstinline{WRITE-CHAR}
пишет одиночный символ в поток. \lstinline{WRITE-LINE} пишет строку, за которой следует символ
конца строки, с учётом реализации для конкретной платформы. Другая функция,
\lstinline{WRITE-STRING} пишет строку, не добавляя символ конца строки. Две разные функции
могут использоваться для того чтобы вывести символ конца строки: \lstinline{TERPRI}~---
сокращение для "TERminate PRInt" (закончить печать) безусловно печатает символ конца
строки, а \lstinline{FRESH-LINE} печатает символ конца строки только в том случае, если текущая
позиция печати не совпадает с началом строки. \lstinline{FRESH-LINE} удобна в том случае, когда
желательно избежать паразитных пустых строк в текстовом выводе, генерируемом другими
последовательно вызываемыми функциями. Допустим, например, что есть одна функция, которая
генерирует вывод и после которой обязательно должен идти перенос строки и другая, которая
должна начинаться с новой строки. Но, предположим, что если функции вызываются
последовательно, то необходимо обеспечить отсутствие лишних пустых строк в их выводе. Если
в начале второй функции используется \lstinline{FRESH-LINE}, её вывод будет постоянно начинать
с новой строки, но если она вызывается непосредственно после первой функции, то не будет
выводиться лишний перевод строки.
Некоторые функции позволяют вывести данные Lisp в форме s-выражений: \lstinline{PRINT} печатает
s-выражение, предваряя его символом начала строки, и пробельным символом
после. \lstinline{PRIN1} печатает только s-выражение. А функция \lstinline{PPRINT} печатает
s-выражения аналогично \lstinline{PRINT} и \lstinline{PRIN1}, но использует <<красивую печать>>,
которая пытается печатать s-выражения в эстетически красивом виде.
Однако, не все объекты могут быть напечатаны в том формате, который понимает
\lstinline{READ}. Переменная \lstinline{*PRINT-READABLY*} контролирует поведение при попытке
напечатать подобный объект с помощью \lstinline{PRINT}, \lstinline{PRIN1} или \lstinline{PPRINT}. Когда
она равна \lstinline{NIL}, эти функции напечатают объект в таком формате, что \lstinline{READ} при
попытке чтения гарантировано сообщит об ошибке; в ином случае они просигнализируют об
ошибке вместо того, чтобы напечатать объект.
Еще одна функция, \lstinline{PRINC}, также печатает объекты Lisp, но в виде, удобном для
человеческого восприятия. Например, \lstinline{PRINC} печатает строки без кавычек. Текстовый
вывод может быть ещё более замысловатым, если задействовать потрясающе гибкую, и в
некоторой степени загадочную функцию \lstinline{FORMAT}. В~главе~\ref{ch:18} я расскажу о
некоторых важных тонкостях этой функции, которая, по сути, определяет мини-язык для
форматированного вывода.
Для того, чтобы записать двоичные данные в файл, следует открыть файл функцией \lstinline{OPEN}
с тем же самым аргументом \lstinline{:element-type}, который использовался при чтении данных:
\lstinline{'(unsigned-byte 8)}. После этого можно записывать в поток отдельные байты функцией
\lstinline{WRITE-BYTE}.
Функция блочного вывода \lstinline{WRITE-SEQUENCE} принимает как двоичные, так и символьные
потоки до тех пор, пока элементы последовательности имеют подходящий тип: символы или
байты. Так же как и \lstinline{READ-SEQUENCE}, эта функция наверняка более эффективна, чем
запись элементов последовательности поодиночке.
\section{Закрытие файлов}
Любой, кто писал программы, взаимодействующие с файлами, знает, что важно закрывать файлы,
когда работа с ними закончена, так как дескрипторы норовят быть дефицитным ресурсом. Если
открывают файлы и забывают их закрывать, вскоре обнаруживают, что больше нельзя открыть ни
одного файла\pclfootnote{Некоторые могут полагать, что это не является проблемой в языках со
сборщиком мусора, таких как Lisp. В~большинстве реализаций Lisp все потоки, которые
больше не нужны, автоматически закрываются. Но на это не надо полагаться, так как
сборщик мусора запускается, как правило, когда остаётся мало памяти. Он ничего не знает
о других дефицитных ресурсах таких, как файловые дескрипторы. Если доступно много
памяти, то доступные файловые дескрипторы могут быстро закончиться до вызова сборщика
мусора.}. На первый взгляд может показаться, что достаточно каждый вызов \lstinline{OPEN}
сопоставить с вызовом \lstinline{CLOSE}. Например, можно всегда обрамлять код, использующий
файл, как показано ниже:
\begin{myverb}
(let ((stream (open "/some/file/name.txt")))
;; работа с потоком
(close stream))
\end{myverb}
Однако этом метод имеет две проблемы. Первая~--- он предрасположен к ошибкам: если забыть
написать \lstinline{CLOSE}, то будет происходить утечка дескрипторов при каждом вызове этого
кода. Вторая~--- наиболее значительная~--- нет гарантии, что \lstinline{CLOSE} будет
достигнут. Например, если в коде, расположенном до \lstinline{CLOSE}, есть \lstinline{RETURN} или
\lstinline{RETURN-FROM}, возвращение из \lstinline{LET} произойдёт без закрытия потока. Или, как вы
увидите в главе~\ref{ch:19}, если какая-либо часть кода до \lstinline{CLOSE} сигнализирует об
ошибке, управление может перейти за пределы \lstinline{LET} обработчику ошибки и никогда не
вернётся, чтобы закрыть поток.
Common Lisp предоставляет общее решение того, как удостовериться, что определённый код
всегда исполняется: специальный оператор \lstinline{UNWIND-PROTECT}, о котором я расскажу в
главе~\ref{ch:20}. Так как открытие файла, работа с ним и последующее закрытие очень часто
употребляются, Common Lisp предлагает макрос, \lstinline{WITH-OPEN-FILE}, основанный на
\lstinline{UNWIND-PROTECT}, для скрытия этих действий. Ниже — основная форма:
\begin{myverb}
(with-open-file (stream-var open-argument*)
body-form*)
\end{myverb}
Выражения в \lstinline{body-form*} вычисляются с \lstinline{stream-var}, связанной с файловым
потоком, открытым вызовом \lstinline{OPEN} с аргументами
\lstinline{open-argument*}. \lstinline{WITH-OPEN-FILE} удостоверяется, что поток \lstinline{stream-var}
закрывается до того, как из \lstinline{WITH-OPEN-FILE} вернётся управление. Поэтому читать файл
можно следующим образом:
\begin{myverb}
(with-open-file (stream "/some/file/name.txt")
(format t "~a~%" (read-line stream)))
\end{myverb}
Создать файл можно так:
\begin{myverb}
(with-open-file (stream "/some/file/name.txt" :direction :output)
(format stream "Какой-то текст."))
\end{myverb}
Как правило, \lstinline{WITH-OPEN-FILE} используется в 90-99 процентах файлового
ввода/вывода. Вызовы \lstinline{OPEN} и \lstinline{CLOSE} понадобятся, если файл нужно открыть в
какой-либо функции и оставить поток открытым при возврате из неё. В~таком случае вы должны
позаботиться о закрытии потока самостоятельно, иначе произойдёт утечка файловых
дескрипторов и, в конце концов, вы больше не сможете открыть ни одного файла.
\section{Имена файлов}
\label{ch14:file-names}
До сих пор мы использовали строки для представления имён файлов. Однако, использование
строк как имён файлов привязывает код к конкретной операционной и файловой системам. Точно
так же, если конструировать имена в соответствии правилам конкретной схемы именования
(скажем, разделение директорий знаком <</>>), то вы также привязаны к одной определённой
файловой системе.
Для того, чтобы избежать подобной непереносимости программ, Common Lisp предоставляет
другое представление файловых имён: объекты файловых путей. Файловые пути представляют
собой файловые имена в структурированном виде, что делает их использование лёгким без
привязки к определённому синтаксису файловых имён. А бремя по переводу между строками в
локальном представлении~--- строками имён~--- и файловыми путями ложится на плечи реализаций
Lisp.
К несчастью, как и со многими абстракциями, спроектированными для скрытия деталей
различных базовых систем, абстракция файловых путей привносит свои трудности. В~то время,
когда разрабатывались файловые пути, разнообразие широко используемых файловых систем было
чуть значительнее, чем сегодня. Но это мало проясняет ситуацию, если все, о чем вы
заботитесь,~--- представление имён файлов в Unix или Windows. Однако однажды поняв какие
части этой абстракции можно забыть, как артефакты истории развития файловых путей, вы
сможете ловко управлять именами файлов\pclfootnote{Еще одна причина, по которой система
файловых путей выглядит причудливо,~--- введение логических файловых путей. Однако, вы
можете плодотворно использовать систему файловых путей с единственной мыслью в голове о
логических файловых путях: о них можно не вспоминать. В~двух словах, логические файловые
пути позволяют программам, написанных на Common Lisp, ссылаться на файловые пути без
именования конкретного файла. Они могут быть отображены впоследствии на конкретную точку
файловой системы, когда будет установлена ваша программа, при помощи <<преобразования
логических файловых путей>>, которое преобразует эти имена, соответствующие определённому
образцу, в файловые пути, представляющие файлы в файловой системе, так называемые
физические файловые пути. Они находят применение в определённых ситуациях, но вы может
со спокойной душой пройти мимо них.}.
\vspace{1cm}
\textintable{Как мы до этого докатились?}{Историческое разнообразие файловых систем, существующих в период 70-80 годов, можно легко
забыть. Кент Питман, один из ведущих технических редакторов стандарта Common Lisp, описал
однажды ситуацию в comp.lang.lisp (Message-ID: \lstinline{sfwzo74np6w.fsf@world.std.com}) так:
В~момент завершения проектирования Common Lisp господствующими файловыми системами были
\lstinline{TOPS-10}, \lstinline{TENEX}, \lstinline{TOPS-20}, \lstinline{VAX VMS}, \lstinline{AT\&T Unix}, \lstinline{MIT
Multics}, \lstinline{MIT ITS}, и это не упоминаю группу систем для мэйнфрэймов. В~некоторых
системах имена файлов были только в верхнем регистре, в других~--- смешанные, в третьих~---
чувствительны к регистру, но с возможностью преобразования (как в CL). Какие-то имели
групповые символы (wildcards), какие-то~--- нет. Одни имели \lstinline{:вверх} (\lstinline{:up}) в
относительных файловых путях, другие~--- нет. Также существовали файловые системы без
каталогов, файловые системы без иерархической структуры каталогов, файловые системы без
типов файлов, файловые системы без версий, файловые системы без устройств и т.д.
Если сейчас посмотреть на абстракцию файловых путей с точки зрения какой-нибудь
определённой файловой системы, она выглядит нелепо. Но если взять в рассмотрение даже
такие две похожие файловые системы, как в Windows и Unix, то вы можете заметить отличия,
от которых можно отвлечься с помощью системы файловых путей. Файловые имена в Windows
содержат букву диска в то время, как в Unix нет. Другое преимущество владения абстракцией
файловых путей, которая спроектирована, чтобы оперировать большим разнообразием файловых
систем, которые существовали в прошлом,~--- её вероятная способность управлять файловыми
системами, которые будут существовать в будущем. Если, скажем, файловые системы с
сохранением всех старых данных и истории операций войдут снова в моду, Common Lisp будет к
этому готов.}
Как правило, когда возникает необходимость в файловом имени, вы можете использовать как
строку имени (namestring), так и файловый путь. Выбор зависит от того, откуда произошло
имя. Файловые имена, предоставленные пользователем~--- например, как аргументы или строки
конфигурационного файла~--- как правило, будут строками имён, так как пользователь знает
какая операционная система у него запущена, поэтому не следует ожидать, что он будет
беспокоиться о представлении файловых имён в Lisp. Но следуя общепринятой практике,
файловые имена будут представлены файловыми путями, так как они переносимы. Поток, который
возвращает \lstinline{OPEN}, также представляет файловое имя, а именно, файловое имя, которое
было изначально использовано для открытия этого потока. Вместе эти три типа упоминаются
как указатели файловых путей. Все встроенные функции, ожидающие файловое имя как аргумент,
принимают все три типа указателя файловых путей. Например, во всех предыдущих случаях,
когда вы использовали строку для представления файлового имени, вы также могли передавать
в функцию объект файлового пути или поток.
\section{Как имена путей представляют имена файлов}
Файловый путь~--- это структурированный объект, который представляет файловое имя,
используя шесть компонентов: хост, устройство, каталог, имя, тип и версия. Большинство из
них принимают атомарные значения, как правило, строки; только директория~--- структурный
компонент, содержащий список имён каталогов (как строки) с предшествующим ключевым словом:
\lstinline{:absolute} (абсолютный) или \lstinline{:relative} (относительный). Но не все компоненты
необходимы на всех платформах~--- это одна из тех вещей, которая вселяет страх в начинающих
лисперов, потому что необоснованно сложна. С другой стороны, вам не надо заботиться о том,
какие компоненты могут или нет использоваться для представления имён на определённой
файловой системе, если только вам не надо создать объект файлового пути с нуля, а это
почти никогда и не надо. Взамен Вы обычно получите объект файлового пути либо позволив
реализации преобразовать строку имени специфичной для какой-то файловой системы в объект
файлового пути, либо создав файловый путь, который перенимает большинство компонент от
какого-либо существующего.
Например, для того, чтобы преобразовать строку имени в файловый путь, используйте функцию
\lstinline{PATHNAME}. Она принимает на вход указатель файлового пути и возвращает эквивалентный
объект файлового пути. Если указатель уже является файловым путём, то он просто
возвращается. Если это поток, извлекается первоначальное файловое имя и возвращается. Если
это строка имени, она анализируется согласно локальному синтаксису файловых имён. Стандарт
языка как независимый от платформы документ не определяет какое-либо конкретное
отображение строки имени в файловый путь, но большинство реализаций следуют одним и тем же
соглашениям на данной операционной системе.
На файловых системах Unix, обычно, используются компоненты: директория, имя и тип. В
Windows на один компонент больше~--- обычно устройство или хост~--- содержит букву диска. На
этих платформах строку имени делят на части, а разделителем служит косая черта в Unix и
косая или обратная косая черты в Windows. Букву диска в Windows размещают либо в компонент
устройства или в компонент хост. Все, кроме последнего из оставшихся элементов имени,
размещаются в списке, начинающемся с \lstinline{:absolute} или \lstinline{:relative} в зависимости
от того, начинается ли имя с разделителя или нет (игнорирую букву диска, если таковая
присутствует). Список становится компонентом каталог файлового пути. Последний элемент
делится по самой крайней точке, если она есть, и полученные две части есть компоненты имя
и тип, соответственно\footnote{Многие реализации Common Lisp под Unix трактуют файловые
имена, чей последний элемент начинается с точки и не содержит больше других точек,
следующим образом: помещают весь элемент~--- вместе с точкой~--- в компонент имя и
оставляют компонент тип равным \lstinline{NIL}.
\begin{myverb}
(pathname-name (pathname "/foo/.emacs")) -> ".emacs"
(pathname-type (pathname "/foo/.emacs")) -> NIL
\end{myverb}
Однако, не все реализации следуют этому соглашению. Некоторые создают файловый путь с
пустой строкой в качестве имени и emacs в качестве типа.}\hspace{\footnotenegspace}.
Можно проверить каждый компонент файлового пути с функциями \lstinline{PATHNAME-DIRECTORY},
\lstinline{PATHNAME-NAME} и \lstinline{PATHNAME-TYPE}.
\begin{myverb}
(pathname-directory (pathname "/foo/bar/baz.txt")) -> (:ABSOLUTE "foo" "bar")
(pathname-name (pathname "/foo/bar/baz.txt")) -> "baz"
(pathname-type (pathname "/foo/bar/baz.txt")) -> "txt"
\end{myverb}
Другие три функции~--- \lstinline{PATHNAME-HOST}, \lstinline{PATHNAME-DEVICE} и
\lstinline{PATHNAME-VERSION}~--- позволяют получить остальные три составляющие файлового пути,
хотя они и не представляют интереса в Unix. В~Windows либо \lstinline{PATHNAME-HOST}, либо
\lstinline{PATHNAME-DEVICE} возвратит букву диска.
Подобно другим встроенным объектам, файловые пути обладают своим синтаксисом для чтения:
\lstinline!#p!, за которым следует строка, заключенная в двойные кавычки. Это позволяет
печатать и считывать s-выражения, содержащие объекты файлового пути, но так как синтаксис
зависит от алгоритма анализа строки, эти данные могут быть непереносимыми между разными
операционными системами.
\begin{myverb}
(pathname "/foo/bar/baz.txt") -> #p"/foo/bar/baz.txt"
\end{myverb}
Для того, чтобы файловый путь преобразовать обратно в строку имени~--- например, чтобы
представить его пользователю~--- следует воспользоваться функцией \lstinline{NAMESTRING},
которая принимает указатель файлового пути и возвращает строку имени. Две других
функции~--- \lstinline{DIRECTORY-NAMESTRING} и \lstinline{FILE-NAMESTRING}~--- возвращают часть
строки имени. \lstinline{DIRECTORY-NAMESTRING} соединяет элементы компонента каталог в
локальное имя каталога. \lstinline{FILE-NAMESTRING}~--- компоненты имя и тип\footnote{Имя,
возвращённое функцией \lstinline{FILE-NAMESTRING}, также включает компонент версия на
файловой системе, которая использует это.}\hspace{\footnotenegspace}.
\begin{myverb}
(namestring #p"/foo/bar/baz.txt") -> "/foo/bar/baz.txt"
(directory-namestring #p"/foo/bar/baz.txt") -> "/foo/bar/"
(file-namestring #p"/foo/bar/baz.txt") -> "baz.txt"
\end{myverb}
\section{Конструирование имён путей}
Вы можете создать файловый путь, используя функцию \lstinline{MAKE-PATHNAME}. Она принимает по
одному аргументу-ключу на каждую компоненту файлового пути и возвращает файловый путь,
заполненный всеми предоставленными компонентами\footnote{Хост не может быть равным
\lstinline{NIL}, но если все же это так, то он будет заполнен значением, определённым
конкретной реализаций.}\hspace{\footnotenegspace}.
\begin{myverb}
(make-pathname
:directory '(:absolute "foo" "bar")
:name "baz"
:type "txt") -> #p"/foo/bar/baz.txt"
\end{myverb}
Однако, если вы желаете, чтобы ваши программы были переносимыми, то вряд ли вы пожелаете
создавать файловые пути с нуля, даже если абстракция файловых путей предохраняет вас от
непереносимого синтаксиса файловых имён, ведь файловые имена могут быть непереносимыми ещё
множеством способов. Например, файловое имя \lstinline{"/home/peter/foo.txt"} не очень-то
подходит для OS X, в которой \lstinline{/home/} представлено \lstinline{/Users/}.
Другой причиной, по которой не следует создавать файловые пути с нуля, является тот факт,
что различные реализации используют компоненты файлового пути с небольшими
различиями. Например, как было упомянуто выше, некоторые Windows-реализации LISP хранят
букву диска в компоненте устройство в то время, как другие~--- в компоненте хост. Если вы
напишите:
\begin{myverb}
(make-pathname :device "c" :directory '(:absolute "foo" "bar") :name "baz")
\end{myverb}
то это может быть правильным при использовании одной реализации, но не другой.
Вместо того, чтобы создавать пути с нуля, проще создать новый файловый путь, используя
существующий файловый путь, при помощи аргумента-ключа :defaults функции
\lstinline{MAKE-PATHNAME}. С этим параметром можно предоставить указатель файлового пути, из
которого будут взяты компоненты, не указанные другими аргументами. Для примера, следующее
выражение создаёт файловый путь с расширением \lstinline{.html} и компонентами из файлового
пути \lstinline{input-file}:
\begin{myverb}
(make-pathname :type "html" :defaults input-file)
\end{myverb}
Предполагая, что значение \lstinline{input-file} было предоставлено пользователем, этот код~---
надёжен вопреки различиям операционных систем и реализаций, таким как наличие либо
отсутствие в файловом пути буквы диска или место её расположения\footnote{Для
максимальной переносимости, следует писать:
\begin{myverb}
(make-pathname :type "html" :version :newest :defaults input-file)
\end{myverb}
Без аргумента :version на файловой системе с контролем версий, результирующий файловый
путь унаследует номер версии от входного файла, который, вероятнее всего, будет
неправильным, ведь если файл сохранялся не раз, он будет иметь больший номер, чем
созданный \lstinline{HTML} файл. В~реализациях без поддержки контроля версий, аргумент
\lstinline{:version} должен игнорироваться. Забота о переносимости~--- на вашей совести.}\hspace{\footnotenegspace}.
Используя эту же технику, можно создать файловый путь с другой компонентой директория.
\begin{myverb}
(make-pathname :directory '(:relative "backups") :defaults input-file)
\end{myverb}
Однако этот код создаст файловый путь с компонентой директория, равной относительному пути
\lstinline!backups/!, безотносительно к любым другим компонентам файла \lstinline!input-file!.
Например:
\begin{myverb}
(make-pathname :directory '(:relative "backups")
:defaults #p"/foo/bar/baz.txt") -> #p"backups/baz.txt"
\end{myverb}
Возможно, когда-нибудь вы захотите объединить два файловых пути, один из которых имеет
относительный компонент директория, путём комбинирования их компонент
директория. Например, предположим, что имеется относительный файловый путь
\lstinline!#p"foo/bar.html"!, который вы хотите объединить с абсолютным файловым путём
\lstinline!#p"/www/html/"!, чтобы получить \lstinline!#p"/www/html/foo/bar.html"!. В~этом
случае \lstinline{MAKE-PATHNAME} не подойдёт; то, что вам надо,~--- \lstinline{MERGE-PATHNAMES}.
\lstinline{MERGE-PATHNAMES} принимает два файловых пути и соединяет их, заполняя при этом
компоненты, которые в первом файловом пути равны \lstinline{NIL}, соответствующими значениями
из второго файлового пути. Это очень похоже на \lstinline{MAKE-PATHNAME}, которая заполняет все
неопределённые компоненты значениями, предоставленными аргументом
\lstinline{:defaults}. Однако, \lstinline{MERGE-PATHNAMES} особенно относится к компоненте
директория: если директория первого файлового пути~--- относительна, то директорией
окончательного файлового пути будет директория первого пути относительно директории
второго. Так:
\begin{myverb}
(merge-pathnames #p"foo/bar.html" #p"/www/html/") -> #p"/www/html/foo/bar.html"
\end{myverb}
Второй файловый путь также может быть относительным. В~этом случае окончательный путь
также будет относительным.
\begin{myverb}
(merge-pathnames #p"foo/bar.html" #p"html/") -> #p"html/foo/bar.html"
\end{myverb}
Для того, чтобы обратить это процесс, то есть получить файловый путь, который относителен
определённой корневой директории, используйте полезную функцию \lstinline{ENOUGH-NAMESTRING}.
\begin{myverb}
(enough-namestring #p"/www/html/foo/bar.html" #p"/www/") -> "html/foo/bar.html"
\end{myverb}
Вы можете соединить \lstinline{ENOUGH-NAMESTRING} и \lstinline{MERGE-PATHNAMES} для того, чтобы
создать файловый путь (с одинаковой относительной частью) в другой корневой директории.
\begin{myverb}
(merge-pathnames
(enough-namestring #p"/www/html/foo/bar/baz.html" #p"/www/")
#p"/www-backups/") -> #p"/www-backups/html/foo/bar/baz.html"
\end{myverb}
\lstinline{MERGE-PATHNAMES} используется стандартными функциями для доступа к файлам, чтобы
дополнять незавершённые файловые пути. Например, пусть есть файловый путь, имеющий только
компоненты имя и тип.
\begin{myverb}
(make-pathname :name "foo" :type "txt") -> #p"foo.txt"
\end{myverb}
Если вы попытаетесь передать этот файловый путь как аргумент функции \lstinline{OPEN},
недостающие компоненты, как, например, директория, должны быть заполнены, чтобы Lisp смог
преобразовать файловый путь в действительное файловое имя. Common Lisp добудет эти
значения для недостающих компонент, объединяя данный файловый путь со значением переменной
\lstinline{*DEFAULT-PATHNAME-DEFAULTS*}. Начальное значение этой переменной определено
реализацией, но, как правило, это файловый путь, компонент директория которого
представляет ту директорию, в которой Lisp был запущен. Компоненты хост и устройство
заполнены подходящими значениями, если в этом есть необходимость. Если
\lstinline{MERGE-PATHNAMES} вызвана только с одним аргументом, то она объединит аргумент со
значением \lstinline{*DEFAULT-PATHNAME-DEFAULTS*}. Например, если
\lstinline{*DEFAULT-PATHNAME-DEFAULTS*}~--- \lstinline!#p"/home/peter/"!, то в результате:
\begin{myverb}
(merge-pathnames #p"foo.txt") -> #p"/home/peter/foo.txt"
\end{myverb}
\section{Два представления для имён директорий}
Существует один неприятный момент при работе с файловым путём, который представляет
директорию. Файловые объекты разделяют компоненты директория и имя файла, но Unix и
Windows рассматривают директории как ещё один тип файла. Поэтому, в этих системах, каждая
директория может иметь два различных преставления.
Одно из них, которое я назову \lstinline{представлением файла}, рассматривает директорию, как
любой другой файл и размещает последний элемент строки имени в компоненты имя и
тип. Другое представление~--- представление директории~--- помещает все элементы имени в
компонент директория, оставляя компоненты имя и тип равными \lstinline{NIL}. Если
\lstinline{/foo/bar/}~--- директория, тогда любой из следующих двух файловых путей представляет
ее.
\begin{myverb}
(make-pathname :directory '(:absolute "foo") :name "bar") ; file form
(make-pathname :directory '(:absolute "foo" "bar")) ; directory form
\end{myverb}
Когда вы создаёте файловые пути с помощью \lstinline{MAKE-PATHNAME}, вы можете получить любое
из двух представлений, но нужно быть осторожным, когда имеете дело со строками имён. Все
современные реализации Lisp создают представление файла, если только строка имени не
заканчивается разделителем пути. Но вы не можете полагаться на то, что строки имени,
предоставленные пользователем, будут в том либо ином представлении. Например, предположим,
что вы запросили у пользователя имя директории, в которой сохраните файл. Пользователь
ввёл \lstinline!"/home/peter"!. Если передать функции \lstinline{MAKE-PATHNAME} эту строку как
аргумент \lstinline{:defaults}:
\begin{myverb}
(make-pathname :name "foo" :type "txt" :defaults user-supplied-name)
\end{myverb}
то в конце концов вы сохраните файл как \lstinline{/home/foo.txt}, а не
\lstinline{/home/peter/foo.text}, как предполагалось, так как \lstinline{"peter"} из строки имени
будет помещён в компонент имя, когда \lstinline{user-supplied-name} будет преобразовано в
файловый путь. В~переносимой библиотеке файловых путей, которую я обсужу в следующей
главе, вы напишите функцию pathname-as-directory, которая преобразует файловый объект в
представление директории. С этой функцией вы сможете сохранять наверняка файл в
директории, указанной пользователем.
\begin{myverb}
(make-pathname
:name "foo" :type "txt" :defaults (pathname-as-directory user-supplied-name))
\end{myverb}
\section{Взаимодействие с файловой системой}
Хоть открытие файлов для чтения и записи~--- наиболее частый вид взаимодействия с файловой
системой, порой вам захочется проверить существует ли файл, прочитать содержимое
директории, удалить или переименовать файлы, создать директории или получить такую
информацию о файле, как: кто его владелец, когда он последний раз был изменён, его
длину. Тут-то общность абстракции файловых путей немного некстати. Стандарт языка не
определяет, как функции, взаимодействующие с файловой системой, отображаются на какую-то
конкретную файловую систему, поэтому у создателей конкретных реализаций есть некоторая
свобода действий.
Несмотря на это большинство функций для взаимодействия с файловой системой просты для
понимания. Я расскажу о стандартных функциях и укажу на те, с которыми могут возникнуть
проблемы с переносимостью кода между реализациями. В~следующей главе вы разработаете
переносимую библиотеку файловых путей для того, чтобы сгладить эти проблемы с
переносимостью.
Для того, чтобы проверить существует ли файл, соответствующий указателю файлового пути~---
будь-то файловый путь, строка имени или файловый поток,~--- можно использовать функцию
\lstinline{PROBE-FILE}. Если файл, соответствующий указателю, существует, \lstinline{PROBE-FILE}
вернёт <<настоящее>> имя файла~--- файловый путь с любыми преобразованиями уровня файловой
системой, как, например, следование по символическим ссылкам. В~ином случае, она вернёт
\lstinline{NIL}. Однако, не все реализации позволяют использовать её для того, чтобы проверить
существует ли директория. Также, Common Lisp не предоставляет переносимого способа
определить, чем является существующий файл~--- обычным файлом или директорией. В~следующей
главе вы сделаете функцию-обёртку для \lstinline{PROBE-FILE}~--- \lstinline{file-exists-p}, которая
может проверить существует ли файл и ответить: данное имя является именем файла или
директории.
Так же стандартная функция для чтения списка файлов~--- \lstinline{DIRECTORY}~--- работает
прекрасно в незамысловатых случаях, но из-за различий реализаций её использование
становится мудрёным делом. В~следующей главе вы определите функцию чтения содержимого
директорий, которая сгладит некоторые из различий.
\lstinline{DELETE-FILE} и \lstinline{RENAME-FILE} делают то, что следует из их
названий. \lstinline{DELETE-FILE} принимает указатель файлового пути и удаляет указанный файл,
возвращая истину в случае успеха. В~ином случае она сигнализирует
\lstinline{FILE-ERROR}\footnote{См. главу~\ref{ch:19} насчёт обработки ошибок.}\hspace{\footnotenegspace}.
\lstinline{RENAME-FILE} принимает два указателя файлового пути и изменяет имя файла, указанного
первым параметром, на имя, указанное вторым параметром.
Вы можете создать директории с помощью функции \lstinline{ENSURE-DIRECTORIES-EXIST}. Она
принимает указатель файлового пути и удостоверяется, что все элементы компонента
директория существуют и являются директориями, создавая их при необходимости. Так как она
возвращает файловый путь, который ей был передан, то удобно её вызывать на месте аргумента
другой функции.
\begin{myverb}
(with-open-file (out (ensure-directories-exist name) :direction :output)
...
)
\end{myverb}
Обратите внимание, что если вы передаёте \lstinline{ENSURE-DIRECTORIES-EXIST} имя директории,
то оно должно быть в представлении директории, или последняя директория не будет
создана. Обе функции \lstinline{FILE-WRITE-DATE} и \lstinline{FILE-AUTHOR} принимают указатель
файлового пути. \lstinline{FILE-WRITE-DATE} возвращает количество секунд, которое прошло с
полуночи 1-го января 1900 года, среднее время по Гринвичу (GMT), до времени последней
записи в файл. \lstinline{FILE-AUTHOR} возвращает~--- в Unix и Windows~--- владельца
файла\pclfootnote{Для приложений, которым нужен доступ к другим атрибутам файла в
определённой операционной системе или на файловой системе, некоторые библиотеки
предоставляют обвязки (bindings) для системных вызовов. Библиотека Osicat, размещённая
по адресу \url{http://common-lisp.net/project/osicat/}, предоставляет простой API,
созданный на основе Universal Foreign Function Interface (UFFI). Она должна работать с
большинством реализаций Common Lisp на POSIX-совместимых операционных системах.}.
Для того, чтобы получить размер файла, используйте функцию \lstinline{FILE-LENGTH}. По
историческим причинам \lstinline{FILE-LENGTH} принимает поток как аргумент, а не файловый
путь. В~теории это позволяет \lstinline{FILE-LENGTH} вернуть размер в терминах типа элементов
потока. Однако, так как на большинстве современных операционных системах единственная
доступная информация о размере файла~--- исключая чтение всего файла, чтобы узнать
размер~--- это размер в байтах, что возвращают большинство реализаций, даже если
\lstinline{FILE-LENGTH} передан символьный поток. Однако, стандарт не требует такого поведения,
поэтому ради предсказуемых результатов лучший способ получить размер файла~---
использовать бинарный поток\footnote{Количество байтов и символов в файле может
разниться, даже если не используется многобайтная кодировка. Потому что символьные
потоки также преобразуют специфичные для платформы переносы строк в единственный символ
\lstinline!#\Newline!. В~Windows, в которой используется \lstinline{CRLF} в качестве переноса
строки, количество символов, как правило, будет меньше чем количество байт. Если вам
действительно требуется знать количество символов в файле, то вы должны набраться
смелости и написать что-то похоже на:
\begin{myverb}
(with-open-file (in filename)
(loop while (read-char in nil) count t))
\end{myverb}
\noindent{}или, возможно, что-нибудь более эффективное, вроде этого:
\begin{myverb}
(with-open-file (in filename)
(let ((scratch (make-string 4096)))
(loop for read = (read-sequence scratch in)
while (plusp read) sum read)))
\end{myverb}
}\hspace{\footnotenegspace}.
\begin{myverb}
(with-open-file (in filename :element-type '(unsigned-byte 8))
(file-length in))
\end{myverb}
Похожая функция, которая также принимает открытый файловый поток в качестве аргумента~---
\lstinline{FILE-POSITION}. Когда ей передают только поток, она возвращает текущую позицию в
файле~--- количество элементов, прочитанных из потока или записанных в него. Когда её
вызывают с двумя аргументами, потоком и указателем позиции, она устанавливает текущей
позицией указанную. Указатель позиции должен быть ключевым словом \lstinline{:start},
\lstinline{:end} или неотрицательным целым числом. Два ключевых слова устанавливают позицию
потока в начало или конец. Если же передать функции целое число, то позиция переместится в
указанную позицию файла. В~случае бинарного потока позиция~--- это просто смещение в байтах
от начала файла. Однако символьные потоки немного сложнее из-за проблем с
кодировками. Лучшее что вы можете сделать если нужно переместиться на другую позицию в
текстовом файле~--- всегда передавать \lstinline{FILE-POSITION} в качестве второго аргумента
только то значение, которое вернула функция \lstinline{FILE-POSITION}, вызванная с тем же
потоком в качестве единственного аргумента.
\section{Другие операции ввода/вывода}
Вдобавок к файловым потокам Common Lisp поддерживает другие типы потоков, которые также
можно использовать с разнообразными функциями ввода/вывода для чтения, записи и
печати. Например, можно считывать данные из строки или записывать их в строку, используя
\lstinline{STRING-STREAM}, которые вы можете создать функциями \lstinline{MAKE-STRING-INPUT-STREAM}
и \lstinline{MAKE-STRING-OUTPUT-STREAM}.
\lstinline{MAKE-STRING-INPUT-STREAM} принимает строку и необязательные начальный и конечный
индексы, указывающие часть строки, которую следует связать с потоком, и возвращает
символьный поток, который можно передать как аргумент любой функции символьного ввода,
как, например, \lstinline{READ-CHAR}, \lstinline{READ-LINE} или \lstinline{READ}. Например, если у вас
есть строка, содержащая число с плавающей точкой с синтаксисом Common Lisp, вы можете
преобразовать её в число с плавающей точкой:
\begin{myverb}
(let ((s (make-string-input-stream "1.23")))
(unwind-protect (read s)
(close s)))
\end{myverb}
\lstinline{MAKE-STRING-OUTPUT-STREAM} создаёт поток, который можно использовать с
\lstinline{FORMAT}, \lstinline{PRINT}, \lstinline{WRITE-CHAR}, \lstinline{WRITE-LINE} и т.д. Она не принимает
аргументов. Что бы вы не записывали, строковый поток вывода будет накапливать это в
строке, которую потом можно получить с помощью функции
\lstinline{GET-OUTPUT-STREAM-STRING}. Каждый раз при вызове \lstinline{GET-OUTPUT-STREAM-STRING}
внутренняя строка потока очищается, поэтому существующий строковый поток вывода можно
снова использовать.
Однако, использовать эти функции напрямую вы будете редко, так как макросы
\lstinline{WITH-INPUT-FROM-STRING} и \lstinline{WITH-OUTPUT-TO-STRING} предоставляют более удобный
интерфейс. \lstinline{WITH-INPUT-FROM-STRING} похожа на \lstinline{WITH-OPEN-FILE}~--- она создаёт
строковый поток ввода на основе переданной строки и выполняет код в своём теле с потоком,
который присвоен переменной, вами предоставленной. Например, вместо формы \lstinline{LET} с
явным использованием \lstinline{UNWIND-PROTECT}, вероятно, лучше написать:
\begin{myverb}
(with-input-from-string (s "1.23")
(read s))
\end{myverb}
Макрос \lstinline{WITH-OUTPUT-TO-STRING} также связывает вновь созданный строковый поток вывода
с переменной, вами названной, и затем выполняет код в своём теле. После того, как код был
выполнен, \lstinline{WITH-OUTPUT-TO-STRING} вернёт значение, которое было бы возвращено
\lstinline{GET-OUTPUT-STREAM-STRING}.
\begin{myverb}
CL-USER> (with-output-to-string (out)
(format out "hello, world ")
(format out "~s" (list 1 2 3)))
"hello, world (1 2 3)"
\end{myverb}
Другие типы потоков, определённые в стандарте языка, предоставляют различные способы
<<соединения>> потоков, то есть позволяют подключать потоки друг к другу почти в любой
конфигурации. \lstinline{BROADCAST-STREAM}~--- поток вывода, который посылает записанные данные
множеству потоков вывода, переданных как аргументы функции-конструктору
\lstinline{MAKE-BROADCAST-STREAM}\footnote{\lstinline{MAKE-BROADCAST-STREAM} может создать <<чёрную
дыру>> для данных, если её вызвать без аргументов.}\hspace{\footnotenegspace}. В~противоположность этому,
\lstinline{CONCATENATED-STREAM}~--- поток ввода, который принимает ввод от множества потоков
ввода, перемещаясь от потока к потоку, когда очередной поток достигает конца. Потоки
\lstinline{CONCATENATED-STREAM} создаются функцией \lstinline{MAKE-CONCATENATED-STREAM}, которая
принимает любое количество потоков ввода в качестве аргументов.
Ещё существуют два вида двунаправленных потоков, которые могут подключать потоки друг к
другу~--- \lstinline{TWO-WAY-STREAM} и \lstinline{ECHO-STREAM}. Их функции-конструкторы,
\lstinline{MAKE-TWO-WAY-STREAM} и \lstinline{MAKE-ECHO-STREAM}, обе принимают два аргумента, поток
ввода и поток вывода, и возвращают поток соответствующего типа, который можно использовать
как с потоками ввода, так и с потоками вывода.
В~случае \lstinline{TWO-WAY-STREAM} потока, каждое чтение вернёт данные из потока ввода, и
каждая запись пошлёт данные в поток вывода. \lstinline{ECHO-STREAM} по существу работает точно
так же кроме того, что все данные прочитанные из потока ввода также направляются в поток
вывода. То есть поток вывода потока \lstinline{ECHO-STREAM} будет содержать стенограмму
<<беседы>> двух потоков.
Используя эти пять типов потоков, можно построить почти любую топологию сети потоков,
какую бы вы ни пожелали.
В~заключение, хотя стандарт Common Lisp не оговаривает какой-либо сетевой API, большинство
реализаций поддерживают программирование сокетов и, как правило, реализуют сокеты как ещё
один тип потока. Следовательно, можно использовать с ними все обычные функции
вводы/вывода\pclfootnote{Наибольшим пробелом в стандартных средствах ввода/вывода Common
Lisp является отсутствие способа определения пользователем новых типов потоков. Однако,
для определения пользователем потоков существует два стандарта де-факто. Во время
разработки стандарта Common Lisp, Дэвид Грэй (David Gray) из Texas Instruments предложил
набросок API, который позволяет пользователям определять новые типы потоков. К
сожалению, уже не оставалось времени для разбора всех трудностей, поднятых этим
наброском, чтобы включить его в стандарт. Но все же много реализаций поддерживают в
некоторой форме так называемые потоки Грэя. Они основывают свой API на наброске
Грэя. Другой, более новый API~--- Simple Streams (простые потоки)~--- были разработаны
компанией Franz и включены в Allegro Common Lisp. Они были разработаны, чтобы улучшить
производительность определяемых пользователем потоков, схожих с потоками Грэя. Простые
потоки позже переняли некоторые открытые реализации Common Lisp.}.
Теперь вы готовы двигаться дальше для написания библиотеки, которая позволит сгладить
некоторые различия поведения основных функций для работы с файловыми путями в различных
реализациях Common Lisp.
%%% Local Variables:
%%% mode: latex
%%% TeX-master: "pcl-ru"
%%% TeX-open-quote: "<<"
%%% TeX-close-quote: ">>"
%%% End: