-
Notifications
You must be signed in to change notification settings - Fork 24
/
Copy pathi1prop.tex
2134 lines (2014 loc) · 78.6 KB
/
i1prop.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
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
% Diese Datei ist Teil des Buchs "Schreibe Dein Programm!"
% Das Buch ist lizenziert unter der Creative-Commons-Lizenz
% "Namensnennung - Weitergabe unter gleichen Bedingungen 4.0 International (CC BY-SA 4.0)"
% https://creativecommons.org/licenses/by-sa/4.0/deed.de
\chapter{Eigenschaften von Funktionen}
\label{cha:properties}
Woher wissen wir eigentlich, dass die Funktion, die wir geschrieben
haben, auch richtig funktioniert? Wenn wir mit
Konstruktionsanleitungen arbeiten, stehen die Chancen nicht schlecht.
Die systematische Konstruktion hilft, von vornherein die Funktion
richtig zu schreiben. Aber Kontrolle ist besser: Die Tests helfen,
etwaige Fehler zu finden. Leider funktioniert das nicht immer, weil
jeder Tests nur ein einzelnes Beispiel überprüft. Zumindest war das
bisher so. In diesem Kapitel ändern wir das, in dem wir statt
einzelner Beispiele allgemeine \textit{Eigenschaften} von Funktionen
formulieren. Aus diesen Eigenschaften können wir automatisch Tests
machen, die effektiver sind als die bisherigen, auf
\lstinline{check-expect} basierenden Tests. Für hunderprozentige
Sicherheit können wir gelegentlich auch Eigenschaften mathematisch
beweisen. Dieses Kapitel zeigt, wie es geht.
\section{Korrektheit und Tests}
\mentioncode{eigenschaften/heat-water.rkt}
%
Erinnerst Du Dich an die Funktion \lstinline{heat-water} aus
Abschnitt~\ref{sec:heat-water} auf Seite~\pageref{sec:heat-water}?
Die hatte ziemlich komplizierte Verzweigungen. Dabei sind auch ein
paar Fehler passiert. Zum Schluss hatten wir \lstinline{heat-water}
in zwei Funktionen aufgeteilt, \lstinline{heat->temperature} und
\lstinline{temperature->heat}. Hier ist die erste davon, wobei wir
die Bedingungen in der Verzweigung etwas vereinfacht haben:
%
\indexvariable{heat->temperature}
\begin{lstlisting}
; Aus Wärme Temperatur berechnen
(: heat->temperature (real -> real))
(define heat->temperature
(lambda (heat)
(cond
((<= heat 0) heat)
((<= heat 80) 0)
((<= heat 180) (- heat 80))
(else 100))))
\end{lstlisting}
%
Da waren außerdem ziemlich viele Testfälle:
%
\begin{lstlisting}
(check-expect (heat->temperature -50) -50)
(check-expect (heat->temperature 0) 0)
(check-expect (heat->temperature 20) 0)
(check-expect (heat->temperature 80) 0)
(check-expect (heat->temperature 81) 1)
(check-expect (heat->temperature 180) 100)
(check-expect (heat->temperature 200) 100)
\end{lstlisting}
%
Die Testfälle haben eigentlich zwei Aufgaben:
%
\begin{enumerate}
\item Sie sollen als Beispiele die Funktion \emph{dokumentieren}.
\item Sie sollen außerdem sicherstellen, dass die Funktion
\emph{korrekt} programmiert ist.
\end{enumerate}
%
Bei dieser Funktion allerdings gibt es bei beiden Aspekten Probleme.
Es sind so viele Testfälle und sie sind willkürlich und scheinbar
zufällig ausgewählt. Das macht es Leserinnen und Lesern schwer, das
Wirkprinzip dahinter zu erkennen. Das führt dazu, dass uns (und
vielleicht auch Dich) das Gefühl nicht loslässt, dass bei den
Testfällen noch Fehler durchschlüpfen könnten.
%
\begin{aufgabeinline}
Ändere die Funktion absichtlich so, dass sie einen Fehler enthält,
und zwar so, dass trotzdem noch alle Testfälle erfolgreich laufen.
\end{aufgabeinline}
%
Ein \lstinline{check-expect}-Test ist eben leider immer nur ein
einzelnes Beispiel, was seine Aussagekraft einschränkt.\footnote{In
der professionellen Entwicklung heißt ein solcher Test
\textit{Unit-Test}.\index{Unit-Test}.} Oft ist eine allgemeine
Aussage die bessere Dokumentation.
In diesem Fall könnten wir zum Beispiel aussagen, dass es um Wasser
geht, die Temperatur also niemals größer als 100\si{\degree}C sein kann. Das
geht nicht nur als Text, sondern auch als ein Stück Code:
%
\begin{lstlisting}
(<= (heat->temperature heat) 100)
\end{lstlisting}
%
Für sich genommen ergibt dieser Ausdruck keinen Sinn: \lstinline{heat}
ist nicht gebunden. Wir brauchen noch den Zusatz, dass die Aussage
\emph{für alle} Werte von \lstinline{heat} gilt. Also streng genommen
auch nicht für wirklich \emph{alle} Werte, nur alle Werte der Signatur
\lstinline{real}. Das können wir tatsächlich hinschreiben, mit Hilfe
einer neuen Form namens \lstinline{for-all}:
%
\begin{lstlisting}
(for-all ((heat real))
(<= (heat->temperature heat) 100))
\end{lstlisting}
%
Das \lstinline{for-all}\indexvariable{for-all}
bedeutet, wie der Name schon sagt, "<für alle">. Da steht:
%
\begin{quote}
Für \emph{alle} reellen Zahlen namens \lstinline{heat} muss das
Ergebnis von \lstinline{(heat->temperature heat)} kleiner oder
gleich 100 sein.
\end{quote}
%
"<Warum sind da doppelte Klammern um \lstinline{heat real}?"> wunderst
Du Dich vielleicht. Das liegt daran, dass in dem äußeren Klammernpaar
mehrere Variablen vorkommen können, jede davon mit Signatur in einem
inneren Klammernpaar. Dafür wird es noch Beispiele geben.
Abbildung~\ref{scheme:for-all} beschreibt genauer, wie
\lstinline{for-all}-Ausdrücke im Allgemeinen aufgebaut sind.
\begin{feature}{\lstinline{for-all}}{scheme:for-all}
\lstinline{For-all}\indexvariable{for-all} ermöglicht das
Formulieren von \textit{Eigenschaften\index{Eigenschaft}}. Ein
\lstinline{for-all}-Ausdruck hat die folgende allgemeine Form:
%
\begin{lstlisting}
(for-all (($\mathit{var}\sb{1}$ $\mathit{sig}\sb{1}$) $\ldots$ ($\mathit{var}\sb{n}$ $\mathit{sig}\sb{n}$)) $b$)
\end{lstlisting}
%
Dabei müssen die $\mathit{var}_i$ Variablen sein, die $\_i$ Signaturen und $b$ (der
Rumpf) ein Ausdruck, der entweder einen booleschen Wert oder eine
Eigenschaft liefert. Der \lstinline{for-all}-Ausdruck hat als Wert eine
Eigenschaft, die besagt, dass $b$ gilt für \emph{alle} Werte der
$\mathit{var}_i$, welche die Signaturen $\mathit{sig}_i$ erfüllen.
\end{feature}
Das Ergebnis des \lstinline{for-all}-Ausdrucks wird in der REPL
etwas undurchsichtig angezeigt:
%
\begin{lstlisting}
(for-all ((heat real))
(<= (heat->temperature heat) 100))
|\evalsto| #<:property>
\end{lstlisting}
%
Auf deutsch heißt "<property"> "<Eigenschaft">, denn
es handelt sich um eine Eigenschaft der Funktion \lstinline{heat-temperature}.
Diese Eigenschaft ersetzt nicht (immer) die Unit-Tests, ist aber eine
wertvolle Ergänzung der Dokumentation.
Sie kann außerdem auch dazu beitragen, die Korrektheit sicherzustellen.
Dazu wickeln wir um die Eigenschaft noch ein
\lstinline{check-property}:
%
\begin{lstlisting}
(check-property
(for-all ((heat real))
(<= (heat->temperature heat) 100)))
\end{lstlisting}
%
\lstinline{Check-property} macht wie \lstinline{check-expect} oder
\lstinline{check-within} einen Testfall.
Da \lstinline{heat->temperature} korrekt programmiert ist, meldet
\lstinline{check-property} nur einen bestandenen Test. Wozu
\lstinline{check-property} fähig ist, sieht man erst, wenn die
Funktion einen Fehler enthält. Wenn wir zum Beispiel aus der $180$ eine
$280$ machen, dann erscheint folgende Meldung:
%
\begin{lstlisting}
Eigenschaft falsifizierbar mit heat = |\fbox{206}|
\end{lstlisting}
%
Wichtig: Wenn Du das bei Dir ausprobierst, kann die konkrete Zahl
eine andere sein.
Dickes Wort, "<falsifizierbar">\index{falsifizierbar}: Es heißt, das
Racket ein \textit{Gegenbeispiel} für die Eigenschaft gefunden hat.
Wir können das nachprüfen:
%
\begin{lstlisting}
(heat->temperature 206)
|\evalsto| 126
\end{lstlisting}
%
\ldots{} und 126 ist größer als 100. Das Gegenbeispiel, das der
\lstinline{check-property}-Test gefunden hat, könnte, wenn wir den
Fehler nicht absichtlich gemacht hätte, dabei helfen, ihn zu finden
und zu beseitigen.
Abbildung~\ref{scheme:check-property} beschreibt den Aufbau von
\lstinline{check-property} genau.
\begin{feature}{\lstinline{check-property}}{scheme:check-property}
\lstinline{Check-property}\indexvariable{check-property}
testet eine Eigenschaft analog zu \lstinline{check-expect}. Eine
\lstinline{check-property}"=Form sieht so aus:
%
\begin{lstlisting}
(check-property $e$)
\end{lstlisting}
%
$e$ ist ein Ausdruck, der eine Eigenschaft liefern muss~-- in der Regel
also ein \lstinline{for-all}-Ausdruck. Die Form testet dann diese
Eigenschaft. (Mehr dazu im nächsten Abschnitt.)
\lstinline{Check-property} funktioniert nur für Eigenschaften, bei
denen aus den Signaturen sinnvoll Werte generiert werden können. Dies
ist für die meisten Signaturen der Fall, aber nicht für
\lstinline{any} und Signaturvariablen definiert wurden.
\end{feature}
\begin{aufgabeinline}
Mache noch absichtlich ein paar weitere Fehler in
\lstinline{heat->temperature}. Welche davon werden von dem
\lstinline{check-property}-Test gefunden und welche nicht?
\end{aufgabeinline}
Da \lstinline{check-property} eine Eigenschaft testet, heißt diese
Technik auch \textit{property-based testing}.\index{property-based
testing} Die werden wir im Laufe dieses Kapitels noch auf andere
Funktionen anwenden.
\section{Wie \lstinline{check-property} funktioniert}
Zunächst machen wir aber einen kleinen Exkurs: Was passiert
bei so einem \lstinline{check-property}-Test?
Toll wäre natürlich, wenn dieser mit Gewissheit sagen könnte, was die
Eigenschaft besagt: Dass zum Beispiel die Temperatur wirklich für
\emph{alle} Eingaben höchstens $100$ ist. Das ist im allgemeinen leider
unmöglich.\footnote{Dass das unmöglich ist, wurde mathematisch
bewiesen und als \textit{Satz von Rice} festgehalten. Der ist Thema
in der theoretischen Informatik.} In manchen Fällen ist es trotzdem
möglich, Eigenschaften von Funktionen formal zu beweisen. (Dazu mehr in
Abschnitt~\ref{sec:eigenschaften-beweisen} auf
Seite~\pageref{sec:eigenschaften-beweisen}.)
\lstinline{Check-property} kann also nicht
beweisen, dass eine Eigenschaft gilt. Stattdessen führt es Stichproben durch: Dafür
wählt es für die Signaturen zufällig Werte aus, und
wiederholt diesen Prozess, um so aus einem Testfall viele individuelle
Tests zu machen~-- typischerweise $100$.
Die Verwendung des Begriffs "<zufällig"> ist in diesem Zusammenhang in
der Informatik so üblich, ein besseres Wort wäre aber "<chaotisch">.
Tatsächlich produziert \lstinline{check-property} bei jedem Durchlauf
des Programms die gleichen Tests.
Die Technik des \textit{property-based testing}, also zunächst
allgemeine Eigenschaften zu formulieren und für diese dann Testfälle
zu erzeugen, ist ursprünglich unter dem Namen \textit{QuickCheck}
veröffentlicht worden~\cite{ClaessenHughes2000} und war ein
großer Durchbruch bei der Effektivität von automatischen Tests.
\section{Mehr Eigenschaften und inexakte Zahlen}
\label{sec:inexakt}
Es geht weiter mit \lstinline{heat-water}:
\lstinline{Heat->temperature} ist nur eine Hilfsfunktion dafür,
zusammen mit \lstinline{temperature->heat}. Auch hier könnten wir
eine Eigenschaft aufschreiben, die etwas über den Zahlenbereich
aussagt, der aus der Funktion herauskommt.
Allerdings gibt es noch eine weitaus ergiebigere Eigenschaft:
\lstinline{temperature->heat} soll ist ja gerade
\lstinline{heat->temperature} "<umdrehen">. Daraus können wir
folgende Eigenschaft beziehungsweise folgenden Test machen:
%
\begin{lstlisting}
(check-property
(for-all ((temp real))
(= (heat->temperature (temperature->heat temp))
temp)))
\end{lstlisting}
%
Der besagt also dass, wenn eine Temperatur in Wärme gewandelt wird und
wieder zurück in eine Temperatur, dass dann das gleiche herauskommen
soll. Leider schlägt der Test fehl:
%
\begin{lstlisting}
Eigenschaft falsifizierbar mit temp = |\fbox{0}|
\end{lstlisting}
%
Wir können in der REPL für \lstinline{temp} mal $0$ einsetzen:
%
\begin{lstlisting}
(heat->temperature (temperature->heat 0))
|\evalsto| cond: alle Bedingungen ergaben #f
\end{lstlisting}
%
Da war doch was? Vielleicht erinnerst Du Dich: Eine Temperatur von
$0$\si{\degree}C kann nicht eindeutig einer Wärmezahl zugeordnet werden,
die kann zwischen $0$ und $80$ liegen. Deshalb weigert sich auch
\lstinline{heat->temperature}, für die Eingabe $0$ ein Ergebnis zu
produzieren. Wir müssen also unseren Test anpassen, so dass da steht
"<für alle reellen Zahlen \emph{außer} $0$">.
Das geht folgendermaßen:
%
\begin{lstlisting}
(check-property
(for-all ((temp real))
(==> (not (= temp 0))
(= (heat->temperature (temperature->heat temp))
temp))))
\end{lstlisting}
%
Der Pfeil \lstinline{==>} ist neu und funktioniert nur im Kontext
einer Eigenschaft: Er bedeutet "<gilt unter der Voraussetzung">.
Abbildung~\ref{feature:implication} erklärt im Detail, wie die Form
funktioniert. Hier steht also, dass die Gleichung nur gelten muss
unter der Voraussetzung, dass \lstinline{temp} nicht $0$ ist.
\begin{feature}{Voraussetzung bei Eigenschaften}{feature:implication}
In einer Eigenschaft steht die Form
\begin{lstlisting}
(==> $p$ $e$)
\end{lstlisting}
dafür, dass die Eigenschaft $e$ nur dann gelten muss, wenn der
Ausdruck $p$ den Wert \lstinline{#t} ergibt.
Bei einer \lstinline{==>}-Form generiert \lstinline{check-property}
nur solche Tests, bei denen $p$ \lstinline{#t} ergibt.
\end{feature}
%
\begin{quote}
\noindent \emph{Anmerkung:} Du könntest die Eigenschaft oben
auch mit \lstinline{if} statt \lstinline{==>} hinschreiben:
%
\begin{lstlisting}
(check-property
(for-all ((temp real))
(if (= temp 0))
#t
(= (heat->temperature (temperature->heat temp))
temp)))
\end{lstlisting}
%
Bedeuten würde die Eigenschaft so das gleiche. Allerdings behandelt
\lstinline{check-property} diese Schreibweise anders, nämlich
schlechter. Wenn \lstinline{check-property} 100 Tests generiert, dann
werden alle, bei denen \lstinline{temp} $0$ ist, als Erfolg gewertet,
obwohl da eigentlich nichts getestet wird. Wenn für (hypothetisch)
drei von den Tests \lstinline{temp} $0$ ist, dann werden also nur 97
richtige Tests durchgeführt.
In der Variante mit \lstinline{==>} stellt
\lstinline{check-property} sicher, dass tatsächlich $100$ Tests
durchgeführt werden, bei denen \lstinline{temp} nicht $0$ ist. Damit
wird mehr getestet.
\end{quote}
%
Wenn wir den Testfall laufen lassen, gibt es allerdings eine
merkwürdige Überraschung:
%
\begin{lstlisting}
Eigenschaft falsifizierbar mit temp = #i24.571428571428573
\end{lstlisting}
%
Wie schon gesagt, die konkrete Zahl kann anders aussehen, aber es geht
um das merkwürdige \mbox{\lstinline{#i}}. Das steht für "<inexakt">, weil es
sich um eine sogenannte \textit{inexakte Zahl}\index{inexakte Zahl}
handelt. Solche Zahlen werden von DrRacket (und so gut wie allen
anderen Programmiersprachen) verwendet, um die Ergebnisse von
Berechnungen darzustellen, die (zumindest mit vertretbarem Aufwand)
nicht ganz genau durchgeführt werden können.
Bisher ging in diesem Buch alles noch ganz genau, weil unsere
Programme bisher intern exakte Bruchrechnung verwendet haben. Um so
eine inexakte Zahl zu berechnen, kannst Du zum Beispiel das hier in
der REPL ausprobieren, um die Quadratwurzel ("<square root">) von 2
auszurechnen:
%
\begin{lstlisting}
(sqrt 2)
|\evalsto| #i1.4142135623730951
\end{lstlisting}
%
Die Wurzel von 2 hat unendlich viele Nachkommestellen, weswegen Racket
davon nur einige ausrechnet und rundet. Und damit wir und Du wissen,
dass gerundet wurde, steht das \lstinline{#i} davor.
Das mit dem Runden ist sogar noch komplizierter, als es scheint: Es
wird nämlich \emph{binär} gerechnet. Wie genau abläuft, ist ziemlich
kompliziert und würde ein weiteres Buch füllen. Mehr zu dem Thema
findet sich zum Beispiel im Standardwerk von David
Goldberg~\cite{Goldberg1991}.
Aber was ist denn nun genau bei unserer Eigenschaft passiert? Wir
können die \lstinline{#i}-Zahl von Hand in die Eigenschaft einsetzen
und in der REPL auswerten:
%
\begin{lstlisting}
(heat->temperature (temperature->heat #i24.571428571428573))
|\evalsto| #i24.57142857142857
\end{lstlisting}
%
Du kannst sehen, dass offensichtlich beim Rechnen gerundet wurde, und
zwar bei der letzten Nachkommastelle. Das erscheint Dir vielleicht
merkwürdig, weil in
\lstinline{temperature->heat} doch ausschließlich addiert und
subtrahiert wird~-- da ist keine Spur von "<Runden">. Wenn wir das
\lstinline{#i} weglassen, wird auch exakt gerechnet:\footnote{Falls Du
es mal mit einer der anderen Sprachen zu tun hast, die beim
Racket-System dabei sind: Bei den meisten von ihnen wird, anders als
hier, jede Zahl mit Dezimalpunkt als inexakt behandelt.}
%
\begin{lstlisting}
(heat->temperature (temperature->heat 24.571428571428573))
|\evalsto| 24.571428571428573
\end{lstlisting}
%
Das liegt daran, dass "<Inexaktheit"> ansteckend ist: Wenn beim Aufruf
einer Rechenfunktion wie \lstinline{+} oder \lstinline{*} auch nur
eine Eingabe inexakt ist, wird gerundet.
Weil diese Rundung manchmal überraschend ist, weigert sich übrigens
\lstinline{check-expect}, inexakte Zahlen zu vergleichen.
%
\begin{aufgabeinline}
Probier's aus: \lstinline{(check-expect #i1 #i1)}
\end{aufgabeinline}
%
Bei unserer Eigenschaft
haben für \lstinline{temp} die Signatur \lstinline{real} angegeben: In
dieser Signatur sind auch inexakte Zahlen enthalten, deshalb nimmt da
das Problem seinen Anfang. Wir können es auf zwei Arten angehen:
%
\begin{itemize}
\item Wir ersetzen in der Eigenschaft die Signatur \lstinline{real}
durch \lstinline{rational}. In \lstinline{rational} sind keine
inexakten Zahlen drin. Das hat allerdings den Nachteil, dass auch
nur auf exakten Zahlen getestet wird, obwohl die Funktionen auch auf
inexakten Zahlen funktionieren.
\item Wir berücksichtigen den Effekt der Rundung, indem wir die Bedingung
in der Eigenschaft etwas aufweichen.
\end{itemize}
%
Wir machen letzteres und fordern nur, dass der Abstand zwischen echtem
und erwartetem Ergebnis einen bestimmten Betrag nicht überschreitet:
%
\begin{lstlisting}
(check-property
(for-all ((temp real))
(==> (not (= temp 0))
(<= (abs
(- (heat->temperature (temperature->heat temp))
temp))
0.0000001))))
\end{lstlisting}
%
Zur Erinnerung: Die eingebaute Funktion \lstinline{abs} berechnet den
Absolutbetrag, dreht also bei negativen Zahlen das Vorzeichen um,
siehe Abschnitt~\ref{func:abs} auf Seite~\pageref{func:abs}.
Leider schlägt der Test immer noch fehl, zum Beispiel mit folgender
Ausgabe:
%
\begin{lstlisting}
Eigenschaft falsifizierbar mit temp = 105
\end{lstlisting}
%
Das können wir in der REPL ausprobieren:
%
\begin{lstlisting}
(heat->temperature (temperature->heat 105))
|{\color{red}cond: alle Bedingungen ergaben \#f}|
\end{lstlisting}
%
Das liegt daran, dass \lstinline{temperature->heat} nur für
Temperaturen bis 100\si{\degree}C funktioniert: Wasser kann ja nicht
heißer werden. Wir müssen also unsere Vorbedingung erweitern:
%
\begin{lstlisting}
(check-property
(for-all ((temp real))
(==> (and (not (= temp 0))
(< temp 100))
(<= (abs
(- (heat->temperature (temperature->heat temp))
temp))
0.0000001))))
\end{lstlisting}
%
Dieser Text drückt ganz gut aus, wie \lstinline{heat->temperature} und
\lstinline{temperature->heat} zueinander stehen: Die eine dreht die
andere um, zumindest ungefähr. In der Mathematik heißt es, dass die
eine Funktion die \textit{Inverse} andere anderen Funktion
ist.\index{Inverse}
\begin{aufgabeinline}
Versuche, den letzten \lstinline{check-property}-Test
auszutricksen. Anstatt kleine Fehler einzuführen, versuche es mal
mit ganz anderen Funktionen, die gar nichts mit Wassertemperaturen
zu tun haben, aber trotzdem die obige Eigenschaft haben.
\end{aufgabeinline}
%
Die Aufgabe zeigt, dass Eigenschaften kein Garant für Korrektheit
sind: Genau wie bei Unit-Tests auch braucht es oft mehrere davon oder
zusätzliche \lstinline{check-expect}-Tests, um für genug Sicherheit zu
sorgen.
Die Technik aus der Aufgabe ist dabei hilfreich: Überlege, wie Du
einen Testfall durch falsche Funktionen austricksen kannst. Füge dann
Testfälle hinzu, die diese Fehler aufspüren.
% FIXME: Konstruktionsanleitung?
% FIXME Korrektheit Gürteltiere
\section{Relationale Probleme}
\mentioncode{eigenschaften/sort.rkt}
%
Die Eigenschaften für \lstinline{heat-water} hätten wir auch durch
eine (lange) Reihe von \lstinline{check-expect}-Tests ersetzen können.
Die Eigenschaften sind da hilfreich, aber nicht unverzichtbar. Aber
es gibt Funktionen, bei denen Unit-Tests grundsätzlich nicht das
richtige sind, nämlich solche, die sogenannte \textit{relationale
Probleme} lösen. Um die geht es in diesem Abschnitt.
Wir schreiben zunächst eine solche Funktion, um das Konzept zu
erklären: Sie soll die Mitglieder einer Band nach Alter sortieren.
Hier sind Daten- und Record-Definition für ein Bandmitglied:
%
\begin{lstlisting}
(define-record band-member
make-band-member
(band-member-name string)
(band-member-born natural))
\end{lstlisting}
%
Und hier die konkrete Band dazu (Stand 2021):
%
\indexvariable{axl (make-band-member "Axl Rose" 1962))}
\indexvariable{duff (make-band-member "Duff McKagan" 1964))}
\indexvariable{slash (make-band-member "Slash" 1965))}
\indexvariable{dizzy (make-band-member "Dizzy Reed" 1963))}
\indexvariable{richard (make-band-member "Richard Fortus" 1966))}
\indexvariable{frank (make-band-member "Frank Ferrer" 1966))}
\indexvariable{melissa (make-band-member "Melissa Reese" 1990))}
\indexvariable{guns-n-roses}
\begin{lstlisting}
(define axl (make-band-member "Axl Rose" 1962))
(define duff (make-band-member "Duff McKagan" 1964))
(define slash (make-band-member "Slash" 1965))
(define dizzy (make-band-member "Dizzy Reed" 1963))
(define richard (make-band-member "Richard Fortus" 1966))
(define frank (make-band-member "Frank Ferrer" 1966))
(define melissa (make-band-member "Melissa Reese" 1990))
(define guns-n-roses
(list axl duff slash dizzy richard frank melissa))
\end{lstlisting}
%
Wir machen das mit einem
einfachen, wenn auch ineffizienten Verfahren namens
\textit{Insertionsort}\index{Insertionsort}:
Die Sortierfunktion arbeitet mit einer sortierten Liste als
Akkumulator. Diese ist anfänglich leer, und die Funktion fügt jeweils
ein Element aus der Eingabeliste hinzu, indem sie dies an der
richtigen Stelle einfügt.
%
% Die Funktion soll sowohl aufsteigend als auch absteigend oder nach
% einem anderen Kriterium sortieren können. Das machen wir ähnlich wie
% bei den Suchbäumen in Abschnitt~\ref{sec:suchbaeume} auf
% Seite~\pageref{sec:suchbaeume}, wo wir über die Funktionen abstrahiert
% haben, die Markierungen im Baum vergleichen. Bei den Suchbäumen waren
% das zwei Funktionen für \lstinline{=} und \lstinline{<}, hier
% kombinieren wir beides in eine Funktion wie \lstinline{<=}.
% FIXME: Halbordnung irgendwann?
%
%
Wir schreiben zunächst eine Hilfsfunktion zum Einfügen eines Elements.
Das Testen heben wir uns ausnahmsweise bis zum Testen der
Sortierfunktion auf. Kurzbeschreibung,
Signatur, Gerüst und Schablone:
%
\indexvariable{insert}
\begin{lstlisting}
; Bandmitglied in eine sortierte Liste einfügen
(: insert
(band-member (list-of band-member) -> (list-of band-member)))
(define insert
(lambda (band-member list)
(cond
((empty? list) ...)
((cons? list)
...
(first list)
(insert band-member (rest list))
...))))
\end{lstlisting}
%
Im ersten Fall fügt die Funktion eine leere Liste ein: Das Ergebnis
sollte dann die Liste mit \lstinline{element} als einzigem Element
sein. Im zweiten Fall ist noch unklar, wo \lstinline{band-member}
eingefügt wird, vor oder nach \lstinline{(first list)}. Die Funktion
muss die beiden Geburtsjahre miteinander vergleichen:
%
\begin{lstlisting}
(define insert
(lambda (band-member list)
(cond
((empty? list) (cons band-member empty))
((cons? list)
(if (<= (band-member-born band-member)
(band-member-born (first list)))
(cons band-member list)
(cons (first list)
(insert band-member (rest list))))))))
\end{lstlisting}
%
Mit Hilfe von \lstinline{insert} bauen wir nun die Funktion
\lstinline{sort-band}. Kurzbeschreibung, Signatur und Unit-Test:
%
\begin{lstlisting}
; Band nach Alter sortieren
(: sort-band ((list-of band-member) -> (list-of band-member)))
(check-expect (sort-band guns-n-roses)
(list axl dizzy duff slash richard frank melissa))
\end{lstlisting}
%
Hier die Schablone für die Funktion~-- mit Akkumulator:
%
\begin{lstlisting}
(define sort-band
(lambda (list0)
; Invariante: ...
(define accumulate
(lambda (list acc)
(cond
((empty? list) ...)
((cons? list)
(accumulate (rest list)
... (first list) ... acc ...)))))
(accumulate list0 ...)))
\end{lstlisting}
%
Beim Akkumulieren enthält \lstinline{list} die schon gesehenen
Elemente aus \lstinline{list0}, und zwar sortiert. Daraus können wir
eine Invariante formulieren:
%
\begin{lstlisting}
; Invariante: list enthält die Bandmitglieder
; zwischen list0 und list, sortiert.
\end{lstlisting}
%
Damit können wir die Lücken füllen: \lstinline{acc} ist beim ersten
Aufruf leer. Wenn \lstinline{list} leer ist, dann ist \lstinline{acc}
das gewünschte Endergebnis. Das neue Zwischenergebnis berechnet die
Funktion mit Hilfe von \lstinline{insert}:
%
\indexvariable{sort-band}
\begin{lstlisting}
(define sort-band
(lambda (list0)
; Invariante: list enthält die Bandmitglieder
; zwischen list0 und list, sortiert.
(define accumulate
(lambda (list acc)
(cond
((empty? list) acc)
((cons? list)
(accumulate (rest list)
(insert (first list) acc))))))
(accumulate list0 empty)))
\end{lstlisting}
%
Fertig! Halt, da ist noch ein kleines Problem:
%
\begin{verbatim}
Der tatsächliche Wert
#<list
#<record:band-member "Axl Rose" 1962>
#<record:band-member "Dizzy Reed" 1963>
#<record:band-member "Duff McKagan" 1964>
#<record:band-member "Slash" 1965>
#<record:band-member "Frank Ferrer" 1966>
#<record:band-member "Richard Fortus" 1966>
#<record:band-member "Melissa Reese" 1990>>
ist nicht der erwartete Wert
#<list
#<record:band-member "Axl Rose" 1962>
#<record:band-member "Dizzy Reed" 1963>
#<record:band-member "Duff McKagan" 1964>
#<record:band-member "Slash" 1965>
#<record:band-member "Richard Fortus" 1966>
#<record:band-member "Frank Ferrer" 1966>
#<record:band-member "Melissa Reese" 1990>>.
\end{verbatim}
%
Woran liegt's? Richard Fortus und Frank Ferrer sind im selben Jahr
geboren. Der Unit-Test nimmt an, dass Fortus vor Ferrer
einsortiert wird, aber \lstinline{sort-band} macht es aber genau
umgekehrt. Deswegen ist \lstinline{sort-band} nicht verkehrt: Es gibt
einfach mehrere korrekte Antworten.
Der Unit-Test ist also ungünstig, selbst wenn wir die Reihenfolge so
wählen, dass er nicht fehlschlägt. Wenn wir die Suchfunktion
irgendwann mal ändern, kann
sich die Reihenfolge ändern und der Test schlägt wieder fehl, auch
wenn die Funktion korrekt ist.
Da es für \lstinline{sort-band} für eine gegebene Eingabe mehr als ein
korrektes Ergebnis geben kann, sprechen wir von einem "<relationalen
Problem">:\index{relationales Problem} Es steht nicht das präzise
Ergebnis fest, nur die Beziehung ("<Relation">) zwischen Ein- und
Ausgabe. Und um die zu beschreiben, ist eine Eigenschaft das richtige
Mittel. Wie könnte eine sinnvolle Eigenschaft einer Funktion
aussehen, die sortiert?
Nun, dass die Ausgabe sortiert ist. Um das festzustellen, schreiben
wir eine Funktion:
%
\begin{lstlisting}
; Band sortiert?
(: band-sorted? ((list-of band-member) -> boolean))
\end{lstlisting}
%
Die Tests lassen unterschiedliche Reihenfolgen zu, solange sie
sortiert sind:
%
\begin{lstlisting}
(check-expect
(band-sorted? (list axl dizzy duff slash frank richard melissa))
#t)
(check-expect
(band-sorted? (list axl dizzy duff slash richard frank melissa))
#t)
(check-expect
(band-sorted? (list dizzy axl duff slash richard frank melissa))
#f)
(check-expect
(band-sorted? (list axl dizzy duff richard slash frank melissa))
#f)
\end{lstlisting}
%
Hier sind Gerüst und Schablone:
%
\begin{lstlisting}
(define band-sorted?
(lambda (list)
(cond
((empty? list) ...)
((cons? list)
... (first list) ...
... (band-sorted? (rest list) ...)))))
\end{lstlisting}
%
Der \lstinline{empty}-Fall ist einfach: Eine leere Liste ist sortiert.
Im \lstinline{cons}-Fall ist es etwas schwieriger. Um die Reihenfolge
zu überprüfen, müssen wir zwei Elemente der Liste miteinander
vergleichen, da ist aber nur \lstinline{(first list)}. Wir brauchen
also noch das zweite Element. Das gibt es nur bei Listen mit mehr als
einem Element, weswegen wir eine zweite Verzweigung brauchen:
%
\begin{lstlisting}
(define band-sorted?
(lambda (list)
(cond
((empty? list) #t)
((cons? list)
(cond
((empty? (rest list)) ...)
((cons? (rest list))
...
(first list)
(first (rest list))))
(band-sorted? (rest list))
...))))
\end{lstlisting}
%
Der innere \lstinline{empty}-Fall ist die Liste mit einem Element: Die
ist auch immer sortiert. Im \lstinline{cons}-Fall schließlich können
wir die beiden ersten Elemente vergleichen:
%
\indexvariable{band-sorted?}
\begin{lstlisting}
(define band-sorted?
(lambda (list)
(cond
((empty? list) #t)
((cons? list)
(cond
((empty? (rest list)) #t)
((cons? (rest list))
(and (<= (band-member-born (first list))
(band-member-born (first (rest list))))
(band-sorted? (rest list)))))))))
\end{lstlisting}
%
Diese Funktion können wir nun benutzen, um aufzuschreiben, dass
\lstinline{sort-band} immer sortierte Listen produziert:
%
\begin{lstlisting}
(check-property
(for-all ((list (list-of band-member)))
(band-sorted? (sort-band list))))
\end{lstlisting}
%
Diese Eigenschaft bringt auf den Punkt, was \lstinline{sort-band}
ausmacht, nämlich dass sie sortierte Listen produziert. Sie ist also
schon mal eine gute Dokumentation.
Ist sie auch ein guter Testfall? Vielleicht hast Du ein mulmiges
Gefühl, dass wir für den Test von \lstinline{sort-band} eine weitere
selbstgeschriebene Funktion benutzt haben, die fast ebenso kompliziert
ist wie \lstinline{sort-band} selbst. Was, wenn wir in
\lstinline{band-sorted?} einen Fehler gemacht haben, und zwar so,
dass der Eigenschafts-Testfall dann einen Fehler in
\lstinline{sort-band} nicht mehr findet. Das ist natürlich
theoretisch möglich, ist aber unwahrscheinlich und wird umso
unwahrscheinlicher, je mehr Testfälle mit aussagekräftigen
Eigenschaften dazukommen.
Diese Eigenschaften sind eine Form von Redundanz,\index{Redundanz}
analog dazu, bei Gebäuden lieber die tragenden Wände etwas stärker zu
machen als unbedingt notwendig. Ob diese Redundanz die Arbeit wert
ist, eine Funktion wie \lstinline{band-sorted?} zu schreiben, die nur
für das Testen gut sind, hängt vom Einzelfall ab: Je wichtiger die
Korrektheit der Funktion und je komplizierter sie ist, desto größer
ist der Wert solcher Testfälle.
Trotzdem kann man die obige Eigenschaft austricksen, ziemlich einfach
sogar:
%
\begin{lstlisting}
(define sort-band
(lambda (list0)
empty))
\end{lstlisting}
%
Das ist natürlich ein bisschen gemein. Aber die Funktion, die den
Testfall austrickst, ist einfacher, als die richtige Funktion. Es ist
also einfacher, es falsch zu machen als richtig. Deshalb sollten wir
nach weiteren Eigenschaften suchen, die solche einfachen aber falschen
Lösungen finden. Zum Beispiel könnten wir fordern, dass die
Ausgabeliste genauso lang ist wie die Eingabe:
%
\begin{lstlisting}
(check-property
(for-all ((list (list-of band-member)))
(= (length (sort-band list))
(length list))))
\end{lstlisting}
%
\begin{aufgabeinline}
Die folgende Version von \lstinline{sort-band} besteht die
bisherigen Testfälle:
%
\begin{lstlisting}
(define sort-band
(lambda (list)
(cond
((empty? list) empty)
((cons? list)
(map (lambda (band-member)
(first list))
list)))))
\end{lstlisting}
%
Kannst Du einen Testfall schreiben, der den Fehler in dieser
Funktion findet?
\end{aufgabeinline}
% FIXME:
% - viele check-expect-Tests ersetzen
% - relationale Probleme
% - Invarianten
% - häufig vorkommende Eigenschaften
\section{Konstruktionsanleitungen für Testfälle?}
\label{sec:ka-testfaelle}
Die bisherigen Beispiele für Testfälle mit Eigenschaften haben Dich
hoffentlich überzeugt, dass es eine gute Idee ist, solche Testfälle zu
schreiben. Aber \emph{wie} geht das eigentlich bei der nächsten
Funktion, die getestet werden soll? Toll wären
Konstruktionsanleitungen analog zu denen für Funktionen, die zeigen,
wie wir Eigenschaften aus der Signatur der zu testenden Funktion
herleiten können. Das hätte zumindest bei den Funktionen der
bisherigen Abschnitte dieses Kapitels nicht funktioniert.
Trotzdem gibt es ein paar Dinge, die Du probieren kannst, wenn Du
Eigenschaften für eine neue Funktion formulieren willst:
%
\begin{enumerate}
\item Benutze das Wissen über die Größen Deines Problems.
Beispiele:
\begin{itemize}
\item Wenn es um die Temperatur von Wasser geht, weißt Du,
dass die Temperatur nicht größer als $100$\si{\degree}C sein kann.
\item Du weißt, das
die Ausgabe einer Sortierfunktion sortiert sein sollte.
\end{itemize}
\item Oft gehören zu einem Problem mehrere Funktionen, die auf
bestimmte Art und Weise zusammenarbeiten. Schreibe auf, wie diese
Zusammenarbeit aussieht.
Beispiel: \lstinline{Temperature->heat} dreht
\lstinline{heat->temperature} um.
\item Versuche, die bestehenden Eigenschaften durch einfache aber
fehlerhafte Varianten Deiner Funktion auszutricksen.
Suche dann nach
Eigenschaften, die das Austricksen verhindern.
Beispiel: Die leere Liste als Resultat \lstinline{sort-band} ist
immer sortiert, aber trotzdem falsch.
\end{enumerate}
%
Abschnitt~\ref{sec:algebraische-eigenschaften} auf
Seite~\pageref{sec:algebraische-eigenschaften} wird diese Liste noch
ergänzen um Eigenschaften, die tatsächlich mit den Signaturen der zu
testenden Funktionen zu tun haben.
\section{Suchbäume testen}
\label{sec:suchbaeume-testen}
\mentioncode{baeume/binary-tree.rkt}
%
In Abschnitt~\ref{sec:balanced-search-trees} auf
Seite~\pageref{sec:balanced-search-trees} haben wir die Funktion
\lstinline{balanced-search-tree-insert}, und die war ziemlich
kompliziert. Vielleicht ging es Dir wie uns~-- wir hatten ein etwas
mulmiges Gefühl, ob die Funktion auch wirklich korrekt ist.
Sie hat ziemlich viele Verzweigungen, nicht nur in der Funktion selbst
sondern auch in der Hilfsfunktion \lstinline{make-balanced-node}.
Dass die Testfälle wirklich jede mögliche Form von Suchbaum testen,
ist ziemlich unwahrscheinlich.
Eine weitere Schwierigkeit beim Testen von
\lstinline{balanced-search-tree-insert} ist, dass die Funktion ein
relationales Problem löst: Es gibt mehr als ein korrektes
Ergebnis. Bei einem Testfall mit \lstinline{check-expect}-,
der ein bestimmtes Ergebnis von
\lstinline{balanced-search-tree-insert} erwartet, kann es sein, dass
die Funktion ein anderes, aber trotzdem korrektes Ergebnis liefert.
Der Testfall schlägt dann fehl, und die Suche nach der Ursache ist
mühsam.
Aber mit ein paar Eigenschaften können wir (hoffentlich) das mulmige
Gefühl beseitigen und unser Vertrauen in die Funktion erhöhen. Der
vorige Abschnitt hatte einige Vorschläge, wie wir vorgehen können.
Der erste Vorschlag war:
%
\begin{quote}
Benutze das Wissen über die Größen Deines Problems.
\end{quote}
%
Die Größe unseres Problems ist hier der balancierte Suchbaum. In dem
Begriff steckt schon ziemlich viel Wissen:
%
\begin{enumerate}
\item Das Ergebnis von \lstinline{balanced-search-tree-insert} sollte ein
sortierter Baum sein, bei dem alle Markierungen im linken Teilbaum
eines Knotens kleiner sein sollte als die Markierung des Knotens.
\item Die Bäume sind größenannotiert: Die Größenannotation sollte auch
stimmen, also bei jedem Knoten die tatsächliche Größe des Baums
darunter wiedergeben.
\item Schließlich und endlich sollte der balancierte Suchbaum
natürlich auch \emph{balanciert} sein.
\end{enumerate}
%
Wir fangen mal mit dem zweiten Punkt an, und überlassen Dir danach den
ersten als Übungsaufgabe. Um zu überprüfen, ob die Größenannotation
stimmt, müssen wir die Annotation jeden Knoten des Baums betrachten.
Dafür brauchen wir eine Hilfsfunktion. Sie hat folgende
Kurzbeschreibung und Signatur:
%
\begin{lstlisting}
; Stimmt die Größenannotation am Suchbaum?
(: proper-sized-search-tree? ((sized-search-tree-of %a) -> boolean))
\end{lstlisting}
%
Hier sind zwei einfache Testfälle dafür:
%
\begin{lstlisting}
(check-expect (proper-sized-search-tree? sized-search-tree1) #t)
(check-expect (proper-sized-search-tree?
(make-sized-search-tree
= <
(make-sized-node
5
(make-node (make-sized-label 5 3) #f #f)
(make-node (make-sized-label 2 7) #f #f))))
#f)
\end{lstlisting}
%
Nur zwei popelige Testfälle, magst Du einwenden, mehr wären sicher