forked from TurtlePU/Coq-2025
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathLecture1.v
3029 lines (2400 loc) · 193 KB
/
Lecture1.v
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
(** * Предисловие *)
(* ################################################################# *)
(** * Добро пожаловать! *)
(** Это вводный материал к курсу по Coq в Центральном Университете, основанный
на серии электронных учебников о различных аспектах _Оснований ПО_,
математических оснований создания надёжного программного обеспечения
({http://softwarefoundations.cis.upenn.edu}). В рамках данного курса мы
затронем базовые понятия логики, доказательства теорем с помощью компьютера,
использование средства для интерактивных доказательств Coq, функциональное
программирование, операционную семантику и техники изучения свойств
программ. Знание логики и/или теории языков программирования НЕ
предполагается, однако наличие общей математической культуры не повредит.
Отличительная особенность нашего курса (как и исходной серии книг) состоит в
том, что он на 100% формализован и проверяем машиной: все материалы курса
это буквально скрипты для Coq. Предполагается, что читатель изучает
материал одновременно с запуском интерактивной сессии в Coq. Все тонкости
материала полностью формализованы в Coq и абсолютное большинство упражнений
созданы для выполнения с помощью Coq.
В рамках данного курса всего будет 8 файлов, по числу занятий. В каждом из
них есть как разбираемые на занятии упражнения, так и задачи для
самостоятельного решения, на основе которых будет выставлена оценка. *)
(* ################################################################# *)
(** * Обзор *)
(** Создавать надёжное ПО тяжело -- ОЧЕНЬ тяжело. Масштаб и сложность
современных систем, масса привлечённых к разработке людей и набор
предъявляемых к ним требований делают затруднительным хотя бы более-менее
правильную реализацию продуктов, не говоря уже о 100%-ной корректности.
Вдобавок растущая вовлечённость обработки информации в каждый аспект
общественной жизни многократно увеличивает цену ошибок, багов и проблем с
безопасностью.
По мере взросления отрасли, специалисты компьютерных наук и разработчики
придумали множество техник для улучшения надёжности программных систем,
начиная от методик управления командами разработчиков (эджайл, экстремальное
программирование и т.д.) и парадигм в разработке библиотек (например,
model-view-controller, publish-subscribe, и т.д.) и языков программирования
(ООП, функциональное программирование, ...) до математических техник для
формального задания (спецификации) свойств программ, а также инструментов,
помогающих в проверке этих свойств. В рамках данного курса мы
сосредоточимся на последней категории.
В первой половине курса мы затронем следующие темы:
(1) базовые инструменты из _логики_ для формулировки и доказательства
математически точных утверждений о программах;
(2) использование _ассистентов в формальных доказательствх_ (_proof
assistants_) для конструирования строгих логических аргументов;
(3) _функциональное программирование_, и как методику программирования,
которая упрощает рассуждения о программах, и как мост между
программированием и логикой. *)
(* ================================================================= *)
(** ** Логика *)
(** Логика -- это область исследований, предметом которых являются
_доказательства_ -- неопровержимые аргументы в пользу истинности конкретных
утверждений. О центральной роли логики в компьютерных науках написаны тонны
книг; Манна и Уолдингер назвали это "математическим анализом компьютерных
наук", в то время как статья Халперна и др. _Эффективность логики в
компьютерных науках_ содержит множество способов, с помощью которых логика
предлагает важнейшие инструменты и идеи для анализа программ и алгоритмов.
Действительно, они отмечают, что "На самом деле логика оказалась значительно
более эффективной в компьютерных науках, чем в математике. Это весьма
примечательно, особенно с учетом того, что за последние сто лет математика
во многом придала импульс развитию логики."
В частности, фундаментальные инструменты _индуктивных доказательств_ широко
распространены во всех областях компьютерных наук. Вы наверняка видели их
раньше, возможно, в курсе дискретной математики или анализа алгоритмов, но в
этом курсе мы рассмотрим их более подробно чем вы, вероятно, делали это до
сих пор. *)
(* ================================================================= *)
(** ** Инструменты интерактивного доказательства теорем *)
(** Обмен идеями между логикой и информатикой не был однонаправленным: CS также
привнесли много нового в логику. В частности, было разработано множество
программных средств, помогающих создавать доказательства логических
утверждений. Эти инструменты делятся на две большие категории:
- _Aвтоматизированные средства доказательства теорем_ обеспечивают
"кнопочное" управление: вы даете им предложение, и они возвращают либо
_ИСТИНА_, либо _ЛОЖЬ_ (или, иногда, _НЕ ЗНАЮ: НЕ ХВАТИЛО ВРЕМЕНИ_).
Хотя их возможности к рассуждениям по-прежнему ограничены, за последние
десятилетия они значительно усовершенствовались и в настоящее время
используются во множестве ситуаций. Примеры таких инструментов
включают в себя SAT-решатели, SMT-солверы и средства проверки моделей.
- _Инструменты интерактивного доказательства теорем_ (proof assistants)
-- это гибридные инструменты, автоматизирующие более рутинные аспекты
создания доказательств, но которые в то же время направляются человеком
в более сложных аспектах. Широко используемые ассистенты включают в
себя Isabelle, Agda, Twelf, ACL2, PVS, F*, HOL4, Lean, Coq и многие
другие.
Этот курс основан на Coq, proof assistant-е, который разрабатывается с 1983
года и который в последние годы привлёк большое сообщество пользователей как
в научных кругах, так и в индустрии. Coq предоставляет богатую среду для
интерактивной разработки формальных рассуждений, проверяемых машиной. Ядро
системы Coq -- это простая программа проверки правильности доказательства,
которая гарантирует, что все шаги в логическом рассуждении корректны. В
дополнение к этому ядру среда Coq предоставляет высокоуровневые средства для
разработки доказательств, включая большую библиотеку общих определений и
лемм, мощные _тактики_ построения сложных доказательств полуавтоматически, и
специальный язык программирования для определения новых тактик для
автоматизации доказательств в специфичных ситуациях.
Coq стал ключевым стимулирующим фактором для огромного разнообразия работ по
компьютерным наукам и математике:
- В качестве _платформы для моделирования языков программирования_, он стал
стандартным инструментом для исследователей, которым необходимо описывать
объёмные определения языков и исследовать их свойства. Например, он
использовался для проверки безопасности платформы Java Card, для
формальных спецификаций набора инструкций x86 и LLVM, языков
программирования (того же C).
- В качестве _среды для разработки формально сертифицированного программного
и аппаратного обеспечения_, Coq использовался, например, для создания
CompCert, полностью верифицированного оптимизирующего компилятора для C, и
CertiKOS, полностью верифицированного гипервизора, для проверки
корректности тонких мест в алгоритмах, использующих вычисления c плавающей
запятой, и является основой для CertiCrypt, FCF и SS Prove, фреймворков
для проверки безопасности криптографических алгоритмов. Он также
используется для создания верифицированных реализаций архитектуры
процессора RISC-V с открытым исходным кодом.
- В качестве _реалистичной среды для функционального программирования с
зависимыми типами_, он вдохновил многочисленные инновации. Например,
Хоарова теория типов позволяет рассуждать о "предварительных условиях" и
"постусловиях" (расширение _логики Хоара_, которую мы увидим позже в этом
курсе) в Coq.
- В качестве _proof assistant-a для логики высшего порядка_, Coq был
использован для проверки ряда важных результатов в математике. Например,
его способность производить сложные вычисления по ходу доказательства
позволила разработать первое формально верифицированное доказательство
Теоремы о четырёх красках. Принятие не-верифицированной версии этого
доказательства ранее было спорным вопросом среди математиков, потому что
она требовала компьютерного перебора большого количества вариантов. В свою
очередь, в формализации на Coq проверяется всё, в том числе и правильность
произведённых вычислений. Совсем недавно были предприняты еще более
масштабные усилия, приведшие к формализации на Coq теоремы Фейта-Томпсона,
первого важного шага в классификации конечных простых групп.
Кстати, если вам интересно происхождение названия, вот что написано на
официальном веб-сайте Coq в Inria (французском национальном исследовательском
центре, где в основном разрабатывался Coq): "У некоторых французских
компьютерщиков есть традиция называть свои программы видами животных: Caml,
Elan, Foc или Phox являются примерами этого негласного соглашения.
По-французски "coq" означает "петух", а ещё это звучит как аббревиатура
названия Исчисления Конструкций (Calculus of Constructions, CoC), на котором
он основан." Петух также является национальным символом Франции, а C-o-q --
это первые три буквы фамилии Тьерри Коканда, одного из первых разработчиков
Coq.
Правда, после массового возмущения и мольбы англофонов, язык был переименован
в Rocq. Но в рамках этого курса мы будем пользоваться старым названием. *)
(* ================================================================= *)
(** ** Функциональное программирование *)
(** Термин _функциональное программирование_ применяется как к набору идиом
программирования, которые могут быть использованы практически в любом
языке программирования, так и к семейству языков программирования,
разработанных с учетом этих идиом, включающему Haskell, OCaml, Standard ML,
F##, Scala, Scheme, Racket, Common Lisp, Clojure, Erlang, F* и Coq.
Функциональное программирование разрабатывалось на протяжении многих
десятилетий -- действительно, его корни уходят в лямбда-исчисление Черча,
которое было изобретено в 1930-х годах, задолго до появления первых
электронных компьютеров! Но с начала 90-х годов он вызвал всплеск интереса
среди промышленных инженеров и разработчиков языков программирования, сыграв
ключевую роль в высокоприбыльных системах в таких компаниях, как Jane Street
Capital, Microsoft, Facebook, Twitter и Ericsson.
Самый основной принцип функционального программирования заключается в том,
что вычисления, насколько это возможно, должны быть _чистыми_ в том смысле,
что единственным эффектом исполнения должно быть получение результата в виде
данных: в нём не должно быть _сайд-эффектов_, таких как ввод-вывод,
присвоения значений изменяемым переменным, перенаправления указателей и т.д.
Например, в то время как _императивная_ функция сортировки может взять
список чисел и переставить его указатели, чтобы упорядочить список, чистая
функция сортировки возьмет исходный список и вернет _новый_ список,
содержащий те же числа в отсортированном порядке.
Существенным преимуществом этого стиля программирования является то, что он
облегчает понимание программы и рассуждение о её свойствах. Если каждая
операция со структурой данных приводит к созданию новой структуры данных,
оставляя старую структуру неизменной, то нет необходимости беспокоиться о
том, как эта структура используется совместно и может ли изменение в одной
части программы нарушить инвариант, на который опирается другая часть
программы. Эти соображения особенно важны в конкурентных системах, где
каждая часть изменяемого состояния, доступная из нескольких потоков,
является потенциальным источником опасных ошибок. Действительно, большая
часть современного интереса к применению функционального программирования в
промышленности обусловлено его более простым поведением при наличии
параллелизма.
Еще одна причина нынешнего ажиотажа по поводу функционального
программирования связана с первой: функциональные программы часто гораздо
проще распараллеливать и физически распространять, чем их императивные
аналоги. Если запуск вычислений не дает никакого эффекта кроме получения
конкретного значения, неважно, _где_ эти вычисления запускать. Аналогично,
если к структуре данных никогда не применяется разрушающее изменение, её
можно свободно копировать между ядрами или по сети. Действительно, идиома
"Map-Reduce", которая лежит в основе обработчиков массово распределённых
запросов вроде Hadoop и используется в Google для индексации всего
Интернета, является классическим примером функционального программирования.
Для целей данного курса функциональное программирование имеет еще одну
важную черту: оно служит связующим звеном между логикой и компьютерными
науками. Действительно, Coq сам по себе можно рассматривать как сочетание
небольшого, но чрезвычайно выразительного функционального языка
программирования плюс набор инструментов для формулирования и доказательства
логических утверждений. Более того, при более внимательном рассмотрении мы
обнаружим, что эти две стороны Coq на самом деле являются аспектами одного и
того же базового механизма -- другими словами, _доказательства -- это
программы_. *)
(* ================================================================= *)
(** ** Дополнительная литература *)
(** Этот курс задуман как самостоятельный, но слушатели, которые хотят
глубже разобраться в отдельных темах, найдут некоторые рекомендации для
дальнейшего чтения в главе [Postscript]. Библиографию всех цитируемых работ
можно найти в файле [Bib].*)
(* ################################################################# *)
(** * Практические аспекты *)
(* ================================================================= *)
(** ** Системные требования *)
(** Coq работает на Windows, Linux, и macOS. Материалы этого курса были
протестированы на версии Coq 8.19.2.
Вам потребуется:
- Свежая версия Coq, доступная на домашней странице Coq
({https://coq.inria.fr/download}). "Coq Platform", как правило, установить
проще всего, особенно на Windows.
Если Вы используете комбинацию VSCode + Docker, описанную ниже, Вам не
потребуется устанавливать Coq отдельно.
- IDE для взаимодействия с Coq. Есть несколько вариантов:
- _VsCoq_ это расширение для VS Code, предлагающее простой интерфейс в
знакомой IDE. Это "рекомендованный по умолчанию" вариант. Если Вы
установили Coq с официального сайта, то Вы можете использовать только
версию "VsCoq Legacy", которая в любом случае более стабильна. Если Вы
используете менеджер пакетов opam, Вы также можете попробовать
"VsCoq 2". Это расширение более экспериментальное, но и
функциональности в нём намного больше.
VsCoq может быть использован как обычная IDE, либо может быть совмещён
с Docker (см. ниже) для более легковесной установки.
- _Proof General_ это IDE, основанная на Emacs. Обычно, её предпочитают
пользователи, которым уже привычен Emacs. Он требует отдельной
установки и настройки (загуглите "Proof General", но, как правило,
всё, что Вам нужно сделать, это [M-x package-list-packages],
затем выбрать пакет [proof-general] из списка и нажать [i] для
установки, затем [x] для исполнения).
Желающим поэкспериментировать с Coq внутри Emacs могут понравиться
такие расширения как [company-coq] и [control-lock].
- _CoqIDE_ это упрощённая самостоятельная IDE. Она распространяется
вместе с Coq, так что должна быть доступна сразу, как Вы установите
Coq. Также её можно собрать из исходников, но на некоторых платформах
это может потребовать установки дополнительных пакетов для
GUI-фреймворков и пр.
Пользователям, которым нравится CoqIDE, стоит попробовать запустить
его с выключенными режимами "asynchronous" и "error resilience": [[
coqide -async-proofs off \
-async-proofs-command-error-resilience off Foo.v &
]]
- Ваш покорный слуга предпочитает использовать _NeoVim_ с плагином
_Coqtail_. *)
(* ----------------------------------------------------------------- *)
(** *** Coq с VSCode и Docker *)
(** VS Code может взаимодействовать с платформой виртуализации Docker для
компиляции скриптов на Coq без необходимости какой-либо отдельной
установки Coq. Чтобы все настроить, выполните следующие действия:
- Установите Docker с помощью [https://www.docker.com/get-started/] или
убедитесь, что у Вашей текущей установки актуальная версия.
- Убедитесь, что Docker запущен.
- Установите VS Code из [https://code.visualstudio.com] и запустите его.
- Установите расширение Remote Containers для VS Code из [
https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers
]
- Склонируйте к себе репозиторий курса (в VS Code это можно сделать, выбрав
опцию "Clone Git Repository..."). Помимо файла [.v] для каждой главы,
этот репозиторий будет содержать подпапку [.devcontainer] с инструкциями
для VSCode о том, где найти соответствующий образ Docker, и файл
[_CoqProject], наличие которого является триггером для запуска VsCoq.
- Откройте репозиторий в VS Code. IDE должна спросить вас, хотите ли вы
запустить проект в соответствующем контейнере Docker. (Если вас не
спросят, вы можете открыть командную палитру, нажав клавишу F1, и
выполнить команду “Dev Containers: Reopen in Container”.)
- Убедитесь, что VSCoq работает, дважды щелкнув файл [Lecture1.v] в списке
слева (вы должны увидеть мигающий курсор в открывшемся окне; если нет, вы
можете щелкнуть на этом окне, чтобы выбрать его), и несколько раз нажмите
[alt+стрелка вниз] (в Mac OS -- [control+option+стрелка вниз]). Вы должны
увидеть, как курсор перемещается по файлу, а область над курсором
становится подсвеченной.
- Чтобы узнать, какие ещё сочетания клавиш доступны, нажмите клавишу F1 и
затем введите [Coq:] или посетите веб-страницы VSCoq:
[https://github.com/coq-community/vscoq/tree/vscoq1]. *)
(* ================================================================= *)
(** ** Упражнения *)
(** Каждая глава содержит множество упражнений. Каждое из них отмечено
"звездным рейтингом", который можно интерпретировать следующим образом:
- Одна звездочка: простые упражнения, которые подчеркивают некоторые
моменты в тексте и которые для большинства слушателей должны занять
всего минуту или две. Возьмите за привычку выполнять их по мере чтения.
- Две звезды: простые упражнения (пять-десять минут).
- Три звезды: упражнения, требующие некоторого размышления (от десяти
минут до получаса).
- Четыре и пять звезд: более сложные упражнения (дольше получаса).
Имейте в виду, что автогрейдер присваивает дополнительные баллы за более
сложные упражнения:
1 звездочка = 1 балл
2 звездочки = 2 балла
3 звездочки = 3 балла
4 звездочки = 6 баллов
5 звездочек = 10 баллов
Некоторые упражнения помечены как "продвинутые", а некоторые - как
"необязательные". Выполнение только обязательных, не-продвинутых упражнений
должно обеспечивать хорошее усвоение базового материала. Необязательные
упражнения дают немного дополнительной практики в работе с ключевыми
понятиями и знакомят с второстепенными темами, которые могут представлять
интерес для некоторых слушателей. Продвинутые упражнения предназначены для
слушателей, которым нужен дополнительный челлендж и более глубокое
погружение.
_Пожалуйста, не размещайте решения упражнений в публичном доступе_.
Оригинальная серия книг, Software Foundations, широко используется как для
самостоятельного изучения, так и для университетских курсов. Наличие
легкодоступных решений делает его гораздо менее полезным для курсов, которые
обычно содержат домашние задания с оценкой. Мы особенно просим слушателей
не размещать решения к упражнениям в тех местах, где их могут найти
поисковые системы. *)
(* ================================================================= *)
(** ** Загрузка материалов *)
(** Архив с расширением .tar, содержащий все исходники к "публичному релизу"
оригинальной книги (в виде коллекции скриптов на Coq и файлов HTML) доступен
по ссылке {https://softwarefoundations.cis.upenn.edu}.
Наши слегка отредактированные и переведённые на русский язык файлы доступны
в приватном репозитории курса. По ходу прохождения курса, в нём будут
появляться новые материалы каждую неделю. *)
(* ################################################################# *)
(** * Ресурсы *)
(* ================================================================= *)
(** ** Лекционные видео *)
(** Лекции по двум летним интенсивам, основанным на оригинальной книге _Logical
Foundations_ (входит в программу регулярной летней школы DeepSpec) можно
найти по следующим ссылкам: {https://deepspec.org/event/dsss17} и
{https://deepspec.org/event/dsss18/}. Качество видео в лекциях за 2017
поначалу не очень хорошее, но в дальнейшем становится гораздо лучше.
Также во внутренней системе ЦУ, как обычно, будут доступны записи всех
занятий. *)
(** * Основы: Функциональное программмирование на Coq *)
(* ################################################################# *)
(** * Введение *)
(** Функциональный стиль программирования основан на простой, повседневной
математической интуиции: если процедура или метод не имеют побочных
эффектов, то (игнорируя эффективность) все, что нам нужно знать для работы с
ним -- это то, как он сопоставляет входные данные с выходными, то есть мы
можем думать о нём просто как о конкретном методе вычисления математической
функции. Это одно из значений слова "функциональный" в словосочетании
"функциональное программирование". Прямая связь между программами и
простыми математическими объектами поддерживает как формальную корректность
доказательств, так и обоснованные неформальные рассуждения о поведении
программы.
Другой смысл, в котором функциональное программирование является
"функциональным", заключается в том, что оно подчеркивает использование
функций в качестве значений _первого класса_ -- т.е. значений, которые могут
передаваться в качестве аргументов другим функциям, возвращаться в качестве
результатов, включаться в структуры данных и т.д. Понимание того, что
функции можно рассматривать как данные, порождает множество полезных и
мощных идиом программирования.
Другие общие черты функциональных языков включают в себя _алгебраические
типы данных_ и _сопоставление c образцом_, которые упрощают создание сложных
структур данных и манипулирование ими, а также _полиморфные системы типов_,
поддерживающие абстракцию и повторное использование кода. В Coq доступны
все эти функции.
В первой четверти этого занятия представлены наиболее важные элементы
собственного функционального языка программирования Coq, _Gallina_. Во
второй части представлены некоторые базовые _тактики_, которые можно
использовать для доказательства свойств программ, написанных на Gallina. *)
(* ################################################################# *)
(** * Сдача домашних заданий *)
(** Мы используем скрипты на Coq для автопроверки Ваших домашних заданий. Чтобы
эти скрипты работали корректно (и чтобы вы получили полную оценку за свою
работу!), пожалуйста, внимательно следуйте этим правилам:
- Не меняйте названия упражнений. В противном случае скрипты для
выставления оценок не смогут найти Ваше решение.
- Не удаляйте упражнения. Если вы пропустите какое-либо упражнение
(например, поскольку оно помечено как "необязательное" или потому что вы
не можете его решить), можно оставить частичное доказательство в вашем
файле [.v]; в этом случае, пожалуйста, убедитесь, что оно заканчивается
ключевым словом [Admitted] (а не, например, [Abort]).
- В своих решениях можно использовать дополнительные определения
(вспомогательных функций, полезных лемм и т.д.). Вы можете поместить их
перед теоремой, которую вас просят доказать.
- Если вы вводите вспомогательную лемму, которую вы в конечном итоге не
можете доказать, заканчивайте её словом [Admitted], затем не забудьте
также закончить основную теорему, в которой вы ее используете, словом
[Admitted], а не [Qed]. Это поможет вам получить частичный балл в том
случае, если вы используете эту основную теорему для решения
последующего упражнения.
Вы также заметите, что каждый файл (например, [Lecture1.v]) сопровождается
_тестирующим скриптом_ ([Lecture1Test.v]), который автоматически
подсчитывает баллы за готовые домашние задания в этой главе. Эти скрипты в
основном предназначены для автопроверки, но вы также можете воспользоваться
ими, чтобы еще раз проверить, правильно ли отформатирован ваш файл, прежде
чем сдать его. Ввполните в терминале команду "[make Lecture1Test.vo]" или
выполните следующие действия:
coqc -Q . Lectures Lecture1.v
coqc -Q . Lectures Lecture1Test.v
Более подробную информацию о том, как интерпретировать выходные данные
тестирующих скриптов, смотрите далее.
Сам тестирующий скрипт ([Lecture1Test.v]) сдавать не нужно!
Для сдачи домашних заданий сделайте следующее:
- Создайте приватную копию репозитория курса.
- Выдайте лектору доступ к Вашей приватной копии.
- Решайте домашнее задание прямо в файлах [Lecture1.v], [Lecture2.v],...
- Для сдачи домашнего задания на проверку создавайте коммиты с Вашими
изменениями и отправляйте их в Вашу приватную копию.
- В репозитории настроен GitHub Workflow, запускающий автопроверку при
каждом коммите в основную ветку. Это поможет и Вам, и проверяющему сразу
увидеть результаты проверки. *)
(* ################################################################# *)
(** * Типы данных и функции *)
(* ================================================================= *)
(** ** Типы-перечисления *)
(** Одной из примечательных особенностей Coq является то, что встроенная в него
функциональность _чрезвычайно_ ограничена. Например, вместо предоставления
обычного набора атомарных типов данных (логические значения, целые числа,
строки и т.д.), Coq предлагает мощный механизм для определения новых типов
данных с нуля, предоставляя возможность задать все эти знакомые типы на
уровне библиотек.
Естественно, дистрибутив Coq поставляется с обширной стандартной
библиотекой, предоставляющей определения логических значений, чисел и многих
распространенных структур данных, таких как списки и хэш-таблицы. Но в этих
библиотечных определениях нет ничего волшебного или примитивного. Чтобы
проиллюстрировать это, в этом курсе мы подробно рассмотрим (почти) все
определения, которые нам нужны, вместо того чтобы брать их из стандартной
библиотеки. *)
(* ================================================================= *)
(** ** Дни недели *)
(** Чтобы увидеть, как работает этот механизм определений, давайте начнем с
очень простого примера. Следующее объявление сообщает Coq-у, что мы
определяем набор значений данных -- _тип_. *)
Inductive day : Type :=
| monday
| tuesday
| wednesday
| thursday
| friday
| saturday
| sunday.
(** Новый тип называется [day], а его членами являются [monday], [tuesday] и
т.д.
Определив [day], мы можем написать функции, которые работают с днями
недели. *)
Definition next_working_day (d:day) : day :=
match d with
| monday => tuesday
| tuesday => wednesday
| wednesday => thursday
| thursday => friday
| friday => monday
| saturday => monday
| sunday => monday
end.
(** Обратите внимание, что типы аргументов и возвращаемых значений этой функции
объявлены здесь явно. Как и в большинстве функциональных языков
программирования, Coq часто может сам определять эти типы, если они не
заданы явно -- т.е. он может выполнять _вывод типов_ -- но, как правило, мы
выписываем их явно, чтобы облегчить чтение. *)
(** Определив функцию, мы можем проверить, работает ли она на некоторых
примерах. На самом деле в Coq есть три разных способа выполнения примеров.
Во-первых, мы можем использовать команду [Compute] для вычисления составного
выражения, включающего [next_working_day]. *)
Compute (next_working_day friday).
(* ==> monday : day *)
Compute (next_working_day (next_working_day saturday)).
(* ==> tuesday : day *)
(** (Мы показываем ответы Coq в комментариях; если с Вами сейчас есть Ваш
компьютер, это отличный момент, чтобы запустить интерпретатор Coq в вашей
любимой среде IDE (инструкции по установке смотрите выше) и попробовать его
самостоятельно. Загрузите этот файл, [Lecture1.v], из репозитория, найдите
приведенный выше пример, отправьте его в Coq и понаблюдайте за
результатом.) *)
(** Во-вторых, мы можем записать _ожидаемый_ результат в виде примера Coq: *)
Example test_next_working_day:
(next_working_day (next_working_day saturday)) = tuesday.
(** Это объявление делает две вещи: оно создает утверждение (что вторым рабочим
днем после [субботы] является [вторник]) и присваивает утверждению имя,
которое можно использовать для ссылки на него позже. Создав утверждение, мы
также можем попросить Coq проверить его следующим образом: *)
Proof. simpl. reflexivity. Qed.
(** Детали сейчас не важны, но, по сути, этот небольшой скрипт можно прочитать
как "Утверждение, которое мы только что сделали, можно доказать, заметив,
что обе стороны равенства приводят к одному и тому же результату". *)
(** В-третьих, мы можем попросить Coq извлечь из нашего [Определения] программу
на более традиционном языке программирования (OCaml, Scheme или Haskell) с
помощью высокопроизводительного компилятора. Это средство очень полезно,
поскольку оно прокладывает нам путь от корректных-по-построению алгоритмов,
написанных на Gallina, к эффективному машинному коду.
(Конечно, мы доверяем корректности компилятора OCaml/Haskell/Scheme и
средствам извлечения Coq самим по себе, но это все равно большой шаг вперед
по сравнению с тем, как сегодня разрабатывается большинство программ!)
Действительно, это одно из основных применений, для которых был разработан
Coq. Мы вернемся к этой теме в следующих главах. *)
(** Инструкция [Require Export] в следующей строке указывает Coq использовать
модуль [String] из стандартной библиотеки. Мы будем использовать строки
для различных целей в последующих занятиях, но нам нужно [Потребовать] это
здесь, чтобы скрипты автопроверки могли использовать строки для своих
целей. *)
From Coq Require Export String.
(* ================================================================= *)
(** ** Логические значения *)
(** Аналогично дням недели, определённым выше, мы можем определить стандартный
тип логических значений [bool] с элементами [true] и [false]. *)
Inductive bool : Type :=
| true
| false.
(** Функции над логическими значениями могут быть определены таким же образом,
как выше: *)
Definition negb (b:bool) : bool :=
match b with
| true => false
| false => true
end.
Definition andb (b1:bool) (b2:bool) : bool :=
match b1 with
| true => b2
| false => false
end.
Definition orb (b1:bool) (b2:bool) : bool :=
match b1 with
| true => true
| false => b2
end.
(** (Хотя здесь мы используем наши собственные логические значения ради создания
всего с нуля, Coq, конечно же, по умолчанию предоставляет реализацию
логических значений, а также множество полезных функций и лемм. Везде, где
это было возможно, мы давали нашим собственным определениям и теоремам
названия, соответствующие тем, что есть в стандартной библиотеке.) *)
(** Последние два из них иллюстрируют синтаксис Coq для определения функций с
несколькими аргументами. Соответствующий синтаксис _применения_ функций с
несколькими аргументами проиллюстрирован следующими "юнит-тестами", которые
представляют собой полную спецификацию -- таблицу истинности -- для функции
[orb].: *)
Example test_orb1: (orb true false) = true.
Proof. simpl. reflexivity. Qed.
Example test_orb2: (orb false false) = false.
Proof. simpl. reflexivity. Qed.
Example test_orb3: (orb false true) = true.
Proof. simpl. reflexivity. Qed.
Example test_orb4: (orb true true) = true.
Proof. simpl. reflexivity. Qed.
(** Мы также можем ввести немного знакомого инфиксного синтаксиса для логических
операций, которые мы только что определили. Команда [Notation] определяет
новую символьную запись для существующих определений. *)
Notation "x && y" := (andb x y).
Notation "x || y" := (orb x y).
Example test_orb5: false || false || true = true.
Proof. simpl. reflexivity. Qed.
(** _Примечание о нотации_: В файлах [.v] мы используем квадратные скобки
для выделения фрагментов кода Coq в комментариях; это соглашение, также
используемое инструментом документации [coqdoc], визуально отделяет их от
окружающего текста. В HTML-версии файлов эти фрагменты текста выделены
другим шрифтом. *)
(** Эти примеры также дают повод показать еще одну маленькую особенность
Gallina: условные выражения... *)
Definition negb' (b:bool) : bool :=
if b then false
else true.
Definition andb' (b1:bool) (b2:bool) : bool :=
if b1 then b2
else false.
Definition orb' (b1:bool) (b2:bool) : bool :=
if b1 then true
else b2.
(** Условные выражения в Coq точно такие же, как и в любом другом языке, с одним
небольшим обобщением:
Поскольку тип [bool] не встроен, Coq фактически поддерживает условные
выражения для _любого_ индуктивно определенного типа с ровно двумя
конструкторами в его определении. Условие считается истинным, если оно
вычисляется как первый конструктор [Индуктивного] определения (которое,
просто потому что мы так выбрали, в данном случае называется [true]), и
ложной, если она вычисляется как второй. *)
(** Например, мы можем определить следующий тип данных [bw] с помощью двух
конструкторов, представляющих черный ([b]) и белый ([w]) цвета, и определить
функцию [invert], которая инвертирует значения этого типа с помощью
условного выражения. *)
Inductive bw : Type :=
| bw_black
| bw_white.
Definition invert (x: bw) : bw :=
if x then bw_white
else bw_black.
Compute (invert bw_black).
(* ==> bw_white : bw *)
Compute (invert bw_white).
(* ==> bw_black : bw *)
(** **** Упражнение: 1 звезда, стандартное (nandb)
Команда [Admitted] может использоваться как заглушка для неполного
доказательства. Мы используем ее в упражнениях, чтобы указать части,
которые мы оставляем для Вас, т.е. ваша задача -- заменить [Допущенные]
части настоящими доказательствами.
Удалите "[Admitted.]" и завершите определение следующей функции; затем
убедитесь, что Coq справляется с проверкой каждого из приведенных ниже
[Примеров]. (Т.е. заполните каждое доказательство, следуя приведенной выше
модели тестов [orb], и убедитесь, что Coq их принимает.) Функция должна
возвращать [true], если один или оба ее входных сигнала равны [false].
Подсказка: если [simpl] не упростит цель вашего доказательства, вероятно,
это потому, что вы определили [nandb] без использования выражения [match].
Попробуйте другое определение [nandb] или просто пропустите [simpl] и
перейдите непосредственно к [reflexivity]. Мы объясним этот феномен позднее
в рамках этого занятия. *)
Definition nandb (b1:bool) (b2:bool) : bool
(* ЗАМЕНИТЕ ЭТУ СТРОКУ НА ":= _ваше_определение_ ." *). Admitted.
Example test_nandb1: (nandb true false) = true.
(* ЗАПОЛНИТЕ ЗДЕСЬ *) Admitted.
Example test_nandb2: (nandb false false) = true.
(* ЗАПОЛНИТЕ ЗДЕСЬ *) Admitted.
Example test_nandb3: (nandb false true) = true.
(* ЗАПОЛНИТЕ ЗДЕСЬ *) Admitted.
Example test_nandb4: (nandb true true) = false.
(* ЗАПОЛНИТЕ ЗДЕСЬ *) Admitted.
(** [] *)
(** **** Упражнение: 1 звезда, стандартное (andb3)
Сделайте то же самое для функции [andb3] ниже. Эта функция должна возвращать
[true] когда все её входы [true], и [false] в противном случае. *)
Definition andb3 (b1:bool) (b2:bool) (b3:bool) : bool
(* ЗАМЕНИТЕ ЭТУ СТРОКУ НА ":= _ваше_определение_ ." *). Admitted.
Example test_andb31: (andb3 true true true) = true.
(* ЗАПОЛНИТЕ ЗДЕСЬ *) Admitted.
Example test_andb32: (andb3 false true true) = false.
(* ЗАПОЛНИТЕ ЗДЕСЬ *) Admitted.
Example test_andb33: (andb3 true false true) = false.
(* ЗАПОЛНИТЕ ЗДЕСЬ *) Admitted.
Example test_andb34: (andb3 true true false) = false.
(* ЗАПОЛНИТЕ ЗДЕСЬ *) Admitted.
(** [] *)
(* ================================================================= *)
(** ** Типы *)
(** Каждое выражение в Coq имеет тип, описывающий, что именно оно вычисляет.
Команда [Check] просит Coq распечатать тип выражения. *)
Check true.
(* ===> true : bool *)
(** Если после [Check] следует двоеточие и объявление типа, Coq проверит,
соответствует ли тип выражения заданному типу и остановится с ошибкой, если
это не так. *)
Check true
: bool.
Check (negb true)
: bool.
(** Функции, подобные [negb], сами по себе также являются значениями, как [true]
и [false]. Их типы называются _типами функций_ и пишутся со стрелками. *)
Check negb
: bool -> bool.
(** Тип [negb], записываемый как [bool -> bool] и произносимый как "[bool]
стрелка [bool]", можно прочитать как "Получив на вход данные типа [bool],
эта функция возвращает данные типа [bool]". Аналогично, тип [and],
записанный как [bool -> bool -> bool], может быть прочитан следующим
образом: "Получив на вход два значения, каждое из которых имеет тип [bool],
эта функция возвращает значение типа [bool]". *)
(* ================================================================= *)
(** ** Новые типы из старых *)
(** Типы, которые мы определили до сих пор, являются примерами
"типов-перечислений": их определения явно перечисляют конечный набор
элементов, называемых _конструкторами_. Вот более интересное определение
типа, где один из конструкторов принимает аргумент: *)
Inductive rgb : Type :=
| red
| green
| blue.
Inductive color : Type :=
| black
| white
| primary (p : rgb).
(** Давайте изучим его более подробно.
[Индуктивное] определение выполняет две функции:
- Оно определяет набор новых _конструкторов_. Например, [red], [primary],
[true], [false], [monday] и т.д. являются конструкторами.
- Оно группирует их в новый именованный тип, например [bool], [rgb] или
[color].
_Конструкторные выражения_ формируются с помощью применения конструктора к
некоторому (0 или более) количеству других конструкторов или конструкторных
выражений, соблюдая заявленное количество и типы аргументов конструктора.
Например, это правильные конструкторные выражения...
- [red]
- [true]
- [primary red]
- и т.д.
... а эти -- нет:
- [red primary]
- [true red]
- [primary (primary red)]
- и т.д. *)
(** В частности, определения [rgb] и [color] указывают, какие конструкторные
выражения принадлежат множествам [rgb] и [color]:
- [red], [green] и [blue] принадлежат набору [rgb].;
- [black] и [white] относятся к множеству [color];
- если [p] является конструкторным выражением, принадлежащим набору [rgb],
то [primary p] ("конструктор [primary], примененный к аргументу [p]")
является конструкторным выражением, принадлежащим множеству [color]; и
- конструкторные выражения, сформированные этими способами, являются
_единственными_ принадлежащими множествам [rgb] и [color]. *)
(** Мы можем определить функции на цветах, используя сопоставление с образцом,
точно так же, как мы это делали для [day] и [bool]. *)
Definition monochrome (c : color) : bool :=
match c with
| black => true
| white => true
| primary p => false
end.
(** Раз конструктор [primary] принимает аргумент, то образец, соответствующий
[primary], должен содержать либо переменную, как мы только что сделали
(обратите внимание, что мы можем свободно выбирать ее имя), либо константу
соответствующего типа (как показано ниже). *)
Definition isred (c : color) : bool :=
match c with
| black => false
| white => false
| primary red => true
| primary _ => false
end.
(** Шаблон "[primary _]" здесь является сокращением для обозначения
"конструктора [primary], применяемого к любому конструктору [rgb], кроме
[red]". *)
(** (Пустой образец [_] имеет тот же эффект, что и фиктивная переменная-образец
[p] в определении [monochrome].) *)
(* ================================================================= *)
(** ** Модули *)
(** Coq предоставляет _модульную систему_, помогающую в организации больших
проектов. Большая часть её функций нам не понадобятся, но одна из них здесь
будет полезна: если мы поместим набор объявлений между маркерами [Module X]
и [End X], то в оставшейся части файла после [Конца] на эти определения
ссылаются как [X.foo] вместо просто [foo]. Мы будем использовать эту
функцию для ограничения области видимости определений, чтобы свободно
переиспользовать имена. *)
Module Playground.
Definition foo : rgb := blue.
End Playground.
Definition foo : bool := true.
Check Playground.foo : rgb.
Check foo : bool.
(* ================================================================= *)
(** ** Кортежи *)
Module TuplePlayground.
(** Для создания типа кортежа можно использовать один конструктор с несколькими
параметрами. В качестве примера рассмотрим представление четырех битов в
ниббле (половина байта). Сначала мы определяем тип данных [bit], который
напоминает [bool] (используя конструкторы [B0] и [B1] для двух возможных
значений битов) и затем определяем тип данных [nybble], который по сути
является кортежем из четырех битов. *)
Inductive bit : Type :=
| B1
| B0.
Inductive nybble : Type :=
| bits (b0 b1 b2 b3 : bit).
Check (bits B1 B0 B1 B0)
: nybble.
(** Конструктор [bits] действует как обёртка для его содержимого. Развёртывание
может быть выполнено путем сопоставления с образцом, как в приведенной ниже
функции [all_zero], которая проверяет, все ли биты в ниббле равны [B0].
Мы используем символ подчеркивания (_), чтобы избежать придумывания имен
переменных, которые не будут использоваться. *)
Definition all_zero (nb : nybble) : bool :=
match nb with
| (bits B0 B0 B0 B0) => true
| (bits _ _ _ _) => false
end.
Compute (all_zero (bits B1 B0 B1 B0)).
(* ===> false : bool *)
Compute (all_zero (bits B0 B0 B0 B0)).
(* ===> true : bool *)
End TuplePlayground.
(* ================================================================= *)
(** ** Числа *)
(** Мы поместили этот раздел в модуль, чтобы наше собственное определение
натуральных чисел не мешалось определению из стандартной библиотеки. В
оставшейся части книги мы будем использовать определение из стандартной
библиотеки. *)
Module NatPlayground.
(** Все типы, которые мы определили до сих пор -- как "перечисляемые типы",
такие как [day], [bool] и [bit], так и типы кортежей, такие как [nybble],
построенные на их основе -- конечны. Натуральные числа, с другой стороны,
представляют собой бесконечное множество, поэтому нам нужно будет
использовать более мощный способ объявления типа для их представления.
Существует множество вариантов представления чисел. Вы почти наверняка
знакомы с десятичной системой счисления (основание 10), в которой цифры от 0
до 9 используются, например, для образования числа 123. Скорее всего, вы
также сталкивались с шестнадцатеричной системой счисления (основание 16), в
которой то же самое число представлено как 7B, или восьмеричное (основание
8), где оно равно 173, или двоичное (основание 2), где оно равно 1111011.
Используя перечислимый тип для представления цифр, мы могли бы использовать
любое из них в качестве нашего представления натуральных чисел.
Действительно, есть обстоятельства, когда каждый из этих вариантов был бы
полезен.
Двоичное представление ценно при изготовлении микросхем, потому что цифры
могут быть представлены только двумя различными уровнями напряжения тока,
что приводит к упрощению схемы. Аналогично, в данном случае мы хотели бы
выбрать представление, которое делает _доказательства_ проще.
На самом деле, существует еще более простое представление чисел, чем в
двоичной системе, а именно унарный код (основание 1), в котором используется
только одна цифра (как, возможно, делали наши предки для подсчета дней,
делая царапины на стенах их пещер). Для представления унарных чисел с
помощью типа данных Coq мы используем два конструктора. Конструктор [O]
(заглавная О) представляет ноль. Конструктор [S], применённый к
представлению натурального числа n, представляет число n+1, где [S] означает
"последующий", "successor" (или "царапина", "scratch"). Вот полное
определение типа данных: *)
Inductive nat : Type :=
| O
| S (n : nat).
(** В таком определении, 0 представляется как [O], 1 -- как [S O], 2 -- как
[S (S O)] и т.д. *)
(** Неофициально пункты определения могут быть прочитаны следующим образом:
- [O] -- это натуральное число (помните, что это буква "[O]",
а не цифра "[0]").
- [S] можно поставить перед натуральным числом, чтобы получить другое
натуральное число; то есть, если [n] -- натуральное число, то и [S n]
тоже. *)
(** Опять же, давайте изучим это определение более подробно. В определении
[nat] говорится о том, какие выражения в множестве [nat] мы можем построить:
- конструкторное выражение [O] принадлежит множеству [nat];
- если [n] является конструкторным выражением, принадлежащим множеству