jede Woche eine Vorlesung, dort werden Hausaufgaben gestellt
Aufgaben zur Präsentation in der nächsten Übung
jeder soll (als Teil einer Gruppe) insgesamt 3 mal präsentieren
Aufgaben in autotool (selbständige Bearbeitung innerhalb der nächsten zwei Wochen)
jeder soll insgesamt 50 % der Pflichtaufgaben erledigen
Prüfung: Klausur am Computer (autotool, in Präsenz unter Aufsicht)
Skript (Woche für Woche)
Plan: vgl. How I Teach Functional Programming, WFLP 2017,
Quelltexte aus Vorlesung, Vorbereitung Hausaufgaben (zur Präsentation):
Einschreiben! (Projekt wird dann private geschaltet)
Hausaufgaben (individuell)
Einschreiben! (Übungsgruppe wählen, Hinweis beachten)
Arbeiten im Pool: Shell, $PATH
, ghci
,
vgl.
$ cabal update
# $HOME/.config/cabal/config editieren (-haddock)
$ cabal install --lib leancheck
$ ghci
GHCi, version 9.12.2: https://www.haskell.org/ghc/ :? for help
ghci> import Test.LeanCheck
ghci> check $ \ p q -> (p && q) == (q && p)
ghci> :doc check
ssh-keygen
, .ssh/id_ed25519.pub
\(\Rightarrow\) gitlab.dit
,
git clone
wenn Zeit ist: autotool 15-1,
1. Sem: Modellierung (formale Spezifikationen (von konkreten und abstrakten Datentypen))
1./2. Sem Grundlagen der (AO) Programmierung
imperatives Progr. (Programm ist Folge von Anweisungen, bewirkt Zustandsänderung)
strukturiertes P. (genau ein Eingang/Ausgang je Teilp.)
objektorientiertes P. (Interface \(=\) abstrakter Datentyp, Klasse \(=\) konkreter Datentyp)
2. Sem: Algorithmen und Datenstrukturen
(Spezifikation, Implementierung, Korrektheit, Komplexität)
3. Sem: Softwaretechnik (industrielle Softwareproduktion)
deklarative Programmierung
(Programm ist ausführbare Spezifikation)
insbesondere: funktionale Programmierung
Def: Programm berechnet Funktion \(f:\text{Eingabe}\mapsto\text{Ausgabe}\),
(kein Zustand, keine Zustandsänderungen)
Daten (erster Ordnung) sind Bäume
Programm ist Gleichungssystem
Programme sind auch Daten (höherer Ordnung)
ausdrucksstark, sicher, effizient, parallelisierbar
funktionale Programmierung: foldr (+) 0 [1,2,3]
foldr f z l = case l of
[] -> z ; (x:xs) -> f x (foldr f z xs)
logische Programmierung: append(A,B,[1,2,3]).
append([],YS,YS).
append([X|XS],YS,[X|ZS]):-append(XS,YS,ZS).
Constraint-Programmierung
(set-logic QF_LIA) (set-option :produce-models true)
(declare-fun a () Int) (declare-fun b () Int)
(assert (and (>= a 5) (<= b 30) (= (+ a b) 20)))
(check-sat) (get-value (a b))
Rechnen \(=\) Auswerten von Ausdrücken (Termbäumen)
Dabei wird ein Wert bestimmt
und es gibt keine (versteckte) Wirkung.
(engl.: side effect, dt.: Nebenwirkung)
Werte können sein:
“klassische” Daten (Zahlen, Listen, Bäume…)
True :: Bool
,
[3.5, 4.5] :: [Double]
Funktionen (Sinus, …)
[sin, cos] :: [Double -> Double]
Aktionen (Datei lesen, schreiben, …)
readFile "foo.text" :: IO String
…der funktionalen Programmierung
Beweisbarkeit: Rechnen mit Programmen
wie in der Mathematik mit Termen
Sicherheit: es gibt keine Nebenwirkungen
und Wirkungen sieht man bereits am Typ
Aussdrucksstärke, Wiederverwendbarkeit: durch Funktionen höherer
Ordnung (sog. Entwurfsmuster)
Effizienz: durch Programmtransformationen im Compiler,
Parallelität: keine Nebenwirkungen \(\Rightarrow\) keine data
races,
fktl. Programme sind automatisch parallelisierbar
import Test.LeanCheck
append :: forall t . [t] -> [t] -> [t]
append [] y = y
append (h : t) y = h : (append t y)
associative f =
\ x y z -> f x (f y z) == f (f x y) z
commutative f = \ x y -> ...
test = check (associative (append @Bool))
Übung: Kommutativität (formulieren und testen)
app :: forall t . [t] -> [t] -> [t]
app [] y = y
app (h : t) y = h : (app t y)
Lemma: app x (app y z) .=. app (app x y) z
Proof by induction on List x
Case []
To show: app [] (app y z) .=. app (app [] y) z
Case h:t
To show: app (h:t) (app y z) .=. app (app (h:t) y) z
IH: app t (app y z) .=. app (app t y) z
CYP https://github.com/noschinl/cyp,
ist vereinfachte Version
von Isabelle https://isabelle.in.tum.de/
-- Länge der Collatz-Folge
collatz :: Int -> Int
collatz x = if x <= 1 then 0
else 1 + collatz (if even x then div x 2 else 3*x+1)
-- Summe der Längen
main :: IO ()
main = print $ sum
$ map collatz [1 .. 10^7]
wird parallelisiert durch Strategie-Annotation:
import Control.Parallel.Strategies
...
main = print $ sum
$ withStrategy (parListChunk (10^5) rseq)
$ map collatz [1 .. 10^7]
Die Anzahl der 1-Bits einer nichtnegativen Zahl:
Func<int,int>f =
x=>{int s=0; while(x>0){s+=x%2;x/=2;}return s;}
\(\displaystyle\sum_{x=0}^{2^{26}-1} f(x)\)
Enumerable.Range(0,1<<26).Select(f).Sum()
automatische parallele Auswertung, Laufzeitvergleich:
Time(()=>Enumerable.Range(0,1<<26).Select(f).Sum())
Time(()=>Enumerable.Range(0,1<<26)
.AsParallel().WithDegreeOfParallelism(4)
.Select(f).Sum())
vgl. Introduction to PLINQ
…der statischen Typisierung
The language in which you write profoundly affects the design of programs written in that language.
For example, in the OO world, many people use UML to sketch a design. In Haskell or ML, one writes type signatures instead. Much of the initial design phase of a functional program consists of writing type definitions.
Unlike UML, though, all this design is incorporated in the final product, and is machine-checked throughout.
Simon Peyton Jones, in: Masterminds of Programing, 2009;
funktionale Programmierung: diese Vorlesung
logische Programmierung: in Grundl. Künstl. Intell.
Constraint-Programmierung: als Wahlfach (WS 23)
Beziehungen zu weiteren LV: Voraussetzungen
Bäume, Terme (Modellierung, Alg.+DS)
Logik (Grundlagen TI, Softwaretechnik)
Anwendungen:
Softwarepraktikum
weitere Sprachkonzepte in Prinzipien v. Programmiersprachen
Programmverifikation (vorw. f. imperative Programme)
Funktionale Programmierung ist ein Konzept. Realisierungen:
in prozeduralen Sprachen:
Unterprogramme als Argumente (in Pascal)
Funktionszeiger (in C)
in OO-Sprachen: Befehlsobjekte
Multi-Paradigmen-Sprachen:
Lambda-Ausdrücke in C#, Scala, Clojure
funktionale Programmiersprachen (LISP, ML, Haskell)
Die Erkenntnisse sind sprachunabhängig.
A good programmer can write LISP in any language.
Learn Haskell and become a better Java programmer.
Terme, Termersetzungssysteme, algebraische Datentypen, Pattern Matching, Persistenz
Funktionen (polymorph, höherer Ordnung), Lambda-Kalkül, Rekursionsmuster
Typklassen zur Steuerung der Polymorphie
(Anwendung: automatische Testdatenerzeugung)
Bedarfsauswertung, unendl. Datenstrukturen
Konstruktorklassen (Functor, Applicative, Monad)
Collections (endliche Mengen, Abbildungen, Folgen)
als Anwendung vorher gezeigter Konzepte
algebraische Datentypen, Pattern Matching, Termersetzungssysteme
Scale: case class, Java: Entwurfsmuster Kompositum,
immutable objects (record
), das Datenmodell von
Git
Funktionen (höherer Ordnung), Lambda-Kalkül, Rekursionsmuster
Lambda-Ausdrücke in C#, Entwurfsmuster Besucher
Codequalität, code smells, Refaktorisierung
Typklassen zur Steuerung der Polymorphie: Interfaces
Bedarfsauswertung, unendl. Datenstrukturen
Iteratoren, Ströme, LINQ
Functor, Applicative, Monad: map
,
flatMap
wissenschaftliche Quellen zur aktuellen Forschung und Anwendung der funktionalen Programmierung
Journal of Functional Programming (CUP) https://www.cambridge.org/core/journals/journal-of-functional-programming
Intl. Conference Functional Programming (ACM SIGPLAN) https://www.icfpconference.org/
Intl. Workshop Trends in Functional Programming in Education https://wiki.tfpie.science.ru.nl/
(Sprachstandard, Werkzeuge, Bibliotheken, Tutorials),
Skript aktuelles Semester https://www.imn.htwk-leipzig.de/~waldmann/lehre.html
How I Teach Functional Programming (WFLP 2017) https://www.imn.htwk-leipzig.de/~waldmann/talk/17/wflp/
Kriterium für Haskell-Tutorials und -Lehrbücher:
wo werden data
(benutzerdefinerte algebraische
Datentypen) und case
(pattern matching) erklärt?
Je später, desto schlechter!
Q: Aber in Wikipedia/Stackoverflow steht, daß …
A: Na und.
Es mag eine in Einzelfällen nützliche Übung sein,
sich mit dem Halbwissen von Nichtfachleuten auseinanderzusetzen.
(Aber
)
In VL und Übung verwenden und diskutieren wir die durch Dozenten/Skript/Modulbeschreibung vorgegebenen Quellen (Lehrbücher, referierte Original-Artikel, Standards zu Sprachen und Bibliotheken)
…gilt entsprechend für Ihre Bachelor- und Master-Arbeit.
Wikipedia: benutzen—ja (um Primärquellen zu finden), zitieren—nein (ist keine wissenschaftliche Quelle).
jede Woche eine Vorlesung, eine Übung
Hausaufgaben
gruppenweise: markierte Aufgaben aus dem Skript: anmelden (Wiki), diskutieren (Issue-Tracker), vorrechnen (in der jeweils nächsten Übung)
individuell (jeweils 2 Wochen Bearbeitungszeit) https://autotool.imn.htwk-leipzig.de/new/
Prüfungszulassung: regelmäßiges und erfolgreiches Bearbeiten der Übungsaufgaben.
Vorrechnen: 3 mal,
Autotool: 50 Prozent der Pflicht-Aufgaben,
Prüfung: Klausur 120 min (am Computer), keine Hilfsmittel
Informationen zur VL: https://www.imn.htwk-leipzig.de/~waldmann/lehre.html
digitale Selbstverteidigung: Browser und Suchmaschine datenschutzgerecht auswählen und einstellen.
Das Geschäftsmodell der Überwachungswirtschaft ist es, Ihren Bildschirmplatz, und damit Ihre Aufmerksamkeit und Ihre Lebenszeit an Anzeigenkunden zu verkaufen. Um dabei höhere Erlöse zu erzielen, wird Ihr Verhalten vermessen, gespeichert, vorhergesagt und beeinflußt. Die dazu angelegten Personenprofile erlauben eine umfassende privatwirtschaftliche und staatliche Überwachung. Diese soll verschleiert, verharmlost und legalisiert werden.
Siehe auch
OS Überwachungskapitalismus https://www.imn.htwk-leipzig.de/~waldmann/talk/19/ubkap/,
VL Informatik (Nebenfach) https://www.imn.htwk-leipzig.de/~waldmann/edu/ws21/inf/folien/#(11)
Benutzung Rechnerpool (ssh, tmux, ghci) https://www.imn.htwk-leipzig.de/~waldmann/etc/pool/
Beispiel Funktionale Programmierung
$ /usr/local/waldmann/opt/ghc/latest/bin/ghci
ghci> length $ takeWhile (== '0') $ reverse $ show $ foldr (*) 1 [1 .. 100 :: Integer]
Typ und Wert von Teilausdrücken feststellen, z.B.
ghci> :set +t
ghci> foldr (*) 1 [1..100 :: Integer]
Beachte polymorphe numerische Literale.
(Auflösung der Polymorphie durch Typ-Annotation.)
Warum ist 100 Fakultät als Int
gleich 0?
Welches ist der Typ der Funktion takeWhile
?
Beispiel:
odd 3 ==> True ; odd 4 ==> False
takeWhile odd [3,1,4,1,5,9] ==> [3,1]
ersetze in der Lösung takeWhile
durch andere
Funktionen des gleichen Typs (suche diese mit Hoogle), erkläre
Semantik
typische Eigenschaften dieses Beispiels (nachmachen!)
statische Typisierung, Schachtelung von Funktionsaufrufen, Funktion höherer Ordnung, Benutzung von Funktionen aus Standardbibliothek (anstatt selbstgeschriebener).
schlechte Eigenschaften (vermeiden!)
Benutzung von Zahlen und Listen (anstatt anwendungsspezifischer Datentypen) vgl. http://www.imn.htwk-leipzig.de/~waldmann/etc/untutorial/list-or-not-list/
Haskell-Entwicklungswerkzeuge
Compiler, REPL: ghci (Fehlermeldungen, Holes)
API-Suchmaschine
Editor: Emacs https://xkcd.com/378/,
IDE? gibt es, brauchen wir (in dieser VL) nicht https://hackage.haskell.org/package/haskell-language-server
Softwaretechnik im autotool:
benutzen Sie gitlab.imn zur Koordinierung: (einmalig) Einteilung in Dreiergruppen, (wöchentlich) Bearbeitung der Aufgaben. Benutzen Sie Wiki und Issues mit sinnvollen Titeln/Labeln. Schließen Sie erledigte Issues.
Jede der markierten Aufgabe kann in jeder Übung aufgerufen werden (Bsp: Aufg. 3 in den INB-Übungen und in der MIB-Übung) Es kann dann eine vorher gemeinsam (von mehreren Gruppen) vorbereitete Lösung präsentiert werden—die aber von jedem einzelnen Präsentator auch verstanden sein sollte.
Auch die nicht markierten Aufgaben können in den Übungen diskutiert werden—wenn dafür Zeit ist.
SS 25: Aufgabe 1, 4, 5
Digitale Selbstverteidigung
Welche Daten gibt Ihr Browser preis?
Starten Sie in einer Konsole den Befehl
nc -l -p 9999
(Konsole soll sichtbar bleiben)
Rufen Sie im Browser die Adresse http://localhost:9999
auf, beobachten Sie die Ausgabe in der Konsole.
Wie (personen)spezifisch ist diese Information?
Wie können weitere Informationen extrahiert werden? Verwenden Sie https://www.eff.org/press/releases/test-your-online-privacy-protection-effs-panopticlick (Electronic Frontier Foundation, 2015–)
Stellen Sie Firefox datenschutzgerecht ein. (Das beginnt mit der Default-Startseite!)
Zeigen Sie die Benutzung von temporary containers, von Profilen (z.B. ein Profil für Browsing im Screen-Share).
Führen Sie Browser-Plugins uMatrix
,
uBlockOrigin
vor.
zu: E. W. Dijkstra: Answers to Questions from Students of Software Engineering (Austin, 2000) (EWD 1035)
putting the cart before the horse
übersetzen Sie wörtlich ins Deutsche,
geben Sie eine entsprechende idiomatische Redewendung in Ihrer Muttersprache an,
wofür stehen cart und horse hier konkret?
sind die empfohlenen exakten Techniken der Programmierung für große Systeme anwendbar?
Erklären Sie lengths of …grow not much more than linear with the lengths of ….
Welche Längen werden hier verglichen?
Modellieren Sie das System als Graph, die Knoten sind die Komponenten, die Kanten sind deren Beziehungen (direkte Abhängigkeiten).
Welches asymptotische Wachstum ist bei undisziplinierter Entwicklung des Systems zu befürchten?
Welche Graph-Eigenschaft impliziert den linearen Zusammenhang?
Wie gestaltet man den System-Entwurf, so daß diese Eigenschaft tatsächlich gilt? Welchen Nutzen hat das für Entwicklung und Wartung?
Lesen Sie E. W. Dijkstra: On the foolishness of "natural language programming" https://www.cs.utexas.edu/users/EWD/transcriptions/EWD06xx/EWD667.html
und beantworten Sie
womit wird “einfaches Programmieren” fälschlicherweise gleichgesetzt?
welche wesentliche Verbesserung brachten höhere Programmiersprachen, welche Eigenschaft der Maschinensprachen haben sie trotzdem noch?
warum sollte eine Schnittstelle narrow sein?
welche formalen Notationen von Vieta, Descartes, Leibniz, Boole sind gemeint? (jeweils: Wissenschaftsbereich, (heutige) Bezeichnung der Notation, Beispiele)
warum können Schüler heute das lernen, wozu früher nur Genies in der Lage waren?
Übersetzen Sie den Satz “the naturalness of …obvious”.
Geben Sie dazu jeweils an:
die Meinung des Autors, belegt durch konkrete Textstelle und zunächst wörtliche, dann sinngemäße Übersetzung
Beispiele aus Ihrer Erfahrung
Über ein Monoid \((M,\circ, 1)\) mit Elementen \(a, b\in M\) (sowie eventuell weiteren) ist bekannt: \(a^2= b^2 = (ab)^2 = 1\).
Dabei ist \(ab\) eine Abkürzung für \(a\circ b\) und \(a^2\) für \(aa\), usw.
Geben Sie ein Modell mit \(1\neq a\neq b\neq 1\) an.
Überprüfen Sie \(ab = ba\) in Ihrem Modell.
Leiten Sie \(ab=ba\) aus den Monoid-Axiomen und gegebenen Gleichungen ab.
Das ist eine Übung zur Wiederholung der Konzepte abstrakter und konkreter Datentyp sowie Spezifikation.
im Rechnerpool live vorführen:
ein Terminal öffnen
ghci
starten (in der aktuellen Version), Fakultät
von 100 ausrechnen
Datei F.hs
mit Texteditor anlegen und öffnen,
Quelltext f = ...
(Ausdruck mit Wert 100!
)
schreiben, diese Datei in ghci
laden, f
auswerten
Dabei wg. Projektion an die Wand:
Schrift 1. groß genug und 2. schwarz auf weiß.
Vorher Bildschirm(hintergrund) aufräumen, so daß bei Projektion keine
personenbezogenen Daten sichtbar werden. Beispiel:
export PS1="$ "
ändert den Shell-Prompt (versteckt den
Benutzernamen).
Wer eigenen Rechner im Pool benutzt:
Aufgaben wie oben und
ssh
-Login auf einen Rechner des Pools
(damit wird die Ausrede GHC (usw.) geht auf meinem Rechner nicht hinfällig)
ssh
-Login oder remote-Desktop-Zugriff von
einem Rechner des Pools auf Ihren Rechner (damit das projiziert werden
kann, ohne den Beamer umzustöpseln)
(falls das alles zu umständlich ist, dann eben doch einen Pool-Rechner benutzen)
(Prädikatenlogik) Signatur \(\Sigma\) ist Menge von Funktionssymbolen mit Stelligkeiten
ein Term \(t\) in Signatur \(\Sigma\) ist
Funktionssymbol \(f\in\Sigma\)
der Stelligkeit \(k\)
mit Argumenten \((t_1,\ldots,t_k)\),
die selbst Terme sind.
\(\operatorname{Term}(\Sigma)=\) Menge der Terme über Signatur \(\Sigma\)
(Graphentheorie) ein Term ist ein
gerichteter, geordneter, markierter Baum
(Datenstrukturen)
Funktionssymbol \(=\) Konstruktor, Term \(=\) Baum
Signatur: \(\Sigma=\{ Z/0, S/1, f/2 \}\)
Elemente von \(\operatorname{Term}(\Sigma)\):
\(Z(), \quad S(S(Z())), \quad f(S(S(Z())), Z())\)
Abkürzung: das leere Argument-Tupel (die Klammern) nach nullstelligen Symbolen weglassen, \(f(S(S(Z)),Z)\)
Signatur: \(\Gamma=\{ E/0, A/1, B/1 \}\)
Elemente von \(\operatorname{Term}(\Gamma)\): …
Bezeichnung: für Signatur \(\Sigma\) und \(k\in\NN\):
\(\Sigma_k\) bezeichnet Menge der Symbole aus \(\Sigma\) mit Stelligkeit \(k\)
\(\Sigma_0=\{Z\},\Sigma_1=\{S\},\Sigma_2=\{f\},\)
\(\Gamma_0=\{E\},\Gamma_1=\dots, \Gamma_2=\dots\)
die Größe: ist Funktion \(|\cdot|:\operatorname{Term}(\Sigma)\to\NN\) mit
für \(f\in\Sigma_k\) gilt \(|f(t_1,\ldots,t_k)| = 1 + |t_1| + \ldots + |t_k|\)
die Größe eines Terms ist der Nachfolger der Summe der Größen seiner Kinder
Bsp: \(|S(S(Z()))|=1+|S(Z())|=1+1+|Z()| = 1+1+1\)
\(|f(S(S(Z())),Z()|=\dots\)
die Höhe: ist Funktion \(\operatorname{\textsf {height}}:\operatorname{Term}(\Sigma)\to\NN\):
für \(t=f(t_1,\ldots,t_k)\) gilt
wenn \(k=0\), dann \(\operatorname{\textsf {height}}(t)= 0\)
wenn \(k>0\), dann \(\operatorname{\textsf {height}}(t)=1 + \max(\operatorname{\textsf {height}}(t_1),\ldots,\operatorname{\textsf {height}}(t_k))\)
Satz: \(\forall t\in\operatorname{Term}(\{a/0,b/2\}): |t|\equiv 1 \pmod 2\) (die Größe ist ungerade)
Beweis durch Induktion über den Termaufbau:
IA (Induktions-Anfang): \(t=a()\)
Beweis für IA: \(|t|=|f()|=1\equiv 1\pmod 2\)
IS (I-Schritt): \(t=b(t_1,t_2)\)
zu zeigen ist: IB (I-Behauptung): \(|t|\equiv 1\pmod 2\)
dabei benutzen: IV (I-Voraussetzung) \(|t_1|\equiv|t_2|\equiv 1\pmod 2\)
Beweis für IS: \(|t|=|b(t_1,t_2)|=1+|t_1|+|t_2| \equiv 1 + 1 + 1 \equiv 1 \pmod 2\)
Bezeichnung: das heißt IV, und nicht I-Annahme, damit es nicht mit I-Anfang verwechselt wird
Beispiel: Deklaration des Typs
data Foo = Con {bar :: Int, baz :: String}
deriving Show
Bezeichnungen:
Foo
ist Typname
Con
ist Konstruktor
bar, baz
sind Komponenten-Namen des
Konstruktors
Int, String
sind Komponenten-Typen
Beispiel: Konstruktion eines Datums dieses Typs
Con { bar = 3, baz = "hal" } :: Foo
der Ausdruck (vor dem ::
) hat den Typ
Foo
Beispiel: Deklaration des Typs
data Foo = Con Int String
Bezeichnungen:
Foo
ist Typname
Con
ist zweistelliger Konstruktor
…mit anonymen Komponenten
Int
, String
sind
Komponenten-Typen
Beispiel: Konstruktion eines Datums dieses Typs
Con 3 "hal" :: Foo
auch ein Konstruktor mit benannten Komponenten kann positionell aufgerufen werden
Beispiel (selbst definiert)
data T = A { foo :: Bool }
| B { bar :: Ordering, baz :: Bool }
deriving Show
Bespiele (in Standardbibliothek (Prelude) vordefiniert)
data Bool = False | True
data Ordering = LT | EQ | GT
Konstruktion solcher Daten:
False :: Bool
A { foo = False } :: T ; A False :: T
B EQ True :: T
(bisher) einsortige Signatur
ist Abbildung von Funktionssymbol nach Stelligkeit
(neu) mehrsortige Signatur
Menge von Sortensymbolen \(S=\{S_1,\ldots\}\)
msS ist Abb. von Funktionssymbol nach Typ
Typ ist Element aus \(S^*\times S\)
Folge der Argument-Sorten, Resultat-Sorte
Bsp.: \(S=\{Z,B\}, \Sigma=\{0 \mapsto ([],Z), p \mapsto ([Z,Z],Z),\) \(e\mapsto ([Z,Z],B), a\mapsto([B,B],B)\}\).
\(\operatorname{Term}(\Sigma,B)\) (Terme dieser Signatur mit Sorte \(B\)): …
Konstruktoren mit benannten Komponenten
data Tree = Leaf {}
| Branch { left :: Tree , right :: Tree }
mit anonymen Komponenten
data Tree = Leaf | Branch Tree Tree
Objekte dieses Typs erzeugen, Bsp:
Leaf :: Tree; Branch (Branch Leaf Leaf) Leaf :: Tree
Bezeichnung
data Tree = ... | Node ...
ist falsch (irreführend), denn sowohl äußere Knoten (Leaf) als auch innere Knoten (Branch) sind Knoten (Node)
Ü: die data-Dekl. für \(S=\{Z,B\}\), \(\Sigma=\{0 \mapsto ([],Z),\) \(p \mapsto ([Z,Z],Z),\) \(e\mapsto ([Z,Z],B), a\mapsto([B,B],B)\}\).
mathematisches Modell: Term über Signatur
programmiersprachliche Bezeichnung: algebraischer Datentyp (die Konstruktoren bilden eine Algebra)
praktische Anwendungen:
Formel-Bäume (in Aussagen- und Prädikatenlogik)
Suchbäume (in VL Algorithmen und Datenstrukturen, in
java.util.TreeSet<E>
)
DOM (Document Object Model) https://www.w3.org/DOM/DOMTR
JSON (Javascript Object Notation) z.B. für AJAX https://www.ecma-international.org/publications/standards/Ecma-404.htm
Geben Sie die Signatur des Terms \(\sqrt{a\cdot a + b\cdot b}\) an.
Bestimmen Sie \(|\sqrt{a\cdot a + b\cdot b}|\), \(\operatorname{\textsf {height}}(\sqrt{a\cdot a + b\cdot b})\).
Geben Sie ein Element \(t \in \operatorname{Term}(\{ f/1, g/3, c/0 \})\) an mit \(|t|=5\) und \(\operatorname{\textsf {height}}(t)\le 2\).
die Menge \(\operatorname{Term}(\{ f/1,
g/3, c/0 \})\) wird realisiert durch den Datentyp
data T = F T | G T T T | C deriving Show
deklarieren Sie den Typ in ghci, erzeugen Sie o.g. Term \(t\) (durch Konstruktoraufrufe)
Holes (Löcher) in Ausdrücken als Hilfsmittel bei der Programmierung durch schrittweises Verfeinern
ghci> data T = A Bool | B T deriving Show
ghci> A _
<interactive>:2:3: error:
• Found hole: _ :: Bool
• In the first argument of ‘A’, namely ‘_’
In the expression: A _
In an equation for ‘it’: it = A _
• Relevant bindings include ...
Valid hole fits include ...
False :: Bool
True :: Bool
...
SS 25: Aufgaben 2, 3, 4.
autotool-Aufgabe 15-2
Allgemeine Hinweise zur Bearbeitung von Haskell-Lückentext-Aufgaben in autotool:
Schreiben Sie den angezeigten Quelltext (vollständig! ohne
zusätzliche Leerzeichen am Zeilenanfang!) in eine Datei mit Endung
.hs
, starten Sie ghci mit diesem Dateinamen als
Argument
ändern Sie den Quelltext: ersetzen Sie undefined
durch einen geeigneten Ausdruck, hier z.B.
solution = S.fromList [ False, G ]
im Editor speichern, in ghci neu laden (:r
)
reparieren Sie Typfehler, werten Sie geeignete Terme aus, hier
z.B. S.size solution
werten Sie test
aus, wenn test
den Wert
True ergibt, dann tragen Sie die Lösung in autotool ein.
Geben Sie einen Typ \(T\) (eine
data
-Deklaration) an, der alle Terme der einsortigen
Signatur \(\Sigma=\{E/0, F/2, G/3\}\)
enthält.
Konstruieren Sie Elemente dieses Typs.
Geben Sie \(t\in\operatorname{Term}(\Sigma)\) an mit
\(\operatorname{\textsf {height}}(t)=2\) und \(|t|\) möglichst klein
\(\operatorname{\textsf {height}}(t)=2\) und \(|t|\) möglichst groß
Allgemeine Hinweise zu Arbeit und Präsentation im Pool:
beachten Sie https://www.imn.htwk-leipzig.de/~waldmann/etc/pool/
(PATH
und ggf. LD_LIBRARY_PATH
)
Freigabe (für Dozenten-Rechner) mit krfb
,
Einmalpaßwort
Schrift schwarz auf weiß! Vernünftige Schriftgröße (Control-Plus)!! Gleichzeitig sichtbar (d. h.: keine Verdeckungen, Umschaltungen): Aufgabenstellung, Programmtext, Ausgabe/Fehlermeldungen. Wenn der Desktop-Hintergrund sichtbar ist—wurde Platz verschenkt!!!
Geben Sie einen Typ (eine data
-Deklaration) mit
genau 71 Elementen an. Sie können weitere Data-Deklarationen benutzen.
Minimieren Sie die Gesamt-Anzahl der Konstruktoren.
Bsp:
data Bool = False | True ; data T = X Bool Bool
dieses T
hat 4 Elemente, 3 Konstruktoren
(False,True,X
)
Beweisen Sie \(\forall \Sigma: \forall t\in\operatorname{Term}(\Sigma): \operatorname{\textsf {height}}(t) \le |t|-1\).
durch Induktion über den Term-Aufbau.
Induktions-Anfang: \(t=f()\) (nullstelliges Symbol \(f\))
Induktions-Schritt:
\(t=f(t_1,\dots,t_k)\) (\(k\)-stelliges Symbol \(f\), für \(k>0\))
dabei Induktions-Voraussetzung: die Behauptung gilt für \(t_1,\ldots,t_k\).
Induktions-Behauptung: …für \(t\).
Für welche Terme \(t\) gilt Gleichheit? Wo sieht man das im Beweis?
wieviele Elemente des Datentyps data T = L | B T T
haben …
die Größe 9
die Größe \(\le 9\)
die Höhe \(0, 1, 2, 3, \dots, k, \dots\)
Sie müssen diese Elemente nicht alle einzeln angeben.
Bestimmen sie ihre Anzahl durch dynamische Programmierung (von Hand).
Ersetzen Sie undefined
, so daß der Ausdruck
test
den Wert True hat.
module Blueprint where
import qualified Data.Set as S
-- aus Prelude importiert:
-- data Bool = False | True
data C = R | G | B deriving (Eq, Ord, Show)
data T = X C Bool | Y Bool | Z deriving (Eq, Ord, Show)
solution :: S.Set T
solution = S.fromList undefined
test :: Bool
test = S.size solution == 9
Ansatz einer Lösung:
solution = S.fromList [ X R False, Y True ]
einen Term in einer mehrsortigen Signatur angeben (Programmiersprache: Java)
Gesucht ist ein Ausdruck vom Typ int
in der Signatur
Foo e;
String g;
static char a ( Bar x, String y, String z );
static Bar b ( Foo x );
static int c ( int x, String y );
static int d ( char x, Foo y, String z );
static Foo f ( int x );
static String h ( Foo x, String y, char z );
Lösungsansatz
d (a (b (e), ...), ...)
Fill holes (_) (replace with one item)
and ellipsis (...) (replace with several items)
in the following, so that the claim at the top becomes true.
For this exercise, each data declaration can only refer to types defined earlier -
not to itself or later (it cannot be recursive).
size_of (T) == 71 where
{ data Bool = False | True
; data Col = R | G | B
; data S = ...
; data T = ...
}
wir haben: für Baum-artige Daten:
mathematisches Modell: Terme über einer Signatur
Realisierung als: algebraischer Datentyp
(data
)
wir wollen: für Programme, die diese Daten verarbeiten:
mathematisches Modell: Termersetzung
Realisierung als: Pattern matching (case
)
Position: Folge von natürlichen Zahlen
(bezeichnet einen Pfad von der Wurzel zu einem Knoten)
Beispiel: für \(t = S(f(S(S(Z())), Z()))\)
ist \([0,1]\) eine Position in \(t\).
\(\operatorname{Pos}(t) =\) die Menge der Positionen eines Terms \(t\)
Definition: wenn \(t=f(t_0,\ldots,t_{k-1})\), d.h., \(k\) Kinder
dann \(\operatorname{Pos}(t)= \{ [] \} \cup \{ [i] \cdot p \mid 0\le i< k \wedge p\in \operatorname{Pos}(t_i) \}\).
dabei bezeichnen:
\([]\) die leere Folge,
\([i]\) die Folge der Länge 1 mit Element \(i\),
\(\cdot\) den Verkettungsoperator für Folgen
\(t[p] =\) der Teilterm von \(t\) an Position \(p\)
Beispiel: \(S(f(S(S(Z())), Z()))[0,1] = \dots\)
Definition (durch Induktion über die Länge von \(p\)): …
\(t[p := s]\) : wie \(t\), aber mit Term \(s\) an Position \(p\)
Beispiel: \(S(f(S(S(Z())), Z()))[[0,1]:=S(Z)] = \dots\)
Definition (durch Induktion über die Länge von \(p\)): …
\(\operatorname{Term}(\Sigma,V)=\) Menge der Terme über Signatur \(\Sigma\) mit Variablen aus \(V\)
Beispiel: \(\Sigma=\{Z/0,S/1,f/2\}, V=\{y\}\), \(f(Z(),y)\in\operatorname{Term}(\Sigma,V)\).
Substitution \(\sigma\): partielle Abbildung \(V \to \operatorname{Term}(\Sigma)\)
Beispiel: \(\sigma_1 = \{(y,S(Z()))\}\)
eine Substitution auf einen Term anwenden: \(t \sigma\):
Intuition: wie \(t\), aber statt \(v\) immer \(\sigma(v)\)
Beispiel: \(f(Z(),y)\sigma_1 = f(Z(),S(Z()))\)
Definition durch Induktion über \(t\)
Daten \(=\) Terme (ohne Variablen)
Programm \(R\) \(=\) Menge von Regeln
Bsp: \(R=\{ (f(Z(),y),y), \; (f(S(x),y),S(f(x,y)))\}\)
Regel \(=\) Paar \((l,r)\) von Termen mit Variablen
Relation \(\to_R\) ist Menge aller Paare \((t,t')\) mit
es existiert \((l,r)\in R\)
es existiert Position \(p\) in \(t\)
es existiert Substitution \(\sigma:(\operatorname{Var}(l)\cup \operatorname{Var}(r))\to\operatorname{Term}(\Sigma)\)
so daß \(t[p] = l \sigma\) und \(t' = t[p := r \sigma]\).
\(\to_R\) beschreibt einen Schritt der Rechnung von \(R\),
transitive und reflexive Hülle \(\to_R^*\)
beschreibt Folge von Schritten.
Resultat einer Rechnung ist Term in \(R\)-Normalform
(\(:=\) ohne \(\to_R\)-Nachfolger)
dieses Berechnungsmodell ist im allgemeinen
nichtdeterministisch \(R_1=\{ C(x,y) \to x, C(x,y) \to y \}\)
(ein Term kann mehrere \(\to_R\)-Nachfolger haben,
ein Term kann mehrere Normalformen erreichen)
nicht terminierend \(R_2= \{ p(x,y) \to p(y,x) \}\)
(es gibt eine unendliche Folge von \(\to_R\)-Schritten,
es kann Terme ohne Normalform geben)
Für TRS \(R\) über Signatur \(\Sigma\): Symbol \(s\in\Sigma\) heißt
definiert, wenn \(\exists
(l,r)\in R : l[] = s(\dots)\)
(das Symbol in der Wurzel ist \(s\))
sonst Konstruktor.
Das TRS \(R\) heißt Konstruktor-TRS, falls:
definierte Symbole kommen links nur in den Wurzeln vor
Übung: diese Eigenschaft formal spezifizieren
Beispiele: \(R_1=\{a(b(x)) \to b(a(x))\}\) über \(\Sigma_1=\{a/1,b/1\}\),
\(R_2=\{f(f(x,y),z) \to f(x,f(y,z))\) über \(\Sigma_2=\{f/2\}\):
definierte Symbole? Konstruktoren? Konstruktor-System?
Funktionale Programme sind ähnlich zu Konstruktor-TRS.
Termersetzungssystem:
Signatur: \(\{(S,1),(Z,0),(f,2)\}\), Variablenmenge \(\{x',y\}\)
Ersetzungssystem \(\{ f(Z,y) \to y, f(S(x'),y) \to S(f(x',y)) \}\).
ist Konstruktor-System, definierte Symbole: \(\{f\}\), Konstruktoren: \(\{S,Z\}\),
Startterm \(f(S(S(Z)),S(Z))\).
funktionales Programm:
data N = Z | S N -- Signatur für Daten
f :: N -> N -> N -- Signatur für Funktion
f Z y = y ; f (S x') y = S (f x' y) -- Gleichungen
f (S (S Z)) (S Z) -- Benutzung der definierten Fkt.
für die Definition einer Funktion \(f\) mit diesem Typ
data N = Z | S N ; f :: N -> N -> N
(eben gesehen) mehrere Gleichungen
f Z y = y
f (S x') y = S (f x' y)
äquivalente Notation: eine Gleichung,
in der rechten Seite: Verzweigung (erkennbar an
case
)
mit zwei Zweigen (erkennbar an ->
)
f x y = case x of
{ Z -> y ; S x' -> S (f x' y) }
data N = Z | S N ; data Bool = False | True
positive :: N -> Bool
positive x = case x of { Z -> False ; S x' -> True }
Syntax:
case <Diskriminante> of
{ <Muster> -> <Ausdruck> ; ... }
<Muster>
enthält Konstruktoren und Variablen,
entspricht linker Seite einer Term-Ersetzungs-Regel,
<Ausdruck>
entspricht rechter Seite
statische Semantik (eines case-Ausdrucks mit Typ
T
)
jedes <Muster>
hat gleichen Typ wie
<Diskrim.>
,
jeder <Ausdruck>
hat den Typ
T
dynamische Semantik:
Def.: \(t\) paßt zum Muster \(l\): es existiert \(\sigma\) mit \(l\sigma=t\)
für das erste Muster, das zum Wert der Diskriminante paßt, wird \(r\sigma\) ausgewertet
ein case
-Ausdruck heißt
disjunkt, wenn die Muster nicht überlappen
(es gibt keinen Term, der zu mehr als 1 Muster paßt)
vollständig, wenn die Muster den gesamten Datentyp abdecken
(es gibt keinen Term, der zu keinem Muster paßt)
Bespiele (für data T = A | B T | F T T
)
nicht disjunkt:
case t of { F (B x) y -> .. ; F x (B y) -> .. }
nicht vollständig
case t of { F x y -> .. ; A -> .. }
data
und case
typisches Vorgehen beim Verarbeiten algebraischer Daten vom Typ
T
:
Für jeden Konstruktor des Datentyps
data T = C1 ...
| C2 ...
schreibe einen Zweig in der Fallunterscheidung
f x = case x of
{ C1 ... -> ...
; C2 ... -> ... }
Argumente der Konstruktoren sind Variablen \(\Rightarrow\) Case-Ausdruck ist disjunkt und vollständig.
Scala: case classes https://docs.scala-lang.org/tutorials/tour/case-classes.html
Java (ab JDK 21)
jshell --enable-preview # Version 21-ea
interface I {}
record A (int x) implements I {}
I o = new A(4)
switch (o) {
case A(var y) : System.out.println(y);
default : }
Nicht verwechseln mit regular expression matching zur String-Verarbeitung. Es geht um algebraische (d.h. baum-artige) Daten!
der Datentyp
import qualified Prelude
data Bool = False | True
deriving Prelude.Show
die Negation
not :: Bool -> Bool
not x = case x of { False -> _ ; True -> _ }
die Konjunktion (als Operator geschrieben)
(&&) :: Bool -> Bool -> Bool
x && y = case x of { False -> _ ; True -> _ }
die Syntax eines Namens bestimmt, ob er als Funktion oder Operator verwendet wird:
Name aus Buchstaben (Bsp.: not
,
plus
)
steht als Funktion vor den Argumenten
Name aus Sonderzeichen (Bsp.: &&
)
steht als Operator zw. erstem und zweitem Argument
zwischen Funktion und Operator umschalten:
in runden Klammern: Operator als Funktion
(&&)::Bool->Bool->Bool
,
(&&) False True
in Backticks: Funktion als Operator
3 `plus` 4
not x = case x of { False -> _; True -> _ }
Alternative Notation (links), Übersetzung (rechts)
not x = case x of
False -> _
True -> _
not x = case x of
{False -> _
;True -> _
}
Abseitsregel (offside rule): wenn das nächste (nicht leere)
Zeichen nach of
kein { ist, werden eingefügt:
{ nach of
;
nach Zeilenschaltung bei gleicher
Einrückung
} nach Zeilenschaltung bei höherer Einrückung
Für die Signatur \(\Sigma=\{f/1,g/3,c/0\}\):
geben Sie ein \(t\in\operatorname{Term}(\Sigma))\) an mit \(t[1]=c\).
Beweisen Sie \(\forall \Sigma: \forall t\in\operatorname{Term}(\Sigma): |t| = |\operatorname{Pos}(t)|\).
Für die Signatur \(\Sigma=\{Z/0, S/1, f/2\}\):
für welche Substitution \(\sigma\) gilt \(f(x,Z) \sigma = f(S(Z),Z)\)?
für dieses \(\sigma\): bestimmen Sie \(f(x,S(x)) \sigma\).
Dabei wurde angewendet:
Abkürzung für Anwendung von 0-stelligen Symbolen: anstatt \(Z()\) schreibe \(Z\). (Vorsicht: dann kann man Variablen nicht mehr von 0-stelligen Symbolen unterscheiden. Man muß dann immer die Signatur explizit angeben oder auf andere Weise vereinbaren, wie man Variablen erkennt, z.B. Buchstaben am Ende das Alphabetes (\(\dots,x,y,\dots\)) sind Variablen, das ist aber riskant)
Für die Deklarationen
-- data Bool = False | True (aus Prelude)
data T = F T | G T T T | C
entscheide/bestimme für jeden der folgenden Ausdrücke:
syntaktisch korrekt?
statisch korrekt?
Resultat (dynamische Semantik)
disjunkt? vollständig?
1. case False of { True -> C }
2. case False of { C -> True }
3. case False of { False -> F F }
4. case G (F C) C (F C) of { G x y z -> F z }
5. case F C of { F (F x) -> False }
6. case F C of { F x -> False ; True -> False }
7. case True of { False -> C ; True -> F C }
8. case True of { False -> C ; False -> F C }
9. case C of { G x y z -> False; F x -> False; C -> True }
Listen von Wahrheitswerten:
data List = Nil | Cons Bool List deriving Prelude.Show
and :: List -> Bool
and l = case l of ...
entsprechend or :: List -> Bool
(Wdhlg.) welche Signatur beschreibt binäre Bäume
(jeder Knoten hat 2 oder 0 Kinder, die Bäume sind; es gibt keine Schlüssel)
geben Sie die dazu äquivalente data
-Deklaration an:
data T = ...
implementieren Sie dafür die Funktionen
size :: T -> Prelude.Int
depth :: T -> Prelude.Int
benutze Prelude.+
(das ist Operator),
Prelude.min, Prelude.max
für Peano-Zahlen data N = Z | S N
implementieren Sie plus, mal, min, max
SS 25: KW 18: Aufgaben 1, 4, 7; KW 19: Aufgaben 5, 6, 8
Arithmetik auf Peano-Zahlen
Für \(R=\{ f(S(x),y) \to f(x, S(y)), f(Z,y) \to y\}\)
bestimme alle \(R\)-Normalformen von \(f(S(Z), S(Z))\).
für \(R_d = R\cup\{ d(x)\to f(x,x) \}\)
bestimme alle \(R_d\)-Normalformen von \(d(d(S(Z)))\).
Bestimme die Signatur \(\Sigma_d\) von \(R_d\).
Bestimme die Menge der Terme aus \(\operatorname{Term}(\Sigma_d)\), die \(R_d\)-Normalformen sind.
Welche Rechenoperationen simulieren die Regeln für \(f\), für \(d\)?
welche Terme haben große Normalformen?
Simulation von Wort-Ersetzung durch Term-Ersetzung.
Abkürzung für mehrfache Anwendung eines einstelligen Symbols: \(A(A(A(A(x)))) = A^4(x)\)
für \(\{ A(B(x)) \to B(A(x)) \}\)
über Signatur \(\{A/1, B/1, E/0\}\):
bestimme Normalform von \(A^k (B^k (E))\)
für \(k=1, 2, 3,\) allgemein.
für \(\{ A(B(x)) \to B(B(A(x))) \}\)
über Signatur \(\{A/1, B/1, E/0\}\):
bestimme Normalform von \(A^k (B (E))\)
für \(k=1, 2, 3,\) allgemein.
für die Signatur \(\{A/2, D/0\}\):
definiere Terme \(t_0=D, t_{i+1} = A(t_i, D)\).
Zeichne \(t_3\). Bestimme \(|t_i|,\operatorname{depth}(t_i)\) .
für \(S=\{ A(A(D,x),y)\to A(x,A(x,y)) \}\)
bestimme \(S\)-Normalform(en), soweit existieren, der Terme \(t_0, t_1, \dots, t_4\).
Geben Sie für \(t_2\) die ersten Ersetzungs-Schritte explizit an.
Normalform von \(t_i\) allgemein.
Für die Deklarationen
-- data Bool = False | True (aus Prelude)
data S = A Bool | B | C S S
entscheide/bestimme für jeden der folgenden Ausdrücke:
syntaktisch korrekt?
Resultat-Typ (statische Semantik)
Resultat-Wert (dynamische Semantik)
Menge der Muster ist: disjunkt? vollständig?
1. case False of { True -> B }
2. case False of { B -> True }
3. case C B B of { A x -> x }
4. case A True of { A x -> False }
5. case A True of { A x -> False ; True -> False }
6. case True of { False -> A ; True -> A False }
7. case True of { False -> B ; False -> A False }
8. case B of { C x y -> False; A x -> x; B -> True }
weitere Beispiele selbst herstellen und dann in der Übung die anderen Teilnehmer fragen.
für selbst definierte Wahrheitswerte: deklarieren, implementieren und testen Sie:
die zweistellige Antivalenz,
die Implikation,
die dreistellige Majoritätsfunktion.
import qualified Prelude
data Bool = False | True deriving Prelude.Show
not :: ...
xor :: ...
...
Definieren Sie die Majorität auf verschiedene Weisen
mit einer Gleichung (evtl. mit case
, evtl.
geschachtelt)
ohne case
(evtl. mehrere
Gleichungen)
mit einer Gleichung ohne Fallunterscheidung, mittels anderer (selbst definierter) Funktionen
für binäre Bäume ohne Schlüssel
data Tree = Leaf | Branch Tree Tree
deklarieren, implementieren und testen Sie ein einstelliges Prädikat über solchen Bäumen, das genau dann wahr ist, wenn das Argument eine ungerade Anzahl von Blättern enthält.
Diese Anzahl nicht ausrechnen, sondern direkt den Wahrheitswert!
in welchen Fällen zeigt der Compiler GHC(i) die Warnung overlapping patterns? https://downloads.haskell.org/ghc/latest/docs/users_guide/using-warnings.html#ghc-flag-Woverlapping-patterns
Desgl. incomplete patterns?
Definieren Sie formal. Vergleichen Sie mit Def. disjunkt und vollständig aus VL.
Geben Sie Beispiele an mit selbst gebauten
data
-Typen.
also nicht die Beispiele aus der Dokumentation - keine Maschinen- oder andere Zahlen (Int, Integer), keine polymorphen Container (Tupel, Listen).
Der Haskell-Standard enthält
data Ordering = LT | EQ | GT
Für diesen Typ gibt es eine Operation (<>)
,
Bsp.
ghci> LT <> GT -- Resultat ist LT
implementieren Sie diese Funktion (unter anderem Namen) durch
eine möglichst kurze Fallunterscheidung.
op x y = case x of ...
ist sie kommutativ? assoziativ?
besitzt die Operation ein linksneutrales Element? ein rechtsneutrales? eine Umkehr-Operation? (ist das Monoid eine Gruppe?)
Testen Sie die Übereinstimmung der Definitionen sowie das Vorliegen der Eigenschaften mit leancheck, Bsp.
ghci> import Test.LeanCheck
ghci> check $ \ (x :: Ordering) -> (x <> x) == x
wie heißt die hier getestete Eigenschaft?
(Nachtrag statische Semantik, TODO: das sollte auf eine separate Folie) Beantworten Sie durch den Haskell-Standard (Language Report):
warum ist der folgende Ausdruck statisch falsch?
-- data Bool = False | True (aus Prelude)
data S = A Bool | B | C S S -- wie oben
case C B (C (A True) B) of { C x (C y x) -> True }
Erläuterungen zur Fehlermeldung siehe https://errors.haskell.org/messages/GHC-10498/, aber Verweis auf Standard fehlt dort, selbst suchen unter https://haskell.org/documentation/.
Welcher Abschnitt erklärt Fallunterscheidung? Dort wird Grammatik-Variable pat verwendet. In welchem anderen Abschnitt stehen deren Regeln? Nach diesen Regeln steht ein Satz, darin ein Begriff (ein Adjektiv) für eine Eigenschaft, die das Muster im Beispiel eben nicht hat.
Für Term-Ersetzungs-Regeln ist es erlaubt, in der logischen Programmierung auch, in der funktionalen üblicherweise nicht, weil die Implementierung des pattern matching dann deutlich teurer wäre. Warum?
gesucht ist für das System
TRS { variables = [ x, y, z ]
, rules =
[ f ( f ( x, y ), z ) -> f ( x, f ( y, z ) )
, f ( x, f ( y, z ) ) -> f ( f ( x, y ), z )
] }
eine Folge von Schritten
von f ( a, f ( f ( b, f ( c, d ) ), e ) )
nach f ( f ( a, f ( b, c ) ), f ( d, e ) )
Lösungs-Ansatz:
( f ( a, f ( f ( b, f ( c, d ) ), e ) )
, [ Step { rule_number = 1 , position = [ ]
, substitution = listToFM
[ (x, a), (z, e), (y, f (b, f (c, d))) ]
} ] )
Programmierer (Software-Ingenieur) muß beweisen, daß Programm (Softwareprodukt) die Spezifikation erfüllt
vgl. (Maschinen)Bau-Ingenieur: Brücke, Flugzeug
vgl. Dijkstra (EWD 1305) zum Verhältnis von Programmieren (Wagen) und Beweisen (Pferd)
für funktionale Programmierung: direkte Entsprechung zw. Konstruktion/Ausführung von Programm und Beweis:
Auswertungs-Schritte: Gleichungskette
Verzweigung (case): Fallunterscheidung
strukturelle Rekursion: vollständige Induktion
verschiedene Formen des Beweises:
Beweis durch hand waving, durch Autorität
formaler Beweis (handschriftlich, LaTeX)
formaler Beweis mit maschineller Prüfung
statische Programm-Eigenschaften:
als Typ-Aussagen formuliert (Bsp:
f x :: Bool
)
und durch Compiler bewiesen
für Eigenschaften, die sich (in Haskell) nicht als Typ
formulieren lassen (Bsp: f x == True
),
Benutzung anderer Notation und Werkzeuge.
wir verwenden CYP (Noschinski et al.)
ausdrucksstärkere Programmiersprachen ist z.B. Agda
data Bool = False | True
not :: Bool -> Bool
not False = True
not True = False
Lemma nnf: not (not False) .=. False
Proof by rewriting not (not False)
(by def not) .=. not True
(by def not) .=. False
QED
vgl. Definition/Autotool-Aufgabe Term-Ersetzung
Lemma nnx: forall x::Bool : not (not x) .=. x
Proof by case analysis on x :: Bool
Case False
Assume XF : x .=. False
Then Proof by rewriting not (not x)
(by XF) .=. not (not False)
...
QED
...
QED
vollständige Menge der Muster in der Fallunterscheidung
Notation ...
für Lücken auch in
Autotool-Aufgaben
Axiome von G. Peano: \(0\in \NN, \forall x:x\in \NN\Rightarrow (1+x)\in\NN\)
realisiert als algebraischer Datentyp
data N = Z -- Null, Zero
| S N -- Nachfolger, Successor
Zahl \(n\in \NN\) dargestellt als \(S^n(Z)\), Bsp: \(2=S(S(Z))\)
Ableitung der Implementierung der Addition
plus :: N -> N -> N
plus x y = case x of Z -> y ; S x' -> ...
benutze Assoziativität \(x+y=(1+x')+y=\dots\)
Bsp: Addition von Peano-Zahlen
Spezifikation:
Typ: plus :: N -> N -> N
Axiome (Bsp): plus
ist kommutativ
Test der Korrektheit durch
Aufzählen einzelner Testfälle
plus(S (S Z))(S Z) == plus(S Z)(S (S Z))
Notieren von Eigenschaften (properties)
plus_comm :: N -> N -> Bool
plus_comm x y = plus x y == plus y x
und automatische typgesteuerte Testdatenerzeugung
Test.LeanCheck.checkFor 10000 plus_comm
Beweis für: Addition von Peano-Zahlen ist assoziativ
zu zeigen ist
plus a (plus b c) == plus (plus a b) c
Beweismethode: Induktion (nach a
)
und Umformen mit Gleichungen (äquiv. zu Implement.)
plus Z y = y
plus (S x') y = S (plus x' y)
Anfang: plus Z (plus b c) == ..
Schritt: plus (S a') (plus b c) ==
== S (plus a' (plus b c)) == ..
Es ist \(\forall t\in\operatorname{Term}(\Sigma): P(t)\) zu zeigen
\(P\) gilt für alle Terme der Signatur \(\Sigma\).
Beweis durch strukturelle Induktion
(IA) Induktions-Anfang:
wir zeigen \(P(t)\) für alle Terme \(t=f()\), d.h., Blätter
(IS) Induktions-Schritt
wir zeigen \(P(t)\) für alle Terme \(t=f(t_1,\ldots,t_n)\) mit \(n\ge 1\),
d.h., innere Knoten
(IV) Induktions-Voraussetzung:
\(P(t_1)\wedge \dots \wedge P(t_n)\), d.h., \(P\) gilt für alle Kinder von \(t\)
(IB) Induktions-Behauptung: \(P(t)\)
gezeigt wird die Implikation IV \(\Rightarrow\) IB.
Lemma plus_assoc : forall a :: N, b :: N, c :: N :
plus a (plus b c) .=. plus (plus a b) c
Proof by induction on a :: N
Case Z
Show : plus Z (plus b c) .=. plus (plus Z b) c
Proof ... QED
Case S a'
Fix a' :: N
Assume IV :
plus a' (plus b c).=. plus (plus a' b) c
Then Show :
plus(S a')(plus b c) .=. plus (plus (S a') b) c
Proof ... QED
QED
ausführliche Notation erforderlich — das ist Absicht
gegeben: Peano-Zahlen und Binärzahlen:
data B = Zero | Even B | Odd B
value :: B -> N
value Zero = Z
value (Even x) = doubleN (value x)
value (Odd x) = S (doubleN (value x))
gesucht: Nachfolgerfunktion für Binärzahlen
succB :: B -> B -- Implementierung ist zu ergänzen
Lemma :
forall b :: B : value (succB b) .=. S (value b)
Renz, Schwarz, Waldmann: Check your Students’ Proofs—with Holes, WFLP 2020,
Bsp: im Induktionsschritt für Beweis von
forall x::N, y::N : plus' x y .=. plus x y
lautet die Ind.-Voraus.
plus' x' y .=. plus x' y
Beweis der Ind.-Behauptung benötigt Umformung von
plus' x' (S y)
. Das erfordert
Proof by induction on x:: N generalizing y::N
ohne generalizing
: \(\forall y : (\forall x : P(x,y))\),
d.h., für jedes außen fixierte \(y\) eine Induktion nach \(x\)
(I.V. muß mit genau diesem \(y\) benutzt werden)
mit generalizing y
: \(\forall x: (\forall y : P(x,y))\),
d.h., für jedes \(x\) wird die Behauptung für alle \(y\) gezeigt.
(I.V. kann mit beliebiger Belegung von \(y\) benutzt werden)
gegeben sind:
data Tree = Leaf | Branch Tree Tree
leaves :: Tree -> N
leaves Leaf = S Z
leaves (Branch l r) = plus (leaves l) (leaves r)
gesucht ist: g :: Tree -> Bool
mit
Lemma : even (leaves t) .=. g t
Proof by induction on t :: Tree
Case Leaf
Show : even (leaves Leaf) .=. g Leaf
Proof by rewriting ... QED
...
QED
Case Branch l r
Fix l :: Tree, r :: Tree
Assume
IH1: g l .=. even (leaves l)
IH2: g r .=. even (leaves r)
Then Show :
g (Branch l r) .=. even (leaves (Branch l r))
Proof by rewriting
...
QED
zwei Teile der Induktionsvoraussetzung (IH1, IH2)
times :: N -> N -> N
times x y = case x of
Z -> _ ; S x' -> _
vervollständigen durch Umformen der Spezifikation,
Bsp. \((1+x')\cdot y = y + x'\cdot y\)
Eigenschaften formulieren, testen (leancheck),
beweisen (auf Papier, mit CYP)
Multiplikation mit 0, mit 1,
Distributivität (mit Plus), Assoziativität, Kommutativität
ähnliche für Potenzierung
vollständige Spezifikation:
forall x :: N, y :: N : min (plus x y) y = y
forall x :: N, y :: N : min x (plus x y) = x
vollständig bedeutet: es gibt nur eine Funktion, die die Spezifikation erfüllt
Definition durch vollständige Fallunterscheidung
min Z Z = _ ; min Z (S y) = _ ; min (S x) Z = _
min (S x) (S y) = S (min x y)
Ü: Beweis, daß diese Imp. die Spez. erfüllt
Ü: desgleichen für Maximum
minus :: N -> N -> N
modifizierte Subtraktion, Bsp: \(5 \ominus 3 = 2, 3 \ominus 5 = 0\)
Spezifikation: eigentlich \(a\ominus b = \max(a-b,0)\),
vollst. Spez. ohne Verwendung von Hilfsfunktionen:
\(\forall a,b\in \NN: (a+b)\ominus b=a\)
\(\forall a,b\in \NN: a\ominus(a+b)=0\)
Implementierung (Muster disjunkt? vollständig?)
minus Z b = _ ; minus a Z = _
minus (S a') (S b') = _
SS 25: 1, 4, 3
Für die Funktion
f :: N -> N
f Z = Z ; f (S x) = S (S (f x))
beweisen Sie (erst auf Papier, dann mit CYP)
f (plus x y) = plus (f x) (f y)
durch Induktion nach x
.
Papier: Verwenden Sie die angegebenen Bezeichnungen für die Beweis-Schritte, geben Sie IA, IV, IB explizit an.
Die übliche Peano-Addition ist
plus Z y = y ; plus (S x) y = S (plus x y)
Eine andere Implementierung der Addition (vgl. früher angegebenes Termersetzungssystem) ist
plus' Z y = y ; plus' (S x) y = plus' x (S y)
Beweisen Sie mit Cyp
forall x :: N, y :: N : plus' x y .=. plus x y
Beweisen Sie dazu als Hilfssatz
forall x :: N, y :: N : plus x (S y) .=. plus (S x) y
In dieser Induktion nach \(x\) müssen Sie das andere Argument \(y\) generalisieren, da es zwischen Induktionsvoraussetzung und Induktionsbehauptung unterschiedlich ist.
Implementieren Sie Peano-Multiplikation und -Potenz.
Formulieren, testen (leancheck) und beweisen (Papier, CYP) Sie einige Eigenschaften.
CYP: formulieren Sie ggf. Hilfssätze als Axiome, d.h., ohne Beweis—aber mit Tests.
Für zweistelliges min (siehe Folie) und max auf \(\NN\):
Geben Sie eine äquivalente vollständige Spezifikation an, die keine Fallunterscheidung benutzt, sondern nur Addition.
Implementieren Sie min und max nur durch Addition und Subtraktion (\(\ominus\)).
testen (optional: und beweisen) Sie, daß Ihre Implementierung die Spezifikation erfüllt
Implementieren Sie nur mit min und max:
den Median von drei Argumenten
(optional) den Median von fünf Argumenten
Geben Sie Tests an (optional: Beweis)
für das TRS \(R=\{A(A(D,x),y)\to A(x,A(x,y))\}\) über der Signatur \(\Sigma=\{D/0,A/2\}\), vgl. frühere Aufgabe,
die Menge (Folge) aller \(R\)-Normalformen ist \(N_0=D, N_1=A(D,N_0), \dots, N_{k+1}=A(D,N_k),\dots\).
warum gibt es keine anderen \(R\)-Normalformen?
Die \(R\)-Normalform von \(A(N_l,N_r)\) ist \(N_k\) mit \(k=2^l+r\).
Geben Sie Beispiele an (auf Papier oder maschinell)
beweisen Sie durch vollständige Induktion nach \(l\).
(Auf Papier, aber mit korrekten Bezeichnungen.)
welches sind die Terme (z.B.: der Größe 11) mit größter Normalform?
a :: T -> T
b :: T -> T
axiom E : forall x :: T : a(b(a(x))) .=. x
Lemma :
forall x :: T : a(b(b(b(a(a(x)))))) .=. b(b(a(x)))
Proof by rewriting a(b(b(b(a(a(x))))))
(by E) .=. _
(by E) .=. _
...
.=. b(b(a(x)))
QED
Das ist ein Modell für schnittstellen-orientierte Programmierung. Der abstrakte Datentyp \(T\) besteht aus Signatur (\(\{a/1, b/1\}\)) und Axiom \(E\), es ist nichts bekannt über tatsächliche Implementierung.
bisher: Fkt. definiert d. Gleichg.
dbl x = plus x x
jetzt: durch Lambda-Term
dbl = \ x -> plus x x
\(\lambda\)-Terme: mit lokalen Namen (hier: \(x\))
Funktionsanwendung: \((\lambda x.B) A \rightarrow B[x:=A]\)
freie Vorkommen von \(x\) in B werden durch \(A\) ersetzt
Funktionen sind Daten (Bsp: Cons dbl Nil
)
\(\lambda\)-Kalkül: Alonzo Church 1936, Henk Barendregt: The Impact of the Lambda Calculus in Logic and Computer Science,
Bull. Symbolic Logic, 1997. https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.25.9348
ist ein Berechnungsmodell, vgl.: Termersetzungssystem, Turingmaschine, Goto-Programme, While-Programme
Syntax: die Menge der Lambda-Terme \(\Lambda\):
jede Variable ist ein Term: \(v\in V \Rightarrow v\in \Lambda\)
Funktionsanwendung (Applikation):
\(F\in\Lambda, A\in\Lambda\Rightarrow @(F,A) \in \Lambda\)
abstrakte Syntax: 2-stell. Operator \(@\),
konkrete Syntax: Leerzeichen (!)
Funktionsdefinition (Abstraktion):
\(v\in V, B\in \Lambda\Rightarrow (\lambda v.B)\in\Lambda\)
Semantik: Relation \(\to_\beta\) auf \(\Lambda\)
(vgl. \(\to_R\) für Termersetzungssystem \(R\))
Das Vorkommen von \(v\in V\) an Position \(p\) in Term \(t\) heißt frei, wenn darüber kein \(\lambda v.\dots\) steht
Def. \(\operatorname{fvar}(t) =\) Menge der in \(t\) frei vorkommenden Variablen (definiere durch strukturelle Induktion)
Eine Variable \(x\) heißt in \(A\) gebunden, falls \(A\) einen Teilausdruck \(\lambda x.B\) enthält.
Def. \(\operatorname{bvar}(t) =\) Menge der in \(t\) gebundenen Variablen
Bsp: \(\operatorname{fvar}(x (\lambda x.\lambda y.x)) =\{x\}\), \(\operatorname{bvar}(x (\lambda x. \lambda y.x)) =\{x,y\}\),
Relation \(\to_\beta\) auf \(\Lambda\) (ein Reduktionsschritt)
Es gilt \(t \to_\beta t'\), falls
\(\exists p\in\operatorname{Pos}(t)\), so daß
\(t[p] = (\lambda x.B)A\) mit \(\operatorname{bvar}(B)\cap\operatorname{fvar}(A)=\emptyset\)
\(t'=t[p:= B[x:=A]]\)
dabei bezeichnet \(B[x:=A]\) ein Kopie von \(B\), bei der jedes freie Vorkommen von \(x\) durch \(A\) ersetzt ist
Ein (Teil-)Ausdruck der Form \((\lambda
x.B)A\) heißt Redex.
(Dort kann weitergerechnet werden.)
Ein Term ohne Redex heißt Normalform.
(Normalformen sind Resultate von Rechnungen.)
int x = 3;
int f(int y) { return x + y; }
int g(int x) { return (x + f(8)); } // g(4) => 15
Darf f(8)
ersetzt werden durch \(f[y:=8]\) ? - Nein:
int x = 3;
int g(int x) { return (x + (x+8)); } // g(4) => 16
Das freie \(x\) in \((x+y)\) wird fälschlich gebunden.
Lösung: lokal umbenennen
int g(int z) { return (z + f(8)); }
dann ist Ersetzung erlaubt
int x = 3;
int g(int z) { return (z + (x+8)); } // g(4) => 15
dieser Ausdruck hat den Wert 15:
(\x->(((\f-> \x->x + f 8) (\y-> x+y)) 4)) 3
Redex \((\lambda f.B)A\) mit \(B=\lambda x.x+f 8\) und \(A=\lambda y.x+y\):
dort keine \(\to_\beta\)-Reduktion, \(\operatorname{bvar}(B)\cap\operatorname{fvar}(A)=\{x\}\neq\emptyset\).
falls wir die Nebenbedingung ignorieren, erhalten wir
(\x->(( \x->x + (\y-> x+y) 8) 4)) 3
mit Wert 16.
dieses Beispiel zeigt, daß die Nebenbedingung semantische Fehler verhindert
falls wir einen Redex \((\lambda x.B)A\) reduzieren möchten, für den \(\operatorname{bvar}(B)\cap\operatorname{fvar}(A)=\emptyset\) nicht gilt,
dann vorher dort die lokale Variable \(x\) umbenennen (hinter dem \(\lambda\) und jedes freie Vorkommen von \(x\) in \(B\))
Relation \(\to_\alpha\) auf \(\Lambda\), beschreibt gebundene Umbenennung einer lokalen Variablen.
Beispiel \(\lambda x.f x z\to_\alpha \lambda y.f y z\).
(\(f\) und \(z\) sind frei, können nicht umbenannt werden)
Definition \(t\to_\alpha t'\):
\(\exists p\in\operatorname{Pos}(t)\), so daß \(t[p]=(\lambda x.B)\)
\(y\notin \operatorname{bvar}(B)\cup\operatorname{fvar}(B)\)
\(t'=t[p:= \lambda y.B[x:=y]]\)
Applikation ist links-assoziativ, Klammern weglassen:
\[(\dots ((F A_1) A_2) \dots A_n) \sim F A_1 A_2 \dots A_n\]
Beispiel: \(((x z)(y z)) \sim x z (y z)\)
Wirkt auch hinter dem Punkt:
\((\lambda x.x x)\) bedeutet \((\lambda x.(x x))\) — und nicht \(((\lambda x.x)x)\)
geschachtelte Abstraktionen unter ein Lambda schreiben:
\[(\lambda x_1.(\lambda x_2. \dots (\lambda x_n.B)\dots)) \sim \lambda x_1 x_2 \dots x_n . B\]
Beispiel: \(\lambda x.\lambda y.\lambda z.B \sim \lambda x y z.B\)
eine einstellige Funktion zweiter Ordnung:
f = \ x -> ( \ y -> ( x*x + y*y ) )
Anwendung dieser Funktion:
(f 3) 4 = ...
Kurzschreibweisen (Klammern weglassen):
f = \ x y -> x * x + y * y ; f 3 4
Übung:
gegeben t = \ f x -> f (f x)
bestimme t succ 0, t t succ 0,
t t t succ 0, t t t t succ 0, ...
Beispiel (Fkt. 1. Ordnung)
Func<int,int> f = (int x) => x*x;
f (7);
Übung (Fkt. 2. Ordnung) — ergänze alle Typen:
??? t = (??? g) => (??? x) => g (g (x));
t (f)(3);
Anwendungen bei Streams, später mehr
(new int[]{3,1,4,1,5,9}).Select(x => x * 2);
(new int[]{3,1,4,1,5,9}).Where(x => x > 3);
Übung: Diskutiere statische/dynamische Semantik von
(new int[]{3,1,4,1,5,9}).Select(x => x > 3);
(new int[]{3,1,4,1,5,9}).Where(x => x * 2);
funktionales Interface (FI): hat genau eine Methode
Lambda-Ausdruck (burger arrow) erzeugt Objekt einer anonymen Klasse, die FI implementiert.
interface I { int foo (int x); }
I f = (x)-> x+1;
System.out.println (f.foo(8));
vordefinierte FIs:
import java.util.function.*;
Function<Integer,Integer> g = (x)-> x*2;
System.out.println (g.apply(8));
Predicate<Integer> p = (x)-> x > 3;
if (p.test(4)) { System.out.println ("foo"); }
$ node
> let f = function (x){return x+3;}
undefined
> f(4)
7
> ((x) => (y) => x+y) (3) (4)
7
> ((f) => (x) => f(f(x))) ((x) => x+1) (0)
2
\(\Lambda\) (Menge der Terme \(=\) abstrakter Syntaxbäume, AST)
realisiert als algebraischer Datentyp
data Term
= Var String | App Term Term | Abs String Term
Richtungen (Applikation: zwei Kinder, L (links) und R (rechts), Abstraktion: ein Kind, O (only)), Positionen:
data Dir = L | R | O ; type Pos = List Dir
Navigation zu Teilterm:
pos :: Term -> List Pos -- alle Positionen
get :: Pos -> Term -> Maybe Term
get Nil t = Just t
get (Cons L p) (App l r) = get _ _
Teilterm-Ersetzung:
put :: Pos -> Term -> Term -> Maybe Term
put (Cons L p) t (App l r) =
case put p t l of Just l' -> Just (App l' r)
Mengen von Variablen:
import qualified Data.Set as S
var :: Term -> S.Set String -- alle Variablen
bvar :: Term -> S.Set String -- alle gebundenen
fvar :: Term -> S.Set String -- alle frei vorkommenden
Ansatz:
bvar :: Term -> S.Set String
bvar t = case t of
Var v -> S.empty ; App l r -> S.union _ _
Abs v b -> S.insert v _
API-Dokumentation:
SS 25: (3 oder 4), 6; falls noch offen: 1 vorige Woche (Induktion Cyp).
bvar
und fvar
implementieren, Testfälle
vorführen (aus Skript und weitere)
pos
, get
und put
implementieren, Testfälle vorführen.
für \(t=\lambda f x . f (f x)\): AST von \(t t S Z\) zeichnen,
Normalform bestimmen: 1. auf Papier, 2. mit ghci
(dabei
\(Z\) und \(S\) als Konstruktoren der Peano-Zahlen)
3. mit JS (node), dabei \(Z=0\) und \(S=\verb|(x=>x+1)|\).
für \(s=\lambda x y z . x z(y z)\) und \(k=\lambda a b.a\):
Auswertung von \(s k k 0\) an der
Tafel (Terme zeichnen, Redexe angeben usw.), in Haskell, in Javascript
(node
),
optional: in C# (csharp
), Java (jshell
),
oder anderer Sprache
Für \(n\in\NN\) bezeichnet (nur für diese Aufgabe, die eckigen Klammern bedeuten oft etwas anderes) \([n]\) den Lambda-Ausdruck \(\lambda f x. f^n(x)\),
dabei ist \(f^n(x)\) die \(n\)-fach iterierte Anwendung von \(f\) auf \(x\),
also \([0]=\lambda f x.x, [1]=\lambda f x. f x, [2]=\lambda f x.f (f x), [3]=\lambda f x.f (f (f x))\) usw.
die Normalform von \([2] s z\) ist \(s (s z)\), das entspricht der Peano-Repräsentation der Zahl 2.
für \(p=\lambda a b f x.a f (b f x)\): bestimmen Sie die Normalform von \(p [2] [1] s z\)
bestimmen Sie die Normalform von \([3] [2] s z\)
zur Folie Falsches Binden lokaler Variablen (wurde in VL übersprungen)
abstrakte Syntax des Ausdrucks zeichnen (unter der Annahme eines zusätzlichen zweistelligen Symbols für Addition),
die Variablen-Bedingung für angegebenen Redex überprüfen,
lokale Umbenennung durchführen,
dann reduzieren bis Normalform (unter der Annahme einer zusätzlichen arithmetischen Regel für Addition)
Gesucht ist eine Ableitung mit genau 4 Reduce-Schritten , die
(\ z -> (\ u -> u z) (\ u v -> z)) ((\ z -> x y) x)
überführt in
(\ u v -> x y) (x y)
Anfang einer Lösung:
Aktueller Term ist
(\ z -> (\ u -> u z) (\ u v -> z)) ((\ z -> x y) x)
Aktueller Schritt ist
Step
{ position = [ 1 ]
, action = Reduce { formal = z, body = x y, argument = x }
}
Teilterm an Position
[ 1 ] ist
(\ z -> x y) x
Resultat der Anwendung auf den Teilterm ist
x y
Ergebnis der Ableitung ist
(\ z -> (\ u -> u z) (\ u v -> z)) (x y)
(Mathematik) Funktion \(f:A\to B\) als Relation zw. Definitionsbereich \(A\) und Wertebereich \(B\)
(Programmiersprache) Typ-Deklaration
f :: A -> B
(Softwaretechnik) der Typ eines Bezeichners ist seine beste Dokumentation— denn sie wird bei jeder Code-Änderung maschinell überprüft.
andere (schlechtere) Varianten: 1. niemals, 2. nur in Entwurfsphase (bei separater Entwurfssprache)
funktionale Programmierung (Funktionen als Daten):
\(A\) und \(B\) können selbst Mengen von Funktionen sein
statische Typisierung: jeder Bezeichner, jeder Ausdruck (im Quelltext) hat einen Typ
dynamische Typisierung: jeder Wert (im Hauptspeicher) hat einen Typ
statische Typisierung verhindert Laufzeit(typ)fehler
\(\Rightarrow\) alle wichtigen Laufzeiteigenschaften sollen als Typ-Aussagen formuliert werden
für statische Typisierung unterscheiden wir:
Typ-Deklaration (der Typ steht sichtbar im Quelltext)
Typ-Inferenz (der Typ wird vom Compiler bestimmt)
flexible (wiederverwendbare) und sichere Software
durch generische Polymorphie (Typ-Argumente, Typ-Abstraktion,
Typ-Applikation)
benutzt diese Typisierungsregeln:
Abstraktion erzeugt Funktions-Typ:
wenn \(x::T_1\) und \(B::T_2\), dann \(\lambda x.B:: T_1\to T_2\)
Applikation benutzt Funktions-Typ:
wenn \(f :: (T_1\to T_2)\) und \(a :: T_1\), dann \(f a:: T_2\)
das für \(f\) deklarierte \(T_1\) muß genau der Typ von \(a\) sein
damit kein sicherer wiederverwendbarer Code möglich:
denn Bibliotheksfunktionen (Bsp. \(f\)) können anwendungsspezifische Typen (Bsp: von \(a\)) nicht kennen
(schlechte) Auswege: keine statische Typisierung
für die Bibliothek (z.B. Object
in
Mittelalter-Java)
für die Sprache (Bsp: Sprachen mit P (nicht Pascal))
oder keine anwendungsspez. Typen (sondern int
)
Der Typ-Pfeil ist rechts-assoziativ:
\(T_1\to T_2\to \dots \to T_n\to T\) bedeutet \((T_1\to(T_2\to \dots\to (T_n\to T)\cdots))\)
das paßt zu den Abkürzungen für mehrstellige Funkt.:
\(\lambda (x::T_1).\lambda (x::T_2).(B::T)\) hat den Typ \((T_1\to (T_2\to T))\),
mit o.g. Abkürzung \(T_1\to T_2\to T\).
Beispiel: wir schreiben
plus :: N -> N -> N; plus x y = case x of ...
a = plus 2 3
es bedeutet
plus :: N -> (N -> N); plus = \ x -> (\ y -> case ...
a = (plus 2) 3
Fkt. mit monomorphem Typ (Bsp):
f :: Bool -> N; f x = S Z ; f False
-- Deklaration, Definition, Anwendung
Fkt. mit polymorphem Typ (Bsp), mit Typ-Abstraktion
import Data.Kind (Type)
g :: forall (t :: Type) . t -> t; g x = x
Typ-Applikation: g @N
ergibt monomorphes
N -> N
dann (Daten-)Applikation: (g @N) Z
Beispiele (ergänze die Typ-Argumente)
g @_ (g @_ False)
,
(g @_ (g @_)) False
import Data.Kind (Type)
-- Typ-Abstraktion:
g :: forall (t :: Type) . t -> t ; g x = x
g @N Z -- Typ-Applikation
in Java
class C { static <A> A id (A x) {return x;}}
C.<Integer>id(9); // Integer = der Box-Typ von int
in C#
class C { public static A id<A> (A x){return x;}}
C.id<int> (9)
(Haskell) in der Typ-Abstraktion:
Deklaration (Quantor) der Typvariablen weglassen
g :: forall (t :: Type) . t -> t ; g x = x
h :: t -> t ; h x = x
(Haskell, Java, C#) unsichtbare Typ-Applikation:
Typ-Argumente werden durch Compiler inferiert
g Z ; C.id(9) ; C.id(9)
wie bei jeder Abkürzung: das kann
nützlich sein
aber auch irreführend
compose :: (b -> c) -> ((a -> b) -> (a -> c))
-- :: (b -> c) -> (a -> b) -> a -> c
compose f g x = f (g x)
diese Funktion in der Standard-Bibliothek:
der Operator .
(Punkt)
flip :: (a -> b -> c) -> b -> a -> c
aus dem Typ folgt schon die Implementierung!
flip f x y = ...
apply f x = f x
das ist der Operator $
(Dollar), benutzt zum Einsparen
von Klammern: f (g (h x)) = f $ g $ h x
denn ($)
ist rechts-assoziativ
Haskell benutzt Algorithmus W von Damas/Milner,
der zu jedem Lambda-Ausdruck \(A\)
bestimmt, ob \(A\) typisierbar ist (ob ein \(T\) exist. mit \(A::T\))
wenn ja, einen allgemeinsten Typ \(T_p\) von \(A\) inferiert
(für jedes \(T\) mit \(A::T\) exists. Subst. \(\sigma\) mit \(T_p\sigma=T\))
Luis Damas, Robin Milner: Principal Type Schemes for Functional Programs, 1982; Roger Hindley: The Principal Type Scheme of an object in Combinatory Logic, 1969;
Mark P. Jones: Typing Haskell in Haskell, 2000
der deklarierte Typ muß Instanz des inferierten Typs sein
deswegen könnte man Typ-Deklarationen weglassen,
sollte man aber nicht (Typ-Dekl. ist Dokumentation)
Aufgabe: bestimme den allgemeinsten Typ von \(\lambda f x. f(f x)\)
Ansatz mit Typvariablen \(f:: t_1, x::t_2\)
betrachte \((f x)\): der Typ von
\(f\) muß ein Funktionstyp sein, also
\(t_1= (t_{11} \to t_{12})\) mit neuen
Variablen \(t_{11},t_{12}\).
Dann gilt \(t_{11}=t_2\) und \((f x):: t_{12}\).
betrachte \(f(f x)\). Wir haben \(f::t_{11}\to t_{12}\) und \((f x)::t_{12}\), also folgt \(t_{11}=t_{12}\). Dann \(f(f x)::t_{12}\).
betrachte \(\lambda x.f(f
x)\).
Aus \(x::t_{12}\) und \(f(fx)::t_{12}\) folgt \(\lambda x.f(fx):: t_{12}\to
t_{12}\).
betrachte \(\lambda f.(\lambda
x.f(fx))\).
Aus \(f::t_{12}\to t_{12}\) und \(\lambda x.f(fx):: t_{12}\to t_{12}\)
folgt \(\lambda fx.f(fx)::(t_{12}\to
t_{12})\to(t_{12}\to t_{12})\)
ist plus
in flip
richtig benutzt?
Ja!
flip :: (a -> b -> c) -> b -> a -> c
data N = Z | S N
plus :: N -> N -> N
plus (S Z) (S (S Z)) ; flip plus (S Z) (S (S Z))
beachte Unterschied zwischen:
Term-Ersetzung: Funktionssymbol \(\to\) Stelligkeit
abstrakter Syntaxbaum: Funktionss. über Argumenten
Lambda-Kalkül: jeder Lambda-Ausdruck beschreibt einstellige Funktion
Simulation mehrstelliger Funktionen wegen
Isomorphie zwischen \((A\times B)\to C\) und \(A \to (B \to C)\)
Übung/Beispiele: die Funktionen curry
,
uncurry
Motivation: was ist der passende Typ für Division?
data N = Z | S N ; divide :: N -> N -> N -- falsch
data Maybe a = Nothing | Just a
divide :: N -> N -> Maybe N -- richtig
divide (S Z) Z = Nothing ; divide Z (S Z) = Just Z
Maybe
ist ein polymorpher algebraischer Datentyp
genauer: ein einstelliger Typkonstruktor (eine Funktion, die aus einem Typ (N) einen Typ (Maybe N) erzeugt)
dessen Datenkonstruktoren sind polymorphe Funktionen
Nothing :: forall (a :: Type) . Maybe a
Just :: forall (a :: Type) . a -> Maybe a
data Maybe a = Nothing | Just a
data Pair a b = Pair a b
Vorsicht: Typkonstruktor (links) und Datenkonstruktor (rechts) heißen gleich, sind aber verschieden
data Either a b = Left a | Right b
der Typ des Datenkonstruktors ist
Left :: forall a b . a -> Either a b
in Beispielen verwendet (monomorph)
data Bool = False | True
, data Unit = Unit
,
data Void
(keine Konstruktoren!)
_ :: Pair (Pair Bool Unit) (Maybe Void)
alle Elt. von
Maybe (Pair Bool (Maybe Bool))
generische Polymorphie entsteht durch allgemeinste Typen von Lambda-Ausdrücken (Bps: \(\lambda x.x\))
generische Polymorphie ist nützlich für flexibel verwendbare Container (Folge von …, Baum von …)
deren Implementierung kann über Element-Typ gar nichts voraussetzen (denn dieser ist all-quantifiziert)
die Steuerung der Implementierung erfolgt dann durch Funktionen als Daten (Bsp: eine Vergleichsfunktion)
die Implementierung ist dann eine Funktion höherer (zweiter) Ordnung
später: implizite Übergabe dieser Funktionen
als Wörterbücher (Methodentabellen) von Typklassen
aber vorher: Rekursionmuster (als Fkt. höherer Ord.)
den allgemeinsten Typ eines Lambda-Ausdrucks bestimmen, Beispiel
compose ::
compose = \ f g -> \ x -> f (g x)
Musterlösung:
wegen g x
muß g :: a -> b
gelten,
dann x :: a
und g x :: b
wegen f (g x)
muß f :: b -> c
gelten,
dann f (g x):: c
dann \ x -> f (g x) :: a -> c
dann
\ f g -> .. :: (b->c) -> (a->b) -> (a->c)
Isomorphie zwischen \((A\times B)\to C\) und \(A \to (B \to C)\):
falls \(A,B,C\) endlich: die Größen beider Mengen ausrechnen und vergleichen
für \(A=B=C=\textsf{Bool}\): ein Element von \((A\times B)\to C\) angeben und das dazugehörige Element von \(A \to (B \to C)\).
begründen, daß es im allgemeinen keine Isomorphie zu \((A\to B)\to C\) gibt (die Größe ausrechnen)
SS 25: 1, 3, 7
für die Lambda-Ausdrücke
\(\lambda f g h x y . f (g x) (h y)\):
\((\lambda x.x x) (\lambda y.y y)\)
\(\lambda x y z . x y (y z)\)
\(\lambda a b . b (\lambda x . x a)\)
AST zeichnen, allgemeinsten Typ bestimmen, falls möglich
(1. von Hand, mit Begründung, 2. mit ghci überprüfen)
für die Ausdrücke
flip
flip flip
flip flip flip
...
jeweils Argumente \(a_1 a_2\dots\)
angeben (passende Anzahl), so daß der Wert von
flip flip ... flip a1 a2 ...
gleich True
ist.
Dabei alle Typargumente für alle flip
explizit
angeben.
in Haskell: Lambda-Ausdrücke mit diesen Typen angeben, falls möglich:
a -> (a -> c) -> c
a -> b
(a -> b) -> (b -> c) -> a -> c
a -> (a -> a) -> a
(a -> b) -> (a -> c) -> (b -> c -> d) -> a -> d
mit ghci überprüfen (Typen anzeigen lassen)
die Ausdrücke dann auch mit passenden Argumenten aufrufen, so daß der
Wert True
ist (oder von einem andern data
-Typ,
jedenfalls keine Funktion)
(einige der) Funktionen compose
, flip
,
curry
, uncurry
in C# (csharp
) und Java (jshell
)
deklarieren (vorher prüfen, ob es die schon gibt) (anhand von
Primärquellen)
und aufrufen (dabei Typ-Argumente explizit angeben)
Den Typ für 2-Tupel aus der jeweiligen Standardbibliothek benutzen.
(Aufgabe verschoben in nächsten Abschnitt)
Typisierung von \(s k k \textsf{False}\) (vgl. Aufgabe vorige Woche) in Java oder C#.
Vorübung: in Haskell, dabei für \(s\) und \(k\) alle Typen deklarieren und alle Typ-Argumente angeben.
i :: a -> a ; i = \ x -> x -- nur für Beispiel
k :: a -> b -> a ; k = \ x y -> x
s :: (a -> b -> c) -> ... ; s = \ x y z -> x z (y z)
i @(Bool -> Bool) (i @Bool) False -- Beispiel
s @(...) (k @...) (k @...) False
in Java und C#: besteht Unterschied zwischen Werten und Methoden: Methoden können polymorph sein, Werte nicht.
Lambda-Ausdrücke sind Werte, also nicht polymorph:
Function<Boolean,Boolean> i = (x) -> x // monomorph
im Bespiel \(s k k \textsf{False}\) muß \(k\) polymorph sein, da es mit zwei verschiedenen Belegungen der Typ-Argumente benutzt wird.
Lösung (d.h., Umweg): eine polymorphe nullstellige Methode schreiben, die den Lambda-Ausdruck liefert.
$ jshell
jshell> class C {
static <A> Function<A,A> i() { return (x)->x; }
// hier s und k ähnlich deklarieren
}
jshell> C.<Function<Boolean,Boolean>>i().apply(C.<Boolean>i()).apply(false)
In C# genauso, aber das apply
entfällt.
$ csharp
csharp> class C { public static Func<A,A> i<A>() { return x=>x; }}
csharp> C.i<Func<Boolean,Boolean>>()(C.i<Boolean>())(false)
(diese Aufgabe erst nach VL hinzugefügt) (für
Unit
,Maybe
,Pair
,
Either
, Bool
wie auf voriger Folie)
Geben Sie eine Bijektion zwischen diesen Typen an
type T1 a b = Either (Either (Maybe a) b) (Either a Unit)
type T2 a b = Either (Pair (Maybe a) Bool) b
f :: forall a b . T1 a b -> T2 a b
f x = case x of ...
g :: forall a b . T2 a b -> T1 a b
g y = case y of ...
Dazu auch autotool-Aufgabe.
gesucht ist ein Ausdruck vom Typ
M Bool
(oder einem allgemeineren Typ)
in der Signatur
{ j :: forall a . M (M a) -> M a
; m :: forall a b . (a -> b) -> M a -> M b
; x :: M Foo
; g :: Bar -> Int
; c :: Foo -> M Bool
; r :: forall a . a -> M a
}
Fill holes (_) (replace with one item)
in the following, so that the claim at the top becomes true.
size_of (A (Maybe _)) == 9 where
{ data Unit = Unit
; data Maybe a = Nothing | Just a
; data G = F Unit
; data A z = I z z
}
Automatisierter Test für die Aufgabe: Bijektion zwischen
T1 a b
und T2 a b
.
Besonderheiten beim Entwurf dieser Aufgabe
geübt werden polymorphe algebraische Datentypen, aber ohne Rekursion, und wiederholt wird Pattern Matching.
die Bijektion wird als Paar von Funktion \(f\) und Umkehrfunktion \(g\) notiert, getestet werden \(f\circ g = \textsf{id}\) und \(g\circ f = \textsf{id}\).
Ü: warum reicht einer dieser Tests nicht aus?
obwohl nur mit einer konkreten Belegung der Typargumente getestet wird, kann die Implementierung diese Information nicht (zum Betrug) verwenden, weil \(f\) und \(g\) mit polymorphem Typ deklariert sind.
import Test.LeanCheck
data Unit = Unit deriving (Show, Eq)
data Pair a b = Pair a b deriving (Show, Eq)
type T1 a b = Either (Either (Maybe a) b) (Either a Unit)
type T2 a b = Either (Pair (Maybe a) Bool) b
f :: forall a b . T1 a b -> T2 a b
f x = case x of
Left xl -> undefined
Right xr -> undefined
g :: forall a b . T2 a b -> T1 a b
g y = case y of
Left yl -> undefined
Right yr -> undefined
prop1 :: forall a b . (Eq a, Eq b) => T1 a b -> Bool
prop1 = \ x -> g (f x) == x
prop2 :: forall a b . (Eq a, Eq b) => T2 a b -> Bool
prop2 = \ y -> f (g y) == y
test = and
[ holds 1000 $ prop1 @Bool @(Maybe Bool)
, holds 1000 $ prop2 @Bool @(Maybe Bool)
]
instance Listable Unit where
tiers = cons0 Unit
instance (Listable a, Listable b) => Listable (Pair a b) where
tiers = cons2 Pair
binäre Bäume mit Schlüssel vom Typ e
data Tree e = Leaf
| Branch (Tree e) e (Tree e)
Branch Leaf True Leaf :: Tree Bool
Branch Leaf (S Z) Leaf :: Tree N
Bezeichungen: Tree
ist ein polymorpher Datentyp,
äquivalent: Tree
ist ein Typkonstruktor
(\(=\) eine Funktion, die Typen auf
einen Typ abbildet)
unterscheide: Tree
ist der Typkonstruktor,
Branch
ist ein Datenkonstruktor (und der heißt nicht
Node
, weil sowohl Leaf als Branch Knoten sind)
einfach (vorwärts) verkettete Listen
data List a = Nil | Cons a (List a)
Anwendung: Bäume mit Knoten beliebiger Stelligkeit, Schlüssel in jedem Knoten
data Tree a = Node a (List (Tree a))
in diesem Typsystem können wir formulieren
den Typ der Schlüssel/Elemente
aber nicht
die Ordnung der Elemente
die Länge/Höhe/Balance der Struktur
dazu benötigt man dependent types, Bsp. in Agda
disjunkte Vereinigung:
data Either a b = Left a | Right b
data Maybe a = Nothing | Just a
Kreuzprodukt: data Pair a b = Pair a b
Haskell-Notation für Produkte:
(Z,True)::(N,Bool)
das Komma (,)
\(=\)
Name des Typ-Konstruktors \(=\) Name
des Daten-Konstruktors
ist gefährlich (wg. Verwechslung), aber nützlich und empfohlen, falls der Typ genau einen Konstruktor hat
Komma-Notation für Produkte mit \(0, 2, 3,\dots\) Kompon.
0 Komponenten \(=\) die
Einermenge: data () = ()
(englische Bezeichnung: the unit type)
data Maybe a = Nothing | Just a
Nothing
drückt das Fehlen eines Wertes aus,
Just x
sein Vorhandensein
äq. Optional<T>
, “may or may not contain a
value”
head :: List a -> Maybe a
head Nil = Nothing; head (Cons x xs) = Just x
die Maybe
-Vermeidung
falscher Typ und Ausnahme (Exception)
head :: List a -> a; head Nil = error "huh"; ...
Maybe T
als Zeiger-auf-\(T\), oder auf nichts
der billion dollar mistake, Algol W, Tony Hoare 1965,
Beispiele:
Spiegelbild einer Liste:
reverse :: forall e . List e -> List e
Verkettung von Listen mit gleichem Elementtyp:
append :: forall e . List e -> List e
-> List e
Knotenreihenfolge eines Binärbaumes:
preorder :: forall e . Bin e -> List e
Def: der Typ einer polymorphen Funktion
beginnt mit All-Quantoren für Typvariablen.
Bsp: Datenkonstruktoren polymorpher Typen.
cons :: forall e . e -> List e -> List e
data List a = Nil | Cons a (List a)
append xs ys = case xs of
Nil -> _
Cons x xs' -> _
Ü: formuliere, teste und beweise: append
ist
assoziativ.
reverse xs = case xs of
Nil -> _
Cons x xs' -> _
Ü: beweise:
forall xs ys : reverse (append xs ys)
== append (reverse ys) (reverse xs)
https://www.dfg.de/resource/blob/289674/ff57cf46c5ca109cb18533b21fba49bd/230921-stellungnahme-praesidium-ki-ai-data.pdf
Die vorige Implementierung von reverse
ist (für
einfach verkettete Listen) nicht effizient (sondern quadratisch,
vgl.
Besser ist Verwendung einer Hilfsfunktion
reverse xs = rev_app xs Nil
mit Spezifikation
rev_app xs ys = append (reverse xs) ys
noch besser ist es, keine Listen zu verwenden https://arxiv.org/abs/1808.08329 (WFLP 2018)
Plan: \(\text{map} f [x_1,x_2,\ldots,x_n] = [f x_1, f x_2, \ldots , f x_n]\)
Typ und Implementierung:
map :: forall a b . (a -> b) -> List a -> List b
map f Nil = Nil
map f (Cons x xs) = Cons (f x) (map f xs)
ist Funktion zweiter Ordnung (denn erstes Arg. ist Fkt.)
Eigenschaften:
map id = id ; map (f . g) = map f . map g
mit
id :: forall a . a -> a ; id = \ x -> x
(.) :: forall a b c . _ ; (.) = \ f g x -> f (g x)
Funktionen \(f,g: A\to B\) sind extensional gleich,
wenn \(\forall x\in A: f x = g x\).
(gleiche Argumente liefern gleiche Resultate)
Test.LeanCheck.Function.List:
areEqualFor 100 (not . not) id
areEqualFor 2 (||) xor -- Vorsicht!
in Cyp: proof by extensionality
Lemma : map id .=. id
Proof by extensionality with xs :: List a
Show : map id xs .=. id xs Proof ... QED
QED
vgl.: \(f,g\) sind intensional gleich: Implementierungen (Lambda-Ausdrücke) stimmen überein, \(f (\leftarrow_\beta\cup \rightarrow_\beta)^* g\)
Gleichheit von Funktionen als Test-Ziel (Spezifikation)
{-# language AllowAmbiguousTypes #-}
prop_map1 :: forall a . (Eq a, Listable a) => Bool
prop_map1 =
areEqualFor @(List a) @(List a) 100 (map id) id
check $ prop_map1 @(Maybe Bool)
Funktionen als (automatisch erzeugte) Test-Daten
import Test.LeanCheck.Function
prop_map2 :: forall a b c
. (Eq c, Listable a, Listable b, Listable c)
=> (b -> c) -> (a -> b) -> Bool
prop_map2 f g =
areEqualfor 100 (map (f . g)) (map f . map g)
checkFor 100 $ prop_map2 @Bool @Bool @Bool
data List e = Nil | Cons e (List e)
data Bin e = Leaf | Branch (Bin e) e (Bin e)
Knotenreihenfolgen
preorder :: forall e . Bin e -> List e
preorder t = case t of ...
entsprechend inorder
,
postorder
und Rekonstruktionsaufgaben
Adressierug von Knoten
data Dir = L | R
get :: Bin e -> List Dir -> Maybe e
positions :: Bin e -> List (List Bool)
Nutzen der statischen Typisierung:
beim Programmieren: Entwurfsfehler werden zu Typfehlern, diese werden zur Entwurfszeit automatisch erkannt \(\Rightarrow\) früher erkannte Fehler lassen sich leichter beheben
beim Ausführen: keine Lauzeit-Typfehler \(\Rightarrow\) keine Typprüfung zur Laufzeit nötig, effiziente Ausführung
Nutzen der Polymorphie:
Flexibilität, nachnutzbarer Code, z.B. Anwender einer Collection-Bibliothek legt Element-Typ fest (Entwickler der Bibliothek kennt den Element-Typ nicht)
weiterhin statische typisiert (Sicherheit, Effizienz)
Aufgabe (Bsp): x :: Either (Maybe ()) (Pair Bool ())
Lösung (Bsp):
der Typ Either a b
hat Konstruktoren
Left a | Right b
.
Die Substitution für die Typvariablen ist
a = Maybe (), b = Pair Bool ()
.
Wähle Konstruktor Right b
.
x = Right y
mit y :: Pair Bool ()
der Typ Pair a b
hat Konstruktor
Pair a b
.
die Substitution für diese Typvariablen ist
a = Bool, b = ()
.
y = Pair p q
mit
p :: Bool, q :: ()
der Typ Bool
hat Konstruktoren
False | True
, wähle p = False
. der Typ
()
hat Konstruktor ()
, also
q=()
Insgesamt x = Right y = Right (Pair False ())
Vorgehen (allgemein)
bestimme den Typkonstruktor
bestimme die Substitution für die Typvariablen
wähle einen Datenkonstruktor
bestimme Anzahl und Typ seiner Argumente
wähle Werte für diese Argumente nach diesem Vorgehen.
Aufgabe (Bsp.) bestimme Typ von x
(erstes Arg. von
get
):
at :: List N -> Tree a -> Maybe a
at p t = case t of
Node f ts -> case p of
Nil -> Just f
Cons x p' -> case get x ts of
Nothing -> Nothing
Just t' -> at p' t'
Lösung:
bestimme das Muster, durch welches x
deklariert
wird.
Lösung: Cons x p' ->
bestimme den Typ diese Musters
Lösung: ist gleich dem Typ der zugehörigen Diskriminante
p
bestimme das Muster, durch das p
deklariert wird
Lösung: at p t =
bestimme den Typ von p
Lösung: durch Vergleich mit Typdeklaration von at
(p
ist das erste Argument) p :: List N
, also
Cons x p' :: List N
, also x :: N
.
Vorgehen zur Typbestimmung eines Namens:
finde die Deklaration (Muster einer Fallunterscheidung oder einer Funktionsdefinition)
bestimme den Typ des Musters (Fallunterscheidung: Typ der Diskriminante, Funktion: deklarierter Typ)
SS 25: (1 oder 5), 2 (oder andere Aufgabe mit leancheck, 8, 10) 3 (oder andere Aufgabe mit einem Beweis, 4)
für die folgenden Datentypen: geben Sie einige Elemente an (ghci), geben Sie die Anzahl aller Elemente an.
Maybe (Maybe Bool)
Either (Bool, ()) (Maybe ())
Foo (Maybe (Foo Bool))
mit
data Foo a = C a | D
stellen Sie (dann in der Übung) ähnliche Aufgaben
geben Sie ein möglichst kleines Programm an, das nur aus data-Deklarationen besteht, und das einen Typ mit 100 (Zusatz: 1000) Elementen definiert.
Diskussion: vergleiche frühere Aufgabe. Ein solcher Typ ist
Maybe (Maybe (Maybe .... (Maybe ()) .. ))
mit insgesamt nur drei Konstruktoren (Nothing, Just,
()
).
Also sollte man zur gerechten Messung der Programmgröße die Anzahl der Konstruktoren und die Größe der Typ-Ausdrücke addieren.
Implementieren Sie die Post-Order Durchquerung von Binärbäumen.
postorder :: Tree a -> List a
(Zusatz: Level-Order. Das ist schwieriger.)
Verwenden Sie nur die in der VL definierten Typen
(data List a = ...
, nicht Prelude.[]
)
und Programmbausteine (case _ of
)
Implementieren Sie die strukturerhaltende Transformation
tmap :: (a -> b) -> Tree a -> Tree b
Testen Sie mit leancheck die Eigenschaft
postorder . tmap f = map f . postorder
Beweisen Sie (auf Papier, Zusatz: mit Cyp)
forall xs . reverse (reverse xs) == xs
Verwenden Sie ggf.
rev (app xs ys) = app (rev ys) (rev xs)
oder andere Hilfssätze ohne Beweis (aber mit Beispielen und Tests)
(zu voriger Woche, Typ ist nicht polymorph)
Definitionen ergänzen; Eigenschaften testen (Einzelfälle, Leancheck), beweisen (Papier oder Cyp)
data Tree = Leaf | Branch Tree Tree
size :: Tree -> N
leaves :: Tree -> N
branches :: Tree -> N
odd :: N -> Bool
Lemma :
size t .=. plus (leaves t) (branches t)
Lemma : odd (size t)
Folien Induktion über Bäume benutzen.
zum Typ Optional<T>
aus JDK (im Vergleich zu
Maybe t
aus Haskell)
mit jshell
(23 oder neuer) im Pool vorführen:
mit of
ein Objekt konstruieren, darauf
isPresent
anwenden
geben Sie den Typ von isPresent
in Haskell an,
Hinweis: :: Maybe t -> _
,
benutzen Sie https://hoogle.haskell.org/ zur Suche nach Funktionen mit diesem Typ (geben Sie den Typ der Funktion ein, nicht den (vermuteten) Namen)
Rufen Sie solche Funktionen auf (ggf. das passende Modul importieren.
Keine neuen Bibliotheken installieren, base
genügt)
geben Sie den Haskell-Typ von orElse
(aus JDK) an.
Implementieren Sie diese Funktion in Haskell.
für diese Deklarationen (die Konstruktor-Namen sind abgekürzt, P(air), L(eaf), B(ranch), damit man in Beispielen nicht so viel tippen muß)
data Pair a b = P a b
data Tree k = L k | B (Tree (Pair k k))
ergänzen Sie den Ausdruck B (B (L _)) :: Tree Bool
Welche Form haben die Elemente von Tree k
allgemein?
Geben Sie weitere Beispiel an.
Bemerkung: Dieser Typkonstruktor Tree
heißt nicht
regulär, denn er ist rekursiv und bei Rekursion wird das
Typ-Argument geändert. Zum Vergleich:
data List t = Nil | Cons t (List t)
ist regulär, denn das
Argument von List
bleibt t
.
Darstellung von Zahlen als binäre Bäume:
import Numeric.Natural
data T = Z | F T T
value :: T -> Natural
value Z = 0
value (F x y) = 2 ^ value x + value y
-- Beispiel:
value (F (F (F Z Z) (F Z Z)) (F Z Z)) = 9
geben Sie Terme mit Werten \(0, 1, 2, 3, 4, 14, 144\) an (falls möglich, jeweils mehrere)
begründen Sie, daß jede natürliche Zahl durch T
darstellbar ist
ein Baum t :: T
heißt vernünftig, wenn
t == Z
oder t = F x y
und
2 ^ value x > value y
und x
und
y
beide vernünftig sind.
begründen Sie, daß jede natürliche Zahl genau eine vernünftige Darstellung besitzt.
Implementieren Sie für vernünftige Bäume:
Vergleiche (eq :: T -> T -> Bool
,
gt :: T -> T -> Bool
), Nachfolger
(s :: T -> T
), Addition
(p :: T -> T -> T
)
Hinweis: mit gen :: Natural -> T
(als Umkehrfunktion
von value
) geht das so:
p x y = gen (value x + value y)
. Gesucht sind Lösungen
ohne Umweg über Natural
.
Ansatz: Klar ist s Z = F Z Z
. Wann ist
s (F x y) = F x (s y)
falsch? Wie kann man das 1.
feststellen, 2. reparieren?
zu Eigenschaften prop1, prop2 von
map :: forall a b . (a -> b) -> List a -> List b
geben Sie jeweils, falls möglich, eine typkorrekte Implementierung von map an,
die weder prop1 noch prop2 erfüllt,
die prop2, aber nicht prop1 erfüllt.
für beliebig verzweigende Bäume
data List a = Nil | Cons a (List a)
data Tree k = Node k (List (Tree k))
einen vollständigen Binärbaum der Höhe 2 vom Typ
Tree Bool
erzeugen.
dabei für die Aufrufe der Datenkonstruktoren die Typ-Argumente angeben
eine (rekursive) Funktion b :: N -> Tree N
schreiben (N
sind die Peano-Zahlen), so daß die Wurzel von
\(b(k)\) den Schlüssel \(k\) und Kinder \([b(k-1),\dots,b(0)]\) hat. Insbesondere hat
\(b(0)\) keine Kinder.
wieviele Knoten hat \(b(k)\)? Beweis?
(Zusatz) wieviele Knoten hat \(b(k)\) im Abstand \(l\) zur Wurzel?
Für Bäume und Positionen
data Bin e = Leaf | Branch (Bin e) e (Bin e)
data List a = Nil | Cons a (List a)
data Dir = L | R ; type Position = List Dir
diese Funktionen implementieren
alle Positionen im Baum
positions :: Bin e -> List Position
der Teilbaum an einer Position (\(t[p]\))
get :: Position -> Bin e -> Maybe (Bin e)
die Operation \(t[p:=s]\)
(Teilbaum ersetzen) put :: Position -> ...
die Eigenschaft \(\forall t, p: p\in\operatorname{Pos}(t) \Rightarrow t[p := t[p]] = t\) mit Leancheck formulieren und testen
data Tree a = Leaf
| Branch (Tree a) a (Tree a)
summe :: Tree N -> N
summe t = case t of
Leaf -> 0
Branch l k r ->
plus (summe l) (plus k (summe r))
preorder :: Tree a -> List a
preorder t = case t of
Leaf -> Nil
Branch l k r ->
Cons k (append (preorder l) (preorder r))
f :: Tree a -> b
f t = case t of
Leaf -> ...
Branch l k r -> ... (f l) k (f r)
dieses Schema ist eine Funktion höherer Ordnung:
fold :: ( ... ) -> ( ... ) -> (Tree a -> b)
fold leaf branch = \ t -> case t of
Leaf -> leaf
Branch l k r ->
branch (fold leaf branch l)
k (fold leaf branch r)
summe = fold Z (\ x y z-> plus x (plus y z))
(bei Aufruf ist dann x=summe l
, y=k
,
z=summe r
)
and :: List Bool -> Bool
and xs = case xs of
Nil -> True ; Cons x xs' -> x && and xs'
length :: List a -> N
length xs = case xs of
Nil -> Z ; Cons x xs' -> S (length xs')
fold :: b -> ( a -> b -> b ) -> List a -> b
fold nil cons xs = case xs of
Nil -> nil
Cons x xs' -> cons x ( fold nil cons xs' )
and = fold True (&&)
length = fold Z ( \ x y -> S y)
data List a = Nil | Cons a (List a)
fold ( nil :: b ) ( cons :: a -> b -> b )
:: List a -> b
Rekursionsmuster anwenden
\(=\) jeden Konstruktor durch eine passende Funktion ersetzen
\(=\) (Konstruktor-)Symbole interpretieren (durch Funktionen)
\(=\) eine Algebra angeben (Signatur \(=\) Menge der Konstruktoren, Universum \(=\) Resultattyp des Musters)
length = fold Z ( \ _ l -> S l )
aus dem Prinzip ein Rekursionsmuster anwenden \(=\) jeden Konstruktor durch eine passende Funktion ersetzen folgt:
Anzahl der Muster-Argumente \(=\) Anzahl der Konstruktoren (plus eins für das Datenargument)
Stelligkeit eines Muster-Argumentes \(=\) Stelligkeit des entsprechenden Konstruktors
Rekursion im Typ \(\Rightarrow\) Rekursion im Muster
(Bsp: zweites Argument von Cons
)
zu jedem rekursiven Datentyp gibt es genau ein passendes Rekursionsmuster
systematisches experimentelles Vorgehen zur Lösung von:
Schreiben Sie Funktion \(f:T \to R\)
als fold
eine Beispiel-Eingabe (\(t\in T\)) notieren
(als Baum zeichnen, Knoten \(=\) Konstruktoren)
für jeden Teilbaum \(s\) von \(t\), der den Typ \(T\) hat:
den Wert von \(f(s)\) in (neben) Wurzel von \(s\) schreiben
daraus Testfälle für die Funktionen ableiten,
die die Konstrukten ersetzen
diese Fkt. sind die Argumente des Rekursionsmusters
Beispiel: reverse :: List a -> List a
fold
hat immer genau ein Daten-Argument
(in welchem die Konstruktoren ersetzt werden)
wie stellt man mehrstellige Funktionen dar? Bsp.
append :: List a -> List a -> List a
1. Lösung: das ist gar nicht zweistellig, sondern
append :: List a -> (List a -> List a)
fold
über 1. Arg., Resultat
:: List a -> List a
app xs ys = (fold nil cons xs) ys -- Spezifikation
app Nil ys = fold nil cons Nil ys = nil ys = ys
app (Cons x xs) ys = fold nil cons (Cons x xs) ys
= cons x (app xs) ys = Cons x (app xs ys)
app = fold (\ ys -> ys) (\ x f -> Cons x . f )
das ist systematisches symbolisches Vorgehen
fold
hat immer genau ein Daten-Argument
(in welchem die Konstruktoren ersetzt werden)
wie stellt man mehrstellige Funktionen dar? Bsp.
append :: List a -> List a -> List a
2. Lösung: fold über 1. Argument bei fixiertem 2. Arg., Resultat:
List a
app xs ys = fold nil cons xs -- Spezifikation
app Nil ys = fold nil cons Nil = nil = ys
app (Cons x xs) ys = fold nil cons (Cons x xs)
= cons x (app xs ys) = Cons x (app xs ys)
app ys = fold ys (\ x zs -> Cons x zs )
das ist systematisches symbolisches Vorgehen
aus dem Typ das Rekursionsschema ableiten:
data N = Z | S N
fold :: ...
fold z s n = case n of
Z -> _
S n' -> _
Bsp: verdoppeln dbl :: N -> N; dbl = fold z s
I.A. dbl Z = Z
und z = fold z s Z
, also
z = 0
I.S. dbl (S x')=2*(1+x')=2+2*x'=2+dbl x'
,
dbl (S x') = s (dlb x')
, also
s a = 2 + a = S (S a))
Lösung: dbl = fold Z (\ a -> S (S a))
Bsp: plus x y = fold z s x
, bestimme
z, s
I.A.: x = Z; plus Z y = y
;
fold z s Z = z
\(\Rightarrow\) z = y
.
I.S.: x = S x'
,
plus (S x') y = S (plus x' y)
,
plus (S x') y=fold z s (S x')=s(fold z s x')
= s (plus x' y)
, also s a = S a
Lösung: plus x y = fold y (\ a -> S a) x
äquivalent, kürzer: plus x y = fold y S x
weil Addition kommutativ ist: äq.
plus x y = plus y x = fold x S y
dann kann man y
auf beiden Seiten weglassen:
plus x = fold x S
Bsp: plus x y = fold z s x y
, äq.
plus x = fold z s x
, bestimme z, s
I.A.: x = Z; plus Z y = y
;
plus Z = \ y -> y
; also
z = \ y -> y
.
I.S.:
x = S x'
;plus (S x') y = S (plus x' y)
;
plus (S x') = \ y -> S (plus x' y)
= \ y -> (S . plus x') y
= (S . plus x')
plus (S x') = fold z s (S x')
= s (fold z s x')
= s (plus x')
also s a = S . a
(mit
(f . g) = \x-> f (g x)
)
L.: plus x = fold (\ y -> y) (\a -> S . a) x
äq. plus = fold id (S .)
(operator section)
Beispiel: data N = Z | S N
,
\(f:\verb|N|\to\verb|Bool|\), \(f(x)=\) \(x\) ist durch 3 teilbar
wende eben beschriebenes Vorgehen an,
stelle fest, daß die durch Testfälle gegebene Spezifikation nicht erfüllbar ist
Beispiel: binäre Bäume mit Schlüssel in Verzweigungsknoten,
\(f:\verb|Tree k|\to \verb|Bool|\),
\(f(t)=\) \(t\) ist höhen-balanciert (erfüllt die AVL-Bedingung)
\(f:\verb|Tree k|\to \verb|Bool|\),
\(f(t)=\) \(t\) ist höhen-balanciert (erfüllt die AVL-Bedingung)
\(f\) ist nicht als fold darstellbar
\(g:\verb|Tree k|\to \verb|Pair Bool N|;\) \(g(t)=(f(t), \verb|height|(t))\)
\(g\) ist als fold darstellbar
dann f t = first (g t)
mit Projektion
first::Pair a b -> a;first (Pair x y) = x
NB: alternativ: punktfreie Notation
f = first . g
der (fehlende) Punkt ist das Argument \(t\), nicht der Kompositions-Operator
(.)
; diese Bezeichnung ist übernommen aus linearer Algebra:
Rechnen mit Vektoren/Matrizen ohne Notation von Indices.
zu jedem (rekursiven) algebraischen Datentyp gibt es genau ein
Rekursionmuster (der übliche Name ist fold
)
das kann systematisch (mechanisch) konstruiert werden
(NB: Konstruktion auch für nicht rekursive Typen anwendbar und nützlich, siehe Aufgabe)
der Grund ist: algebraischer Datentyp \(=\) Signatur \(\Sigma\), Instanz eines Rekursionsmusters \(=\) eine \(\Sigma\)-Algebra
das Hilfsmittel zur Notation ist: Funktion höherer Ordnung
der softwaretechnische Zweck ist:
die Programmablaufsteuerung (Rekursion) wird vom Anwendungsprogramm in die Bibliothek verschoben,
AP wird dadurch einfacher (kürzer, klarer)
jeder Konstruktor durch sich selbst ersetzt,
mit unveränderten Argumenten: identische Abbildung
data List a = Nil | Cons a (List a)
fold :: r -> (a -> r -> r) -> List a -> r
fold Nil Cons (Cons 3 (Cons 5 Nil))
jeder Konstruktor durch sich,
mit transformierten Argumenten v. nicht-rekursiven Typ
fold Nil (\x y -> Cons (not x) y)
(Cons True (Cons False Nil))
struktur-erhaltende Abbildung. Diese heißt map.
map :: (a -> b) -> List a -> List b
das vordefinierte Rekursionsschema über Listen ist:
foldr :: (a -> b -> b) -> b -> ([a] -> b)
length = foldr ( \ x y -> 1 + y ) 0 -- Bsp
falsche Argument-Reihenfolge beachten
(paßt nicht zu Konstruktor-Reihenfolge)
foldr
(fold right) nicht mit foldl
(fold left) verwechseln (foldr ist das richtige, genau Betrachtung
später)
der Typ von foldr
ist tatsächlich allgemeiner (auch
für andere Container anwendbar, genaueres später)
Aufgaben:
append, reverse, concat, inits, tails
data List a = Nil | Cons a (List a)
fold :: r -> (a -> r -> r) -> List a -> r
schreibe mittels fold
(ggf. verwende
map
)
inits, tails :: List a -> List (List a)
inits [1,2,3] = [[],[1],[1,2],[1,2,3]]
tails [1,2,3] = [[1,2,3],[2,3],[3],[]]
filter :: (a -> Bool) -> List a -> List a
filter odd [1,8,2,7,3] = [1,7,3]
partition :: (a -> Bool) -> List a
-> Pair (List a) (List a)
partition odd [1,8,2,7,3]
= Pair [1,7,3] [8,2]
Eigenbau: die Peano-Zahlen (dann sind alle Operationen und Relationen selbst zu programmieren)
Bibliothek: (dann können 0, +, -, .., <, >, ..
benutzt werden — deren Typ aber erst später erklärt wird)
der Typ Natural
(nach
import Numeric.Natural
)
effiziente Repräsentation beleibig großer Zahlen
der Typ Word
(nach
import Data.Word
)
Maschinenzahl, repräsentiert \(\NN\) modulo \(2^\text{Wortbreite}\)
Merksätze
Word
(uint
) riskant, Int
(int
) riskant und falsch (für \(\NN\))
sicher sind nur: beliebig groß oder: mit Überlauf-Prüfung.
SS 25: 1, 3, 4
Wenden Sie die Vorschrift zur Konstruktion des Rekursionsmusters an auf die Typen
Bool, Maybe a, Pair a b, Either a b
Typ und Implementierung
Testfälle (in ghci vorführen)
gibt es diese Funktion bereits? Suchen Sie nach dem Typ mit https://www.haskell.org/hoogle/
implementieren Sie mit dem Rekursionsmuster auf Peano-Zahlen:
g :: N -> Bool
mit \(g
x=\) (\(x\) ist eine gerade
Zahl)
plus, mal, hoch
für den Datentyp der binären Bäume mit Schlüsseln nur in den Blättern
data Tree a
= Leaf a | Branch (Tree a) (Tree a)
aus dem Typ das Rekursionsschema ableiten
fold :: ...
Beispiele (jeweils zunächst den Typ angeben!)
Anzahl der Blätter
Anzahl der Verzweigungsknoten
Summe der Schlüssel
die Tiefe des Baumes
der größte Schlüssel
der Schlüssel links unten (auf Position \(p \in 0^*\))
Beweisen Sie, daß die modifizierte Vorgängerfunktion
pre :: N -> N; pre Z = Z; pre (S x) = x
kein fold ist.
benutze ergänzende Aufgabe in autotool
Diese Funktion pre
kann jedoch als Projektion einer
geeigneten Hilfsfunktion h :: N -> (N,N)
realisiert
werden. Spezifizieren Sie h
und geben Sie eine Darstellung
von h
als fold an.
Implementieren Sie die (modifizierte) Subtraktion
minus
mit fold
(über das erste oder zweite
Argument?) (und pre
)
für Listen aus Standardbibliothek:
die Funktion scanr
an Beispiele vorführen,
mit foldr
implementieren. Ansatz:
scanr f s = foldr (\ x (y:ys) -> _ ) [s]
die Funktion tails
mit scanr
implementieren
Rekursionsmuster auf Eigenbau-Listen:
mit fold
und ohne Rekursion implementieren:
isPrefixOf :: Eq a => List a -> List a -> Bool
siehe Folie Rekursionsmuster für mehrstellige Funktion, fold über das erste Argument. Ansatz:
isPrefixOf = fold (\ ys -> True) (\ x p ys -> case ys of
Nil -> _ ; Cons y ys' -> _)
(Das Typ-Constraint Eq a
bedeutet, daß der
Operator (==) :: a -> a -> Bool
benutzt werden kann,
genaue Erklärung dazu später.)
die Eigenschaft isPrefixOf xs (append xs ys)
mit
Leancheck überprüfen (für xs :: List Bool
)
Diese Eigenschaft abändern und Gegenbeispiele diskutieren.
für isSuffixOf
: 1. konkrete Testfälle, 2. allgemeine
Eigenschaft, 3. Implementierung ohne Rekursion, ohne Fold, aber unter
Benutzung von reverse
angeben.
Rekursionsmuster auf binären Bäumen (Schlüssel in Verzweigungsknoten)
Beweisen Sie, daß
is_search_tree :: Tree N -> Bool
kein fold ist.
diese Funktion kann jedoch als Projektion einer Hilfsfunktion
h :: Tree N -> (Bool, Maybe (N,N))
erhalten werden, die
für Bäume mit nicht-leerer Schlüsselmenge auch noch deren kleinstes und
größtes Element bestimmt. Stellen Sie h
als fold
dar.
Bemerkung zu \(\NN\) beachten. Hier
ist Numeric.Natural
sinnvoll.
für binäre Bäume mit Schlüsseln in den Blättern:
die strukturerhaltende Abbildung definieren
mapt :: (a -> b) -> Tree a -> Tree b
(1) durch direkte Rekursion, (2) durch fold
die Aussage mapt id = id
überprüfen (3) an konkreten
Testfällen, (4) mit leancheck. Dazu
instance Listable a => Listable (Tree a) where
tiers = cons1 Leaf \/ cons2 Branch
für binäre Bäume mit Schlüsseln in den Blättern: eine Funktion
unfold :: (r -> Either k (r, r)) -> r -> Tree k
definieren, so daß
unfold (\ x -> if x == 0 then Left () else Right (x-1,x-1)) h
einen vollständigen Binärbaum der Höhe \(h\) erzeugt.
(+) in jedem Blatt soll die Höhe des gesamten Baumes stehen.
(++) die Schlüssel in den Blättern sollen (von links nach rechts) \([0,1,2 \dots]\) sein
Wie hoch ist, wieviele Blätter hat, wie heißt dieser Baum?
unfold (\ x -> if x > 1 then Left () else Right (x-1,x-2)) h
Ergänzen Sie die Beziehung zwischen fold
und
unfold
: für alle g
, l
,
b
mit passenden Typen gilt:
wenn forall x : case g x of
Left k -> l k == x
Right (y,z) -> _
dann forall x : fold l b (unfold g x) == x
Implementieren Sie tails
mit fold
:
{-# language TypeApplications #-}
module Blueprint where
import Test.LeanCheck
import Prelude hiding (head, map)
data List a = Nil | Cons a (List a) deriving (Eq, Show)
instance Listable a => Listable (List a) where
tiers = cons0 Nil \/ cons2 Cons
fold :: r -> (a -> r -> r) -> List a -> r
fold nil cons xs = case xs of
Nil -> nil
Cons x xs' -> cons x (fold nil cons xs')
tails :: List a -> List (List a)
tails =
fold undefined ( \ x y -> Cons undefined undefined )
prop :: Eq a => List a -> Bool
prop = \ xs -> head (tails xs) == xs
head :: List a -> a
-- partielle Funktion!
head (Cons x xs) = x
test :: Bool
test = and
[ tails (Cons 1 (Cons 2 Nil))
== Cons (Cons 1 (Cons 2 Nil)) (Cons (Cons 2 Nil) (Cons Nil Nil))
, holds 100 (prop @Bool)
]
KW 15: Daten (Terme, algebraische Datentypen)
KW 17: Programme (TRS, Pattern matching)
KW 19: Beweise, Induktion (cyp)
KW 20: Funktionen als Daten (\(\lambda\) - dyn. Semantik)
KW 21: Polymorphie (\(\lambda\) - stat. Semantik)
KW 23: Rekursive polymorphe algebraische Datentypen
KW 24: Rekursionmuster
KW 25: eingeschränkte Polymorphie (Typklassen, Schnittstellen)
KW 26: Strictness, Bedarfsauswertung
KW 27: Datenströme (Iteratoren)
KW 28: Zusammenfassung, Ausblick