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 (vorbehaltlich Beschluß Prüfungsausschuß)
in elektrischer Form (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)
Arbeiten im Pool: Shell, $PATH
, ghci
,
vgl.
$ cabal update
# $HOME/.config/cabal/config editieren (-haddock)
$ cabal install --lib leancheck
$ ghci
GHCi, version 9.8.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_rsa.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]->[Bool]->[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, 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 24: Aufgabe 1
Digitale Selbstverteidigung
Welche Daten gibt Ihr Browser preis?
Starten Sie in einer Konsole den Befehl
ncat --listen --source-port 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?
Ü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)
welcher Typ ist zu erwarten für die Funktion,
(wurde bereits in der Übung behandlelt) die das Spiegelbild einer Zeichenkette berechnet?
die die Liste aller (durch Leerzeichen getrennten) Wörter einer Zeichenkette berechnet?
f "foo bar" = [ "foo", "bar" ]
Suchen Sie nach Funktionen dieses Typs mit https://www.haskell.org/hoogle/, erklären Sie einige der Resultate, welches davon ist das passende, rufen Sie diese Funktion auf (in ghci).
(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 24: 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), ...), ...)
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 24: Aufgaben 1, 4, 5
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?
Zusatz: Geben Sie eine Funktion \(c:\NN\to\NN\) an, für die gilt: \(\forall n\in\NN: \forall t,t'\in\operatorname{Term}(\Sigma_d): |t|\le n\wedge t\to_{R_d}^* t' \Rightarrow |t'|\le c(n)\).
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!
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 24: Aufgaben 2, 4 von hier sowie Aufgabe 4 aus Abschnitt Daten (\(\forall \Sigma:\forall t\in\operatorname{Term}(\Sigma):\operatorname{\textsf {height}}(t)\le |t|-1)\))
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.
Beispiel: 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
Definition:
ein polymorpher Datentyp ist ein Typkonstruktor
(\(=\) eine Funktion, die Typen auf
einen Typ abbildet)
unterscheide: Tree
ist der Typkonstruktor,
Branch
ist ein Datenkonstruktor
Kreuzprodukt: data Pair a b = Pair a b
disjunkte Vereinigung:
data Either a b = Left a | Right b
data Maybe a = Nothing | Just a
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)
binäre Bäume (Schlüssel in der Verzweigungsknoten)
data Bin a = Leaf
| Branch (Bin a) a (Bin a)
einfach (vorwärts) verkettete Listen
data List a = Nil
| Cons a (List a)
Bäume mit Knoten beliebiger Stelligkeit, Schlüssel in jedem Knoten
data Tree a = Node a (List (Tree a))
data Maybe a = Nothing | Just a
Nothing
drückt das Fehlen eines Wertes aus,
Just x
sein Vorhandensein
head :: List a -> Maybe a
head Nil = Nothing; head (Cons x xs) = Just x
andere Ausdrucksmöglichkeiten (in anderen Sprachen)
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,
Optional<T>
, “may or may not contain a
value”
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.
data List e = Nil | Cons e (List e)
List
ist ein Typkonstruktor
List e
ist ein polymorpher Typ
(ein Typ-Ausdruck mit Typ-Variablen)
List Bool
ist ein monomorpher Typ
(entsteht durch Instantiierung: Substitution der Typ-Variablen durch Typen)
polymorphe Funktion:
reverse:: forall e . List e -> List e
monomorphe Funktion: xor:: List Bool -> Bool
polymorphe Konstante: Nil::forall 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)
Bsp: homogene Listen
data List a = Nil | Cons a (List a)
Aufgabe: implementiere maximum :: List N -> N
Spezifikation:
maximum (Cons x1 Nil) = x1
maximum (append xs ys) = max (maximum xs) (maximum ys)
substitutiere xs = Nil
, erhalte
maximum (append Nil ys) = maximum ys
= max (maximum Nil) (maximum ys)
d.h. maximum Nil
sollte das neutrale Element für
max
(auf natürlichen Zahlen) sein, also 0
(geschrieben Z
).
substitutiere xs = Cons x1 Nil
, erhalte
maximum (append (Cons x1 Nil) ys)
= maximum (Cons x1 ys)
= max (maximum (Cons x1 Nil)) (maximum ys)
= max x1 (maximum ys)
Damit kann der aus dem Typ abgeleitete Quelltext
maximum :: List N -> N
maximum xs = case xs of
Nil -> _
Cons x xs' -> _
ergänzt werden.
Vorsicht: für min, minimum
funktioniert das nicht so,
denn min
hat für N
kein neutrales
Element.
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)
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 (False
\(=\) links, True
\(=\) rechts)
get :: Bin e -> List Bool -> Maybe e
positions :: Bin e -> List (List Bool)
Def: dynamische Typisierung:
die Daten (zur Laufzeit des Programms, im Hauptspeicher) haben einen Typ
Def: statische Typisierung:
Bezeichner, Ausdrücke (im Quelltext) haben einen Typ,
dieser wird zur Übersetzungszeit (d.h., ohne Programmausführung) bestimmt, so daß:
für jede Ausführung des Programms gilt:
der statische Typ eines Ausdrucks ist gleich dem dynamischen Typ seines Wertes
der dynamische Typ muß deswegen zur Laufzeit nicht repräsentiert werden (das spart Platz und Zeit)
Javascript
function f (x) {
if (x > 0) {
return function () { return 42; }
} else { return "foobar"; } } }
Dann: Auswertung von f(1)()
ergibt 42,
Auswertung von f(0)()
ergibt
Laufzeit-Typfehler.
im entsprechenden Haskell-Programm ist bereits die Definition von \(f\) statisch falsch
f x = case x > 0 of
True -> \ () -> 42
False -> "foobar"
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)
gleichzeitig bleibt statische Typsicherheit erhalten
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)
Geben Sie alle Elemente dieser Datentypen an:
Maybe ()
Maybe (Bool, Maybe ())
Either ((),Bool) (Maybe (Maybe Bool))
Operationen auf Listen:
append, reverse, rev_app
Operationen auf Bäumen:
preorder, inorder
get, (positions)
SS 24: für KW 19: 1, 2, 5; für KW 20 (Zusatz) 3, 6, 7.
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.
(Zusatz: Level-Order. Das ist schwieriger.)
Verwenden Sie nur die in der VL definierten Typen
(data List a = ...
, nicht Prelude.[]
)
und Programmbausteine (case _ of
)
Geben Sie einen Algorithmus zur Lösung der Rekonstruktions-Aufgabe (preorder, postorder) an.
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-ea) 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?
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 24: 6, (3 oder 4 oder 5); Zusatz: 1 (diese), 7 (vorige Woche) (Nachfolger und Plus für Zahlen als Binärbäume)
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=(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\) 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 durch anderes Beispiel ersetzt):
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)
(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
flexible (wiederverwendbare) und sichere Software
durch generische Polymorphie (Typ-Argumente)
für statische Typisierung unterscheiden wir:
Typ-Deklaration (der Typ steht sichtbar im Quelltext)
Typ-Inferenz (der Typ wird vom Compiler bestimmt)
C#/Java: polym. deklariert, ML/Haskell: polym. inferiert
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
g :: forall (t :: Type) . t -> t; g x = x
Typ-Applikation: g @N
ergibt monomorphes
N -> N
dann (Daten-)Applikation: (g @N) Z
Beispiele: g @Bool (g @Bool False)
data List a = Nil | Cons a (List a)
Cons :: forall (a :: Type) . a -> List a -> List a
Cons @N Z (Nil @N)
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
Haskell-Notation für Listen:
data List a = Nil | Cons a (List a)
data [a] = [] | a : [a]
Verarbeitung von Listen:
filter :: (a -> Bool) -> [a] -> [a]
takeWhile :: (a -> Bool) -> [a] -> [a]
partition :: (a -> Bool) -> [a] -> ([a],[a])
Vergleichen, Ordnen:
nubBy :: (a -> a -> Bool) -> [a] -> [a]
data Ordering = LT | EQ | GT
minimumBy
:: (a -> a -> Ordering) -> [a] -> a
When You Should Use Lists in Haskell (Mostly, You Should Not) https://arxiv.org/abs/1808.08329
Folgen, repräsentiert als balancierte Bäume:
module Data.Sequence where
data Seq a = ...
-- keine sichtbaren Konstruktoren!
fromList :: [a] -> Seq a
filter :: (a -> Bool) -> Seq a -> Seq a
takeWhile :: (a -> Bool) -> Seq a -> Seq a
Anwendung:
import qualified Data.Sequence as Q
xs = Q.fromList [1, 4, 9, 16]
ys = Q.filter (\x -> 0 == mod x 2) xs
Mengen, repräsentiert als balancierte Such-Bäume:
module Data.Set where
data Set a = ...
-- keine sichtbaren Konstruktoren!
fromList :: Ord a => [a] -> Set a
filter :: Ord a => (a -> Bool) -> Set a -> Set a
das Typ-Constraint Ord a
schränkt die
Polymorphie ein (der Typ, durch den die Typ-Variable a
instantiiert wird, muß eine Vergleichsmethode haben)
Anwendung:
import qualified Data.Set as S
xs = S.fromList [1, 4, 9, 16]
ys = S.filter (\x -> 0 == mod x 2) xs
C# und Java haben: statische Typisierung, Polymorphie
(Inferenz für Typ-Argum., nicht für allgemeinsten Typ),
Lambda-Ausdrücke (anonyme Funktionen), aber
polymorphe Unterprogramme: nur als Methoden,
Funktione als Daten: nur als Lambdas (Objekte)
deren Typ kann nicht inferiert werden (für
var
)
und keine neuen Typvariablen einführen
Func<int,Func<bool,int>> k = x => y => x;
class C{public static Func<B,A> k<A,B>(A x)
{return y => x; }}
import java.util.function.*;
Function<Integer,Function<Boolean,Integer>>
k = (x) -> (y) -> x;
k.apply(0).apply(true)
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)
Implementierung von takeWhile
takeWhile :: (a -> Bool) -> List a -> List a
takeWhile p xs = case xs of
Nil -> Nil
Cons x xs' -> case p x of
False -> Nil
True -> Cons x (takeWhile p xs')
(Vorgriff) eigenschaftsbasiertes Testen mit
(Rudy Matela, 2017). Zu dem (inferierten) Typ der Eigenschaft (Funktion nach Bool) werden Test-Argumente automatisch erzeugt
Installation und Benutzung
cabal install --lib leancheck
ghci> import Test.LeanCheck
ghci> check $ \ x y -> not (x && y) == not x || not y
+++ OK, passed 4 tests (exhausted).
Erzeugung von Test-Funktionen, Festlegung von Typen
import Test.LeanCheck
import Test.LeanCheck.Function
check $ \ (p :: Bool -> Bool) xs ->
takeWhile p xs ++ dropWhile p xs == xs
SS 24: 1, 3, 5.
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.
für Data.List
, Data.Sequence
die Funktionen partition
, span
,
inits
, tails
aufrufen (in ghci Beispiele vorführen), dabei anwenden auf eine Liste/Folge von Bool
die API-Dokumentation aufsuchen (https://hackage.haskell.org/)
einige der dort angegebenen Eigenschaften als Leancheck-Property überprüfen, Beispiel:
$ ghci -package containers -package leancheck
ghci> import qualified Data.Sequence as Q
ghci> import Test.LeanCheck
ghci> check $ \ (xs :: [Bool]) -> Q.reverse (Q.fromList xs) == Q.fromList (reverse xs)
(vgl. ähnliche Aufgabe im autotool)
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)
(autotool) Typisierung im Lambda-Kalkül
(autotool) dropWhile
implementieren und beweisen
xs=append (takeWhile p xs) (dropWhile p xs)
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)
Lückentext (undefined
ersetzen)
{-# language TypeApplications #-}
module Blueprint where
import Prelude hiding (takeWhile, dropWhile)
import Test.LeanCheck
import Test.LeanCheck.Function
data List a = Nil | Cons a (List a)
deriving (Eq, Show)
instance Listable a => Listable (List a) where
tiers = cons0 Nil \/ cons2 Cons
takeWhile :: (a -> Bool) -> List a -> List a
takeWhile p xs = case xs of
Nil -> Nil
Cons x xs' -> case p x of
False -> Nil
True -> Cons x (takeWhile p xs')
dropWhile :: (a -> Bool) -> List a -> List a
dropWhile p xs = case xs of
Nil -> Nil
Cons x xs' -> case p x of
False -> undefined
True -> undefined
append :: List a -> List a -> List a
append Nil ys = ys
append (Cons x xs') ys = Cons x (append xs' ys)
prop :: Eq a => (a -> Bool) -> List a -> Bool
prop = \ p xs -> append (takeWhile p xs) (dropWhile p xs) == xs
test = holds 200 (prop @Bool)
-- Gegenbeispiele sichtbar mit: checkFor 200 (prop @Bool)
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
SS 24: 1, 3, wenn Zeit ist: 2, aber nur ist gerade.
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^*\))
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)
]
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
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 = fold nil cons -- Ansatz
app Nil ys = fold nil cons Nil ys = nil ys = ys
app (Cons x xs) ys = fold nil cons (Cons x xs)
= cons x (app xs ys) = Cons x (app xs ys)
app = fold (\ ys -> 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]
Rekursionsmuster foldr
für Listen benutzen (filter,
takeWhile, append, reverse, concat, inits, tails)
Rekursionmuster für Peano-Zahlen hinschreiben und benutzen (plus, mal, hoch, Nachfolger, Vorgänger, minus)
Rekursionmuster für binäre Bäume mit Schlüsseln nur in den Blättern hinschreiben und benutzen
Rekursionmuster für binäre Bäume mit Schlüsseln nur in den Verzweigungsknoten benutzen für rekursionslose Programme für:
Anzahl der Branch-Knoten ist ungerade (nicht zählen!)
Baum (Tree a
) erfüllt die AVL-Bedingung
Hinweis: als Projektion auf die erste Komponente eines
fold
, das Paar von Bool
(ist AVL-Baum) und
N
(Höhe) berechnet.
Baum (Tree N
) ist Suchbaum (ohne
inorder
)
Hinweis: als Projektion. Bestimme geeignete Hilfsdaten.
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 24: 1, 2, optional 6
Beweisen Sie, daß die modifizierte Vorgängerfunktion
pre :: N -> N; pre Z = Z; pre (S x) = x
kein fold ist.
ggf. ergänzende Aufgaben in autotool (nicht vorführen):
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 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
abstrakter Datentyp (ADT) \(=\) Signatur (Menge von Funktionsdeklarationen \(=\) Name und Typ) \(+\) Axiome
konkreter Datentyp \(=\) Algebra \(=\) Menge von Funktionsimplementierungen
Haskell:
class C; data T; instance C T where ...
OO ähnlich:
interface C; class T implements C {...}
Bsp. Klasse der Typen mit totaler Ordnung
class Ord a
,
interface Comparable<E>
der abstrakte Datentyp totale Ordnung besteht aus
Signatur: die Vergleichsfunktion \((\le)\)
Axiomen: \((\le)\) ist Halbordnung (reflexiv, …), linear (…)
sortBy :: (a -> a -> Ordering) -> [a] -> [a]
sortBy ( \ x y -> ... ) [False, True, False]
Kann mit Typklassen so formuliert werden:
class Ord a where compare :: a -> a -> Ordering
sort :: Ord a => [a] -> [a]
instance Ord Bool where compare x y = ...
sort [False, True, False]
sort
hat eingeschränkt polymorphen Typ
(Belegung der Typvariable a
ist eingeschränkt)
die Einschränkung (das Typconstraint Ord a
)
bedeutet: der Typ (die Menge) a
ist total
geordnet.
das Constraint wird in ein zusätzliches Argument (eine Funktion) übersetzt. Entspricht OO-Methodentabelle.
Realisierung von Mengen durch Suchbäume:
die Polymorphie im Element/Schlüsseltyp muß eingeschränkt werden: der Typ muß total geordnet sein
import Data.Set -- aus Bibliothek: containers
fromList :: Ord a => [a] -> Set a
insert :: Ord a => a -> Set a -> Set a
Realisierung von Mengen durch Hashtabellen:
…muß Hashfunktion und Gleichheitstest besitzen
import Data.HashSet -- Bib.: unordered-containers
fromList :: (Eq a,Hashable a)=>[a]->HashSet a
insert::(Eq a,Hashable a)=>a->HashSet a->HashSet a
Typklasse/Schnittstelle
class Show a where show :: a -> String
interface Show { String show (); }
Instanzen/Implementierungen
data A = A1 ; instance Show A where ..
class A implements Show { .. }
entspr. für
B
in Java ist Show
ein Typ:
static String showList(List<Show> xs) { .. }
showList (Arrays.asList (new A(),new B()))
in Haskell ist Show
ein Typconstraint und kein Typ:
showList :: Show a => List a -> String
showList [A1,B1]
ist Typfehler
Haskell:
f :: (Constr1, ..) => t1 -> t2 -> .. -> res
Definition f par1 par2 .. = ..
wird statisch
übersetzt in f dict1 .. par1 par2 .. = ...
Aufruf f arg1 arg2 ..
wird statisch übersetzt
in f dict1 .. arg1 arg2 ..
Java:
inter I { .. f (T2 par2 ) }; T1 implements I;
bei Aufruf arg1.f(arg2)
wird Methodentabelle des
Laufzeittyps von arg1
benutzt (dynamic
dispatch)
dyn. disp. in Haskell stattdessen durch pattern matching
Für
class Eq a where (==) :: a -> a -> Bool
Vergleichen von Listen elementweise:
Für jeden Typ a
gilt: wenn a in Eq,
dann [a] in Eq:
instance Eq a => Eq [a] where
l == r = case l of
[] -> case r of
[] -> True ; y : ys -> False
x : xs -> case r of
[] -> False
y : ys -> (x == y) && ( xs == ys )
Übung: instance Ord a => Ord [a] where ...
(lexikografischer Vergleich)
class Show a where show :: a -> String -- Typklasse
-- eine konkrete Instanz
instance Show Bool where
show False = "False" ; show True = "True"
-- eingeschränkte generische Instanzen:
instance Show a => Show [a] where
show xs = brackets $ concat
$ intersperse "," $ map show xs
instance (Show a, Show b) => Show (a,b) where ...
-- Anwendung:
show ([True,False],True) = "([True,False],True)"
Wörterbuch für instance Show ([Bool],Bool)
wird vom Compiler konstruiert aus der konkreten Inst. durch Anwendung der generischen Inst.-Regeln
Beispiel: Eq ist Superklasse für Ord:
class Eq a where (==) :: a -> a -> Bool
class Eq a => Ord a where (<=) :: a -> a -> Bool
Auswirkungen:
eine instance Ord T
ist nur gültig, wenn auch
instance Eq T
deklariert wird
wenn Ord a
, dann gilt auch Eq a
, d.h.,
Methoden von Eq
können ebenfalls benutzt werden für Typ
a
in einer Klassendeklaration: zu einer Methodendeklaration (Typ) kann eine Implementierung angegeben werden,
diese wird dann benutzt, wenn in der Instanz-Deklaration keine Implementierung angegeben wird
Beispiel
class C a where m :: a -> String ; m x = "foo"
data T = T
instance C T -- hier keine Impl. von m
m T ==> "foo"
dazu Aufgabe: Default-Impl. der Methoden von
Ord
Literal \(=\) Bezeichner für feststehendes Datum
(Gegensatz: Name in Muster: Bedeutung erst zur Laufzeit durch pattern match festgelegt)
data List a = Nil | Cons a (List a)
Nil
(Konstruktor) hat polymorphen Typ
es gilt Nil :: forall (a::Type) . List a
Zahl-Literale haben eingeschränkt polymorphen Typ
class Num a where { (+) :: a->a->a; ..}
instance Num Integer where { ... }
instance Num Natural where { ... }
42 :: forall (a :: Type) . Num a => a
Rudy Matela: LeanCheck: enumerative testing of higher-order properties, Kap. 3 in Diss. (Univ. York, 2017) https://matela.com.br/paper/rudy-phd-thesis-2017.pdf
Testen von universellen Eigenschaften (\(\forall a\in A : \forall b\in B: p ~ a ~ b\))
automatische Generierung der Testdaten …
…aus dem Typ von \(p\)
…mittels generischer Instanzen
beruht auf: Koen Classen and John Hughes: Quickcheck: a lightweight tool for random testing of Haskell programs, ICFP 2000, (“most influential paper award”, 2010)
assoc :: forall a . Eq a =>(a->a->a)->a->a->a->Bool
assoc op = \ a b c -> op a (op b c)==op (op a b) c
main = check (assoc @[Bool] (++))
dabei werden benutzt (in vereinfachter Darstellung)
class Testable p where check :: p -> Bericht
Instanzen sind alle Typen, die testbare Eigenschaften repräsentieren
instance Testable Bool where
check False = "falsch"; check True = "OK"
type Tiers a = [[a]]
class Listable a where tiers :: Tiers a
Instanzen sind alle Typen, die sich aufzählen lassen (Schicht (tier) \(i\): Elemente der Größe \(i\))
instance Listable Bool where tiers = [[False,True]]
warum geht eigentlich beides (einstellig, zweistellig)
check $ \ x -> x || not x
check $ \ x y -> not (x && y) == not x || not y
weil gilt
instance Testable (Bool -> Bool)
und
instance Testable (Bool -> (Bool -> Bool))
das wird vom Compiler abgeleitet (inferiert) aus:
instance Listable Bool ...; instance Testable Bool ...
instance (Listable a, Testable b)
=> Testable (a -> b) where { ... }
das ist eine (eingeschränkte) Form der logischen Programmierung (auf Typ-Ebene, zur Compile-Zeit)
in jedem Inferenz-Schritt wird Code erzeugt
(das jeweils passende Wörterbuch wird eingesetzt)
type Tiers a = [[a]]
class Listable a where tiers :: Tiers a
instance Listable Int where ...
instance Listable a => Listable [a] where ...
data Result
= Result { args :: [String], res :: Bool }
class Testable a where
results :: a -> Tiers Result // orig: resultiers
instance Testable Bool ...
instance (Listable a, Show a, Testable b)
=> Testable (a -> b) ...
union :: Tiers a -> Tiers a -> Tiers a //orig: (\/)
mapT :: (a -> b) -> Tiers a -> Tiers b
concatT :: Tiers (Tiers a) -> Tiers a
cons2 :: (Listable a, Listable b)
=> (a -> b -> c) -> Tiers c
Interface \(\approx\) einstellige Typklasse \(=\) abstrakter Datentyp
Methodentabelle \(\approx\) Wörterbuch
Unterschiede dabei: statischer/dynamischer dispatch
Typklassen gestatten darüberhinaus:
einfachere Behandlg. von Fkt. mit \(>1\) Argument
class Ord t where compare :: t -> t -> Ordering
(sonst: vgl. T implements Comparable<T>
)
Konstruktion von Wörterbüchern durch Compiler
Typkonstruktorklassen, Bsp: Container
class Foldable c where toList:: c a -> [a]
;
data Tree a = ..
;
instance Foldable Tree
mehrstellige Typconstraints:
class Autotool problem solution where
SS 24: (1 oder 2), 5, optional: 4, 6
diese Aufgabe für selbst definierten Datentyp T
, mit
Typklassen Eq
und Ord
aus der
Standardbibliothek:
Definieren Sie für data T = T Bool Bool Bool
instance Eq T
als komponentenweise
Gleichheit
instance Ord T
als lexikografisches Produkt der
Standard-Ordnung auf Bool (zitieren Sie zunächst die mathematische
Definition, z.B. aus der VL Modellierung)
Formulieren Sie allgemein (d.h., polymorph) als
leancheck-Eigenschaft, daß die Relation (<=)
transitiv
antisymmetrisch
ist und prüfen Sie das für Integer
und für
T
.
instance Listable T where tiers = cons3 T
leq_is_transitiv :: Ord a => a -> a -> a -> Bool
leq_is_transitiv = \ x y z -> _
check (leq_is_transitiv @T)
diese Aufgabe für selbst definierten Datentyp T
, mit
Typklassen Semigroup
und Monoid
aus der
Standardbibliothek:
Definieren Sie für data T = T Bool Bool Bool
instance Semigroup T
als komponentenweise
Konjunktion
instance Monoid T
als dazu passendes neutrales
Element
Formulieren Sie die Monoid-Axiome allgemein (d.h., polymorph) als
Eigenschaft in Leancheck und überprüfen Sie diese für
T
.
Geben Sie eine andere korrekte Monoid-Struktur für T
an,
die nicht kommutativ ist. Testen Sie diese Eigenschaft (leancheck soll
das Gegenbeispiel finden).
für Peano-Zahlen: deklarieren Sie instance Num N
und
implementieren Sie dafür (nur) (+)
und (*)
(mit den bekannten Definitionen als fold)
Test: S (S Z) * S (S (S Z))
und: S (S (S Z)) ^ 3
(warum funktioniert das?)
Implementieren Sie fromInteger
(rekursiv, mit if x > 0 then ... else ...
)
Test (polymorphes Literal) 42 :: N
Modul Data.Map
aus containers, Modul
Data.HashMap
aus unordered-containers:
warum hat fromList
ein Typconstraint (oder sogar
zwei) für den Schlüsseltyp k
, aber nicht für den Werttyp
a
?
Erläutern Sie Typconstraints (oder deren Fehlen) für
singleton
für Data.Map:
warum haben die Typen von unionWith
und
intersectionWith
unterschiedliche Anzahl von
Typvariablen?
Erläutern Sie Typconstraints (oder deren Fehlen) für
singleton
, fromAscList
,
fromDistinctAscList
Definieren Sie einen Typ data K = ... deriving Eq
.
Implementieren Sie instance Ord K
so, daß die Relation
(<=)
keine totale Ordnung ist.
Zeigen Sie durch Beispiele, daß dann M.Map K v
nicht
funktioniert. Z.B.: mit M.insert
eingefügter Schlüssel
nicht in M.toList
zu sehen, sichtbarer Schlüssel nicht
gefunden mit M.lookup
.
für instance Semigroup Ordering
,
instance Monoid Ordering
aus der Standardbibliothek:
Bestimmen Sie die Wertetabelle von (<>)
.
Überprüfen Sie (mit leancheck) die Axiome von Semigroup und Monoid. Ist die Verknüpfung kommutativ?
Für den Datentyp data Pair a b = Pair a b
:
implementieren Sie den lexikografischen Vergleich
instance (Ord a, Ord b) => Ord (Pair a b) where
compare (Pair px py) (Pair qx qy) = _
mit Fallunterscheidungen
ohne Fallunterscheidung, aber mit
(<>)
Im Haskell-Standard (wo genau?) steht
class (Eq a) => Ord a where
compare :: a -> a -> Ordering
(<), (<=), (>=), (>) :: a -> a -> Bool
compare x y =
if x == y then EQ else
if x <= y then LT else GT
x <= y = compare x y /= GT
x < y = compare x y == LT
x >= y = compare x y /= LT
x > y = compare x y == GT
nach den Deklaratioen
data T = A | B deriving Eq
instance Ord T where
{ B <= A = False ; _ <= _ = True }
Beschreiben Sie die Auswertung von A > B
(Schritt für
Schritt bis zum Resultat: welche Gleichungen werden benutzt?)
nach den Deklarationen
data S = C | D deriving Eq
instance Ord S -- keine Implementierung!
beschreiben und begründen Sie die Rechnungen für
C < C
C < D
OO: eine Mode-Erscheinung der 80er; Objekte und Klassen zuerst in Simula (1968), Smalltalk (1980)
realisiert nützliche (ältere) Ideen: Trennung von abstraktem Datentyp (Schnittstelle, interface) und konkretem Datentyp (Implementierung, Klasse)
erschwert Realisierung anderer (alter) nützlicher Ideen (algebraischer Datentyp, Funktionen als Daten, Rekursionsmuster, generische Polymorphie)
diese wurden dann als Entwurfsmuster neu erfunden
in modernen Sprachen nicht mehr nötig, diese verwenden mehrere Paradigmen (darunter OO, FP)
Zusammenfassung von Daten (zu einem Objekt),
Zusammenfassung von Objekten mit gleichen Operationen (zu einer Klasse)
Trennung (und Beziehung) von konkretem Datentyp (Klasse) und abstraktem Datentyp (Schnittstelle)
(dynmische Polymorphie) Auswahl der Implementierung (der Methodetabelle) abhängig vom Laufzeittyp
Bob Harper, 2011: OO ist
anti-modular (wg. Implementierungs-Vererbung)
anti-parallel (wg. Zustandsänderungen)
\(\Rightarrow\) Schnittstellenvererbung, unveränderliche Objekte
Ziel: flexibel wiederverwendbarer sicherer Quelltext
Lösung: Funktionen höherer Ordnung
Simulation davon im OO-Paragidma: Entwurfsmuster
wir wollen: Funktion als Datum (z.B. Lambda-Ausdruck),
wir konstruieren: Objekt, das zu einer (anonymen) Klasse gehört, die
diese Funktion als Methode enthält.
Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides: Entwurfsmuster (design patterns) — Elemente wiederverwendbarer objektorientierter Software, Addison-Wesley 1996.
Aufgabe: Sortieren einer Liste bzgl. wählbarer Ordnung auf Elementen.
Lösung (in Data.List
)
data Ordering = LT | EQ | GT
sortBy :: (a -> a -> Ordering) -> List a -> List a
Simulation (in java.util.*
)
interface Comparator<T> { int compare(T x, T y); }
static <T> void sort(List<T> list,Comparator<T> c);
hier ist c
ein Strategie-Objekt, dessen Klasse
die Vergleichsfunktion als (einzige) Methode enthält
Deklaration
interface Comparator<T> { int compare(T x, T y); }
static <T> void sort(List<T> list,Comparator<T> c);
Benutzung (Notation bisher, anonyme innere Klasse)
Collections.sort(xs,new Comparator<Integer>(){
int compare(Integer x, Integer y) { ... } })
ab Java 8
@FunctionalInterface Comparator<T> { .. }
Collections.sort(xs, (x,y)-> ... )
ein Lambda-Ausdruck (“burger arrow”) erzeugt ein Objekt einer anonymen inneren Klasse, die ein funktionales Interface (mit genau einer Methode) implementiert
Bsp: Gestaltung von zusammengesetzten Layouts.
Modell als algebraischer Datentyp:
data Component = JButton { ... }
| Container (List Component)
Simulation durch Entwurfsmuster Kompositum:
abstract class Component
class JButton extends Component
class Container extends Component
{ void add (Component c); }
import javax.swing.*; import java.awt.*;
public class Composite {
public static void main(String[] args) {
JFrame f = new JFrame ("Composite");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container c = new JPanel (new BorderLayout());
c.add (new JButton ("foo"), BorderLayout.CENTER);
c.add (new JButton ("bar"), BorderLayout.SOUTH);
Container d = new JPanel (new GridLayout (0,2));
d.add (c); d.add (new JButton ("baz"));
f.getContentPane().add(d);
f.pack(); f.setVisible(true); } }
Ziel ist eine Baumstruktur, leider imperativ konstruiert
(Konstruktor new JPanel(List<Component> cs)
fehlt)
Definition: Kompositum \(=\) algebraischer Datentyp (ADT)
ADT data T = .. | C .. T ..
als Kompositum:
Typ T
\(\Rightarrow\) gemeinsame Basisklasse
(interface)
jeder Konstruktor C
\(\Rightarrow\) implementierende
Klasse
jedes Argument des Konstruktors \(\Rightarrow\) Attribut der Klasse
diese Argumente können T
benutzen (rekursiver
Typ)
(Vorsicht: Begriff und Abkürzung nicht verwechseln mit abstrakter Datentyp \(=\) ein Typ, dessen Datenkonstruktoren wir nicht sehen)
Knoten sind äußere (Blatt) und innere (Verzweigung).
data Tree k = Leaf { ... }
| Branch { left :: Tree k , ...
, right :: Tree k }
Schlüssel: in allen Knoten, nur innen, nur außen.
Realisierung als Kompositum
interface Tree<K>;
class Branch<K> implements Tree<K> { ... }
class Leaf<K> implements Tree<K> { ... }
in der modernen Welt: schreibe
record Branch<K>(Tree<K> left,...)
Attribute sind unveränderlich;
Compiler erzeugt: Konstruktor, equals, toString
Wenn Blätter keine Schlüssel haben, geht es musterfrei?
class Tree<K> { // nur ein Typ
Tree<K> left; K key; Tree<K> right; }
Der entsprechende algebraische Datentyp ist
data Tree k = Tree
{ left :: Maybe (Tree k) , key :: k
, right :: Maybe (Tree k) }
erzeugt in Java dieses Problem:
class Tree<K> { .. int size () { ... } }
Tree<Foo> t = ...; int s = t.size();
Übung: betrachte Implementierung in
java.util.TreeMap<K,V>
ein Besucher-Objekt
interface Visitor<K,R> {
R leaf(); R branch(R left, K key, R right); }
repräsentiert die beiden Schema-Argumente des fold
Methode zum Empfangen des Besuchers (zum Auswerten des fold)
interface Tree<K> { ..
<R> R receive (Visitor<K,R> v); }
in jedem Teilnehmer des Komp. implementieren, Bsp.
record Leaf<K>() implements Tree<K> {
<R> R receive (Visitor<K,R> v){return v.leaf();} }
Anwendung: Größe (Höhe, usw.) als Besucherobjekt
bekannt: Aufgabenstellung, Eingaben, Ausgaben (Fehlermeldungen): wie in Hausaufgaben
neu: (konfigurierbar durch Aufgabensteller, je Aufgabe)
Anzeige der Bewertung erst nach Ende der Bearbeitungs (Prüfungs)zeit
…oder doch vorher, dann aber mit
Abwertung bei Fehlern (z.B. Syntaxfehler: minus 1 Prozent, Semantik-Fehler: minus 10 Prozent)
Test in KW26 (Dienstag) 9:30 und 13:45,
Termin/Raum bitte auswählen (autotool)
Verschiedenes: restliche Übungen (Punkte), Umfragen
Folge von Daten:
erzeugen (producer)
transformieren
verarbeiten (consumer)
aus softwaretechnischen Gründen diese drei Aspekte im Programmtext trennen,
aus Effizienzgründen in der Ausführung verschränken (bedarfsgesteuerte Transformation/Erzeugung)
Unix: Prozesskopplung durch Pipes
cat foo.text | tr ' ' '\n' | wc -l
Betriebssystem (Scheduler) simuliert Nebenläufigkeit
OO: Iterator-Muster
Enumerable.Range(0,10).Select(n=>n*n).Sum()
ersetze Daten durch Unterprogr., die Daten produzieren
FP: lazy evaluation (verzögerte Auswertung)
from n = n : from (n+1)
sum $ take 10 $ map ( \ n -> n * n ) $ from 0
Realisierung: Termersetzung \(\Rightarrow\) Graphersetzung,
data Stream a = Cons a (Stream a)
nats :: Stream N ; nf :: N -> Stream N
nats = nf 0 ; nf n = Cons n (nf (n+1))
head (Cons x xs) = x ; tail (Cons x xs) = xs
Obwohl nats
unendlich ist, kann Wert von
head (tail (tail nats))
bestimmt werden:
= head (tail (tail (nf 0)))
= head (tail (tail (Cons 0 (nf 1))))
= head (tail (nf 1))
= head (tail (Cons 1 (nf 2)))
= head (nf 2) = head (Cons 2 (nf 3)) = 2
es wird immer ein äußerer Redex reduziert
(Bsp: nf 3
ist ein innerer Redex)
zu jedem Typ \(T\) betrachte \(T_\bot=\{\bot\}\cup T\)
dabei ist \(\bot\) ein Nicht-Resultat vom Typ \(T\)
Exception undefined :: T
(bedeutet: \(\bot\in T_\bot\))
Nicht-Termination f x=f(f x)
\(\Rightarrow f 0=\bot)\)
Def.: Funktion \(f\) heißt strikt, wenn \(f(\bot)=\bot\).
Fkt. \(f\) mit \(n\) Arg. heißt strikt in \(i\),
falls \(\forall x_1 \dots x_n: (x_i=\bot) \Rightarrow f(x_1,\ldots,x_n)=\bot\)
verzögerte Auswertung eines Arguments
\(\Rightarrow\) Funktion ist dort nicht
strikt
einfachste Beispiele in Haskell:
jeder Konstruktor (Cons,…) ist jedem Arg. nicht strikt
Destruktoren (head, tail,…) sind strikt.
length :: [a] -> Int
ist strikt:
length undefined ==> (exception)
(:) :: a->[a]->[a]
ist nicht strikt im 1.
Argument:
length (undefined : [2,3]) ==> 3
d.h. (undefined : [2,3]
) ist nicht \(\bot\)
(&&)
ist strikt im 1. Arg, nicht strikt im
2. Arg.
undefined && True ==> (exception)
False && undefined ==> False
Begriffe:
nicht strikt: nicht zu früh auswerten
verzögert (lazy): höchstens einmal auswerten
(ist Spezialfall von nicht strikt)
bei jedem Konstruktor- und Funktionsaufruf:
kehrt sofort zurück
Resultat ist thunk (Paar von Funktion und Argument)
thunk wird erst bei Bedarf ausgewertet
Bedarf entsteht durch Pattern Matching
nach Auswertung: thunk durch Resultat überschreiben
(das ist ein Graph-Ersetzungs-Schritt) (nicht: Term-…)
bei weiterem Bedarf: wird Resultat nachgenutzt
def F (x : Int) : Int = {
println ("F", x) ; x*x
}
lazy val a = F(3);
println (a);
println (a);
hier sehen wir, wann die Auswertung stattfindet, weil sie eine Nebenwirkung hat (die Ausgabe).
in Haskell gibt es keine Nebenwirkungen,
man kann lazy evaluation nur indirekt feststellen
(non)strictness (keine Exception)
Ressourcenverbrauch (Speicher, Zeit) (ghci:
:set +s
)
John Hughes: Why Functional Programming Matters, 1984 https://www.cse.chalmers.se/~rjmh/Papers/whyfp.html
Bob Harper 2011 https://existentialtype.wordpress.com/2011/04/24/the-real-point-of-laziness/
Lennart Augustsson 2011 http://augustss.blogspot.de/2011/05/more-points-for-lazy-evaluation-in.html
Nicht-Beispiel (JS hat strikte Auswertung)
function wenn (b,x,y){ return b ? x : y; }
function f(x) {return wenn(x<=0,1,x*f(x-1));}
f(3)
in Haskell geht das (direkt in ghci
)
let wenn b x y = if b then x else y
let f x = wenn (x<= 0) 1 (x * f (x-1))
f 3
in JS simulieren (wie sieht dann f
aus?)
function wenn (b,x,y){ return b ? x() : y(); }
anstatt Wert: eine Funktion mit Argument (), die den Wert
ausrechnet—aber dann jedesmal, ist nicht-strikt,
nicht lazy (dazu müßte der Wert gespeichert werden)
foldr :: (e -> r -> r) -> r -> [e] -> r
or = foldr (||) False
-- Bsp: or [ False, True, undefined ]
wenn f
nicht strikt ist, hier:
f = (||)
,
werden mglw. nicht alle rekursiven Aufrufe ausgewertet
foldr f z (x : xs') = f x (foldr f z xs')
ist nicht möglich mit strikter Auswertung
foldr f z (x : xs') =
let { {- strict -} y = foldr f z xs' } in f x y
strikte Sprachen erschweren benutzerdef. Abstraktion der Ablaufsteuerung, diese ist deswegen nicht modular
stream ist unendliche Datenstruktur vom Typ
data Stream e = Cons e (Stream e)
Zweck ist modulare Ablaufsteuerung
man benutzt meist den eingebauten Typ
data [a] = [] | a : [a]
alle anderen Anwendungen des Typs [a]
sind
falsch
(zur Datenspeicherung: anstelle von Arrays, endliche Mengen)
mehr dazu: https://www.imn.htwk-leipzig.de/~waldmann/etc/untutorial/list-or-not-list/
…und höchstens stellenweise in der Lehre erlaubt, als Beispiel für Induktion (Rekursion).
primes :: [ Nat ]
primes = sieve ( from 2 )
from :: Nat -> [ Nat ]
from n = n : from ( n+1 )
sieve :: [ Nat ] -> [ Nat ]
sieve (x : ys) =
x : sieve (filter (\y ->0 < mod y x) ys)
(Das ist (sinngemäß) das Code-Beispiel auf https://www.haskell.org/)
let
-Bindungender Teilausdruck undefined
wird nicht
ausgewertet:
let { x = undefined ; y = () } in y
alle Namen sind nach jedem =
sichtbar:
let { x = y ; y = () } in x
links von =
kann beliebiges Muster stehen
let { (x, y) = (3, 4) } in x
let { (x, y) = (y, 5) } in x
es muß aber passen, sonst
let { Just x = Nothing } in x
Modell: binäre Bäume wie üblich, mit fold
dazu
data T k = L | B (T k) k (T k)
Aufgabe 1: jeder Schlüssel soll durch Summe aller Schlüssel ersetzt werden.
f (B (B L 2 L) 3 L) = B (B L 5 L) 5 L
f t = let s = fold 0 (\ x y z -> x+y+z) t
in fold L (\ x y z -> Branch x s z) t
Aufgabe 2: dasselbe mit nur einem fold. Hinweis:
f t = let { (s, r) = fold _ _ t } in r
Beispiel 1: untersuche Striktheit der Funktion
f :: Bool -> Bool -> Bool
f x y = case y of { False -> x ; True -> y }
Antwort:
\(f\) ist nicht strikt im 1. Argument,
denn f undefined True = True
\(f\) ist strikt im 2. Argument,
denn dieses Argument (y
) ist die Diskriminante der
obersten Fallunterscheidung.
Beispiel 2: untersuche Striktheit der Funktion
g :: Bool -> Bool -> Bool -> Bool
g x y z =
case (case y of False -> x ; True -> z) of
False -> x
True -> False
Antwort (teilweise)
ist strikt im 2. Argument, denn die Diskriminante
(case y of ..)
der obersten Fallunterscheidung verlangt
eine Auswertung der inneren Diskriminante y
.
SS 24: 1, 6; optional: 2, 7, 8
Aufgabe: strikt in welchen Argumenten?
f x y z = case y || x of
False -> y
True -> case z && y of
False -> z
True -> False
die Eigenschaft lt. Definition: (a) erklären und (b) mit
ghci
experimentell überprüfen
Bereiten Sie eine ähnliche Aufgabe vor, die Sie in der Übung den anderen Teilnehmern stellen.
Bestimmen Sie jeweils die ersten Elemente dieser Folgen (1. auf Papier durch Umformen, 2. mit ghci).
Vorher für die Hilfsfunktionen (map
,
zipWith
, concat
):
1. Typ feststellen, 2. Testfälle für endliche Listen
f = 0 : 1 : f
n = 0 : map (\ x -> 1 + x) n
xs = 1 : map (\ x -> 2 * x) xs
ys = False
: tail (concat (map (\y -> [y,not y]) ys))
zs = 0 : 1 : zipWith (+) zs (tail zs)
siehe auch https://www.imn.htwk-leipzig.de/~waldmann/etc/stream/
Für Peano-Zahlen und Eigenbau-Listen implementieren:
len :: List a -> N
als fold,
Vergleich (gt: greater than, größer als) wie folgt:
gt :: N -> N -> Bool
gt Z y = _
gt (S x) Z = _
gt (S x) (S y) = gt _ _
und diese Auswertung erklären:
gt (len (Cons () (Cons () undefined)))
(len (Cons () Nil))
Unterschiede erklären zu
length (() : () : undefined) > length (() : undefined)
Folie Ablaufsteuerung: Ifthenelse (wenn
) als
Funktion in Haskell, in Javascript, Simulation der nicht-strikten
Auswertung.
Algorithmus aus Appendix A aus Chris Okasaki: Breadth-First
Numbering: Lessons from a Small Exercise in Algorithm Design (ICFP
2000) implementieren (von where
auf let
umschreiben), testen und erklären
für Peano-Zahlen mit plus
, mal
,
hoch
wie üblich (rekursiv oder mit fold) sowie
gt
wie vorige Aufgabe:
wie teuer ist gt (hoch (S (S Z)) (hoch ...)) Z
?
gilt Monotonie der arithmetischen Funktionen?
Bsp: folgt aus gt x y = True
, daß
gt (mal x (S Z)) (mal y (S Z)) = True
? daß
gt (mal (S Z) x) (mal (S Z) y) = True
?
Betrachten Sie Beispiele wie
x = (S (S undefined)); y = (S Z)
.
Def: \(f :: \texttt{Tree k}\to b\) heißt strikt in der Struktur, wenn für für alle \(t\) gilt: wenn \(t\) ein \(\bot\) an Stelle eines Konstruktors enthält, dann \(f (t) =\bot\).
Entspr.: strikt in den Schlüsseln, wenn …\(\bot\) an Stelle eines Schlüssels….
Bsp: nodes
ist nicht strikt in den Schlüsseln, denn
nodes (Branch Leaf undefined Leaf) = 3
Untersuchen Sie diese Eigenschaften für die Funktionen aus früherer Aufgabe für Fold auf binären Bäumen.
aus aktuellem Anlaß: Lazy Evaluation und Künstliche Intelligenz (Kapitel 5 aus Hughes 1984, vereinfacht:)
Funktionen min
und max
für Peano-Zahlen
implementieren, die in der Struktur möglichst lazy sind, Bsp:
min (S Z) (S (S undefined)) = S Z
Diese Funktionen verwenden für die minimax-Bewertung eines (Spiel)baums (binär, Schlüssel in den Blättern).
Begründen Sie: diese Bewertung ist nicht strikt in der Struktur, d.h., der exakte Spielwert kann bestimmt werden, ohne alle Teilbäume anzusehen. (Hinweis: ein Bsp. ist im Paper)
Vereinfach: Bewertung durch Bool (gewonnen/verloren). Was sind dafür min und max? Wie strikt sind diese?
Aufgabenstellung:
For this set of declarations
{ g x y = case (f x y True x) of
{ False -> f True True False x; True -> y }
; f z y p q = case p of { False -> True
; True -> case q of
{ False -> True; True -> False } } }
Prove that function g is not strict:
Write a call where at least one argument is undefined,
such that the result is defined
(it has a head normal form with a constructor on top)
and will be obtained after at most 40 steps.
Beispiel-Lösung: g True undefined
Aufgabenstellung:
Find a replacement expression for the hole (_) in the program
{ f z x =
case (g x (g x z)) of
{ False -> False; True -> False }
; g z x = _
}
such that evaluations of the following expressions
to head normal form gives these results:
[ ( f undefined False, Defined )
, ( f undefined True, Bottom ) ]
Beispiel-Lösung:
case z of {False -> False; True -> x}
Beispiel-Bewertung (Ausschnitt)
evaluate f undefined False
(must have result Defined)
** Address 0 allocate new Thunk (f undefined False) (listToFM [ ])
>> compute head normal form for Address 0
value at address is
Thunk (f undefined False) (listToFM [ ])
** clock tick 1
is thunk, reduce it
is application
** Address 1 allocate new Thunk undefined (listToFM [ ])
** Address 2 allocate new Thunk False (listToFM [ ])
defining equations for function f
[ [ z, x ] -> case (g x (g x z)) of { False -> False; True -> False } ]
>> find first applicable equation, return rhs and matching substitution
** clock tick 2
>> multi_match [ z, x ]
[ Address 1, Address 2 ]
>> match z
Address 1
<< match z
Address 1
result: Just (listToFM [ ( z, Address 1 ) ])
>> match x
Address 2
<< match x
Address 2
result: Just (listToFM [ ( x, Address 2 ) ])
<< multi_match [ z, x ]
[ Address 1, Address 2 ]
result: Just (listToFM [ ( x, Address 2 ), ( z, Address 1 ) ])
...
Verarbeitung von Datenströmen,
durch modulare Programme,
zusammengesetzt aus elementaren Strom-Operationen
Auswirkungen:
ausdrucksstarker, übersichtlicher Code
effizienter Code durch Umformung (im Compiler) (GHC)
automatische Parallelisierung (C#, PLINQ)
externe Datenbank als Datenquelle (C#, LINQ)
geeignete Fkt. höherer Ordnung \(\Rightarrow\) triviale Parallelisierung:
Func<int,int> f = x => ... // teure Rechnung
var s = Enumerable.Range(1, 20000)
.Select( f ).Sum() ;
var s = Enumerable.Range(1, 20000)
.AsParallel()
.Select( f ).Sum() ;
Dadurch werden
Elemente parallel verarbeitet (.Select(f)
)
Resultate parallel zusammengefaßt (.Sum()
)
vgl.
erzeugen (produzieren):
n = 1 : map (\x -> x+1) n
Enumerable.Range(int start, int count)
eigene Instanzen von IEnumerable
transformieren:
elementweise: map
, Select
gesamt: take,drop,filter
(Take, Skip, Where
)
verbrauchen (konsumieren):
fold
, Aggregate
Spezialfälle: All, Any, Sum, Count
imperativ (durch Zustandsänderung)
Iterable<Integer> from(int start) {
return () -> new Iterator<Integer>() {
int k = start; // <-- der Zustand
public Integer next() { return k++; }
public boolean hasNext () { return true; } }; }
Anwendung:
for (Integer x : from(5)) { ... }
funktional (durch Iteration einer Funktion)
Stream<Integer> s = Stream.iterate(5,x->x+1);
Anwendung: s.limit(10).toList()
elementweise (unter Beibehaltung der Struktur)
map :: (a -> b) -> [a] -> [b]
Realisierung in C#:
IEnumerable<B> Select<A,B>
(this IEnumerable <A> source,
Func<A,B> selector);
Rechenregeln (Implementierung) für map
:
map f [] = ... ; map f (x : xs) = ...
damit kann man beweisen:
map f (map g xs) = map (\ x -> ... ) xs
jeder polymorphe algebraischen Datentyp (Container)
data T a = ...
besitzt eine struktur-erhaltende elementweise Transformation (die das Typ-Argument ändern kann)
diese ist Methode der Konstruktorklasse Functor
:
class Functor c where fmap :: (a->b) -> c a -> c b
Instanzen deklarieren
instance Functor T where fmap
oder vom Kompiler erzeugen lassen
{-# language DeriveFunctor #-}
data List a = ... deriving Functor
benutzen:
fmap (\ x -> 2*x) (Cons 3 (Cons 5 Nil))
jeder Funktor-Instanz soll diese Axiome erfüllen:
fmap id = id
(wobei
id = \ x -> x
)
\(\forall \texttt{f},
\texttt{g}:\) fmap (f . g) = fmap f . fmap g
(wobei (f . g) = \ x -> f(g x)
)
die durch deriving Functor
erzeugten tun es
Ü: Implementierung ergänzen und Axiome überprüfen:
instance Functor Maybe where
fmap f m = case m of
Nothing -> _
Just x -> _
Änderung der Struktur, Beibehaltung der Elemente
aus Haskell-Standardbibliothek:
take :: Int -> [a] -> [a]
drop :: Int -> [a] -> [a]
filter :: (a -> Bool) -> [a] -> [a]
Realisierung in C# (LINQ):
Take, Drop, Where
Übung: takeWhile, dropWhile, span
ausprobieren (Haskell, C#)
implementieren
Haskell: 1. mit expliziter Rekursion, 2. mit fold
C# (Enumerator): 1. mit Current,MoveNext
, 2.
yield
IEnumerable<int> stream = from c in cars
where c.colour == Colour.Red
select c.wheels;
LINQ-Syntax nach Schlüsselwort from
(das steht vorn — SQL vom Kopf auf die Füße gestellt)
wird vom Compiler übersetzt in
IEnumerable<int> stream = cars
.Where (c => c.colour == Colour.Red)
.Select (c => c.wheels);
auf Deutsch: map wheels (filter isRed cars)
Funktionen 2. Ordnung: Select \(=\) map, Where \(=\) filter.
from x in Enumerable.Range(0,10)
from y in Enumerable.Range(0,x)
select y*y
wird vom Compiler übersetzt in
Enumerable.Range(0,10)
.SelectMany(x=>Enumerable.Range(0,x))
.Select(y=>y*y)
aus diesem Grund ist SelectMany
wichtig
das mathematische Modell ist >>=
(gesprochen:
bind)
(>>=) :: [a] -> (a -> [b]) -> [b]
xs >>= f = concat (map f xs)
(>>=) :: [a] -> (a -> [b]) -> [b]
xs >>= f = concat (map f xs)
[1 .. 10] >>= \ x ->
[x .. 10] >>= \ y ->
[y .. 10] >>= \ z ->
guard (x^2 + y^2 == z^2) >>= \ _ ->
return (x,y,z)
mit diesen Hilfsfunktionen
guard :: Bool -> [()]
guard b = case b of False->[]; True->[()]
return :: a -> [a] ; return x = [x]
do
-Notation[1 .. 10] >>= \ x ->
[x .. 10] >>= \ y ->
[y .. 10] >>= \ z ->
guard (x^2 + y^2 == z^2) >>= \ _ ->
return (x,y,z)
do x <- [1 .. 10]
y <- [x .. 10]
z <- [y .. 10]
guard $ x^2 + y^2 == z^2
return (x,y,z)
https://www.haskell.org/onlinereport/haskell2010/haskellch3.html#x8-470003.14
für den Stream-Datentyp [a]
\(=\) List a
aus der
Standardbibliothek
unser fold
heißt foldr
, r wegen von
rechts, (Eselsbrücke: richtig), aber die Argument-Reihenfolge ist falsch
(ist nicht die Konstruktor-Reihenfolge)
\(\text{foldr} ~ f ~ s ~ [x_1,x_2,x_3]=f ~x_1~ (f ~ x_2 ~ (f ~ x_3 ~ s))\)
\(\text{foldr}~f~s~[x_1,\dots,x_n]=f ~x_1 ~(\text{foldr}~f~s~[x_2,\dots,x_n])\)
ein anderes Rekursionmuster ist foldl
(von
links)
\(\text{foldl} ~ f ~ s ~ [x_1,x_2,x_3]=f(f(f ~ s~ x_1) ~x_2) ~x_3)\)
\(\text{foldl}~f~s~[x_1,\dots,x_n]=f ~(\text{foldl}~f~s~[x_1,\dots,x_{n-1}]) ~ x_n\)
speziell für Streams, nicht (wie fold) allgemein für Bäume
Anwend.: bestimme f,s
mit
reverse = foldl f s
\(\text{foldl}~f~s~[x_1,\dots,x_n]=f ~(\text{foldl}~f~s~[x_1,\dots,x_{n-1}]) ~ x_n\)
Aufgabe: bestimme f,s
mit
reverse = foldl f s
Herleitung der Lösung durch Beispiel
[3,2,1] = reverse [1,2,3]
= foldl f s [1,2,3]
= f (foldl f s [1,2]) 3
= f (reverse [1,2]) 3 = f [2,1] 3
also f [2,1] 3 = [3,2,1]
, d.h.,
f x y = y : x
Lösung:
reverse = foldl (\ x y -> y : x) []
Eigenschaft (vorige Folie) sollte nicht als Implementierung benutzt werden,
denn \([x_1,\ldots,x_{n-1}]\) ist teuer (erfordert Kopie)
foldl :: (b -> a -> b) -> b -> [a] -> b
foldl f s xs = case xs of
[] -> s
x : xs' -> foldl f (f s x) xs'
zum Vergleich
foldr :: (a -> b -> b) -> b -> [a] -> b
foldr f s xs = case xs of
[] -> s
x : xs' -> f x (foldr f s xs')
der Typ von Prelude.foldl
ist tatsächlich
Foldable t => (b->a->b) -> b -> t a -> b
hierbei ist Foldable
eine
(Typ)Konstruktor-Klasse
mit der einzigen (konzeptuell) wesentlichen Methode
class Foldable t where toList :: t a -> [a]
d.h., produziert eine lazy Liste, d.h., einen Iterator
und Instanzen für viele generische Container-Typen
weitere Methoden aus Effizienzgründen
C# (Linq) | Haskell |
---|---|
IEnumerable<E> |
[e] |
Select | map |
SelectMany | >>= (bind) |
Where | filter |
Aggregate | foldl |
Ü: ergänzen um die Spalte für Streams in Java
dort Zusammenfassung u. Begründung der Bedarfsaus- wertung: Many stream operations …can be implemented lazily, exposing opportunities for optimization.
SS 24: 1, 2, 8 oder 9; vorige Woche: 7, 8
die Funktion fromBits :: [Bool] -> Integer
,
Beispiel fromBits [True,False,False,True,False]=18
…als foldr
oder als foldl
?
Geben Sie die eine Darstellung an, begründen Sie, daß die andere unmöglich ist.
Vervollständigen Sie die Gleichung
foldl f a (map g xs) = foldl _ _ xs
so, daß rechts kein map
steht. Dieses Verschwinden des
map
heißt Fusion.
Fusion mit foldr
vgl. Gill, Launchbury, Peyton Jones:
A Short Cut to Deforestation, FCPA 1993; https://dl.acm.org/doi/10.1145/165180.165214
Vervollständigen Sie die Gleichung
foldr f a xs = foldl _ _ (reverse xs)
und testen Sie mit Leancheck. Verwenden Sie dabei
Test.LeanCheck.Function
, um auch f
zu
generieren.
In einem Kommentar in GHC.List
stellt SLPJ (wer ist das?) fest, daß man in der Gleichung
filter p (filter q xs)
= filter (\ x -> q x && p x) xs
, die zur Programmtransformation während der Kompilation verwendet
wird, die Reihenfolge der Argumente im Teilausdruck
q x && p x
nicht vertauschen darf. Warum—die
Konjunktion ist kommutativ?
Geben Sie jeweils die strukturerhaltende Transformation als Funktor-Instanz an:
Binärbäume mit Schlüssel in Verzweigungsknoten
data Tree a = ...
instance Functor Tree where
fmap f t = case t of ...
data T a = T (Bool -> a)
instance Functor T where fmap f (T g) = ...
Überprüfen Sie (mit Leancheck) die Funktor-Axiome.
Vergleichen Sie das Verhalten Ihrer Implementierung mit der von
deriving Functor
.
warum ist keine der folgenden Instanzen korrekt? Überprüfen Sie statische Korrektheit (Typisierung) und dynamische Korrektheit (Axiome)
instance Functor Maybe where fmap f m = m
instance Functor Maybe where fmap f m = Nothing
instance Functor [] where
fmap f xs = reverse (map f xs)
instance Functor [] where
fmap f xs = take 1 (map f xs)
bei (c) und (d) ist []
der Typkonstruktor für Listen aus
der Standardbibliothek, map
die korrekte
Implementierung.
(Funktionen aus Data.List)
implementieren Sie scanr
mittels
foldr
.
scanr f z = foldr (\x (y:ys) -> _) _
implementieren Sie scanr
mittels
mapAccumR
.
scanr f z = uncurry (:) . mapAccumR _ _
ergänzen Sie die allgemeingültige Aussage
scanr f z (reverse xs) = _ (scanl _ _ xs)
definieren Sie den Operator (>=>)
durch
f >=> g = \ xs -> f xs >>= g
Ergänzen Sie, so daß das Resultat stimmt:
((\x ->[1,x]) >=> _ ) 5 ===> [4,2,8,2]
Prüfen Sie, daß (>=>)
assoziativ ist
(Java) die Bedarfsauswertung eines Streams durch
Stream.allMatch
, Stream.anyMatch
vorführen.
In Haskell: all (\x -> x) [True,False, undefined]
(keine Expection)
In Java nicht so einfach, weil
Stream.of(false,true,false).allMatch(x->x)
alle Elemente
schon bei Konstruktion auswertet.
Ansatz: für eine Funktion f
mit einer Nebenwirkung
(Ausgabe auf Konsole) wird ausgewertet:
Stream.of(...).map(f).allMatch(x->x)
Dabei f
als anonyme Funktion schreiben:
_.map(x -> { ... ; return x })
(C#) (siehe kw27/P.cs im Repo, korrigierte Version des Beispiels aus der VL)
Func<int,bool> p = (x) => { for(int t = 2; t*t<=x; t++){if (0 == x % t) return false;} return x>=2; }
Time(()=> print (Enumerable.Range(0, 10000000).Where(p).Count()));
ausführen, .AsParallel()
an der passenden Stelle
einfügen, ausführen, Lauzeiten vergleichen.
die ausgebenen Zahlen auch vergleichen, untereinander und mit einer verläßlichen Quelle für die Anzahl der Primzahlen \(\le 10^n\).
Dieser Primzahltest p
verwendet Zustandsänderung und
händischer Programmablaufsteuerung. Geben Sie einen rein funktionalen
Test mit bedarfsausgewertetem Stream an. Ergänzen Sie:
Func<int,bool> q = (x) => x >= 2 && Enumerable.Range(...).TakeWhile(...).All(...)
Primzahltest imperativ (mit Zustandsänderung)
Func<int,bool> p = (x) => {
for(int t = 2; t*t<=x; t++){
if (0 == x % t) return false;} return x>=2; }
Programmablaufsteuerung d. Schleifen, return
Primzahltest funktional (ohne sichtbare Z.Ä.)
Func<int,bool> q = (x) => x >= 2
&& Enumerable.Range(2, x-2)
.TakeWhile(t => t*t<=x).All(t => 0!= x % t)
P.A. durch Komposition von Ausdrücken mit bedarfsgesteuerter Auswertung
destruktiv (Java)
interface Tree<K> { void insert (K key); }
Tree<String> t = ... ;
t.insert ("foo");
persistent (Java):
interface Tree<K> { Tree<K> insert (K key); }
Tree<String> t = ... ;
Tree<String> u = t.insert ("foo");
persistent (Haskell):
insert :: Ord k => k -> Tree k -> Tree k
data Tree k = Leaf | Branch (Tree k) k (Tree k)
Invariante: \(\forall\)
t
\(\in\)
Tree k . monotone (inorder t)
monotone :: Ord k => [k] -> Bool
monotone xs = and $ zipWith (<=) xs $ drop 1 xs
insert :: Ord k => k -> Tree k -> Tree k
insert x t = case t of
Leaf -> Branch Leaf x Leaf
Branch l k r -> case compare x k of ...
wie teuer ist die Persistenz?
(wieviele neue Objekte entstehen bei
insert
,delete
?)
\(|t|\): Anzahl der Blätter, \(H(t)\): Höhe
Invariante: Suchbaum und \(\alpha\)-Balance (Bsp: \(\alpha=1/4\))
\(\forall\)
t = Branch l k r
: \(\alpha|t|\le |l|\le
(1-\alpha)|t|\)
für jeden \(\alpha\)-balancierten Baum \(t\) gilt: \(H(t)\le \log_{1/(1-\alpha)} |t|\). Bsp: \(\alpha=1/4, |t|=10^9\)
Guy Blelloch et al.: Just Join for Parallel Sets, 2016 https://arxiv.org/abs/1602.02120v3
Implementierung von insert, delete, union, intersection
durch smart constructor für bereits separierte Argumente
branch :: Ord k => Tree k -> k -> Tree k -> Tree k
merge :: Ord k => Tree k -> Tree k -> Tree k
dabei smart \(=\) stellt Balance her
Suchbaum realisiert abstr. DT Menge. Wie realisiert man
intersection :: Ord k => Set k -> Set k -> Set k
naiv (elementweise), Kosten: \(O(|s_1|\cdot \log|s_2|)\)
fromList $ filter (\x1 -> member x1 s2) $ toList s1
sollte aber schneller gehen, z.B., wenn \(\max s_1<\min s_2\)
inter (Branch l1 k1 r1) s2 =
let (l2, found, r2) = split k1 s2
l = inter l1 l2 ; r = inter r1 r2
in if found then branch l k1 r else merge l r
Kosten \(O(m\cdot \log (\frac M m + 1))\) für \(m=\min(|s_1|,|s_2|),M=\max(|s_1|,|s_2|)\).
vergleichen Sie Laufzeiten der naiven und der richtigen Implementierung.
In ghci messen mit :set +s
. Dabei Laziness von
let-Bindungen beachten. Auswertung erzwingen durch Anwenden der strikten
Funktion length
.
ghci> import qualified Data.Set as S
ghci> :set +s
ghci> s1 = S.fromList [ 1 .. 10^6 :: Int ]
(0.03 secs, 33,432 bytes)
ghci> length s1
1000000
(0.34 secs, 144,071,248 bytes)
ghci> length s1
1000000
(0.00 secs, 37,832 bytes)
ghci> s2 = S.fromList [ 0 .. 10^6-1 :: Int ]
(0.00 secs, 30,888 bytes)
ghci> length s2
1000000
(0.21 secs, 144,071,320 bytes)
ghci> length $ S.intersection s1 s2
999999
(0.07 secs, 75,069,072 bytes)
Konstruieren Sie eine Menge \(s_3\) mit \(|s_3| = |s_2|\), so daß \(s_1\cap s_3\) (viel) schneller ausgewertet wird als \(s_1\cap s_2\)
das softwaretechnische Ziel statischer Typisierung ist:
vollständige Spezifikation \(=\) Typ
Implementierung erfüllt Spezifikation
\(\iff\) Implementierung ist statisch korrekt
und das wird durch den Compiler überprüft
Beispiele:
nicht validieren (:: String -> Bool
),
sondern parsen (:: String -> Maybe T
)
Unterscheidung zw. Aktion
(readFile "f" :: IO Text
)
und ihrem Resultat (t :: Text
)
Balance (log. Höhe) durch Typisierung (Fingertree)
für jedes g :: forall a . a -> [a]
gilt:
\(\forall\)
h :: a -> b, x :: a
:
g (h x) = map h (g x)
das folgt allein aus dem Typ von g
(polymorph in a
\(\Rightarrow\) Implementierung von
g
weiß nichts über a
)
Phil Wadler: Theorems for Free, FCPA 1989
From the type of a polymorphic function we can derive a theorem that it satisfies. Every function of the same type satisfies the same theorem. This provides a free source of useful theorems, courtesy of Reynolds’ abstraction theorem for the polymorphic lambda calculus.
bekannt ist: generische Container-Typen (Element-Typ als Typ-Parameter): ergibt flexiblen (d.h., nachnutzbaren) und sicheren Code.
der Typ kann auch die Form (z.B.: Balance) eines Baumes beschreiben! — Beispiel:
data Tree k = Single k | Deep (Tree (k,k))
Anwendung: containers:Data.Sequence
newtype Seq a = Seq (FingerTree (Elem a))
data FingerTree a = EmptyT | Single a
| Deep (Digit a) (FingerTree (Node a)) (Digit a)
data Node a = Node2 a a | Node3 a a a
Wiederholung: bisher zwei Arten von Funktionen:
von Datum nach Datum (z.B.: Nachfolger, Spiegelbild)
von Typ nach Typ (Typkonstruktor, z.B.: List, Tree)
nützlich ist auch:
v. Datum nach Typ (B.: Zahl \(\to\) Vektoren dieser Länge)
{-# language DataKinds,KindSignatures,GADTs #-}
data N = Z | S N
data Vector (l :: N) e where
Nil :: Vector Z e
Cons :: e -> Vector l e -> Vector (S l) e
Anwendung: head :: Vector (S l) e -> e
ist
total
Agda (Ulf Norell 2007, Catarina Coquand 1999) http://wiki.portal.chalmers.se/agda
genauere Betrachtung von statischer und dynamischer Semantik von programmiersprachlichen Konstrukten
in VL Prinzipien von Programmiersprachen (Master-Pflicht), Compilerbau (M-Wahl)
in Haskell eingebettete domainspezifische Sprachen in
VL Constraint-Programmierung (Bachelor-W), Computermusik (M-W)
Anwendung der fktl. Programmierung in autotool
(Semantik, Datenbank, Weboberfläche)
ich betreue jederzeit gern B- und M-Arbeiten, die den Code dokumentieren, testen, verbessern, erweitern.
Terme, algebraische Datentypen
Muster, Regeln, Term-Ersetzung (Progr. 1. Ordnung)
Beweisverf.: Umformung, Fallunterscheidung, Induktion
Polymorphie, Typvariablen, Typkonstruktoren
Funktionen als Daten, \(\lambda\)-Kalkül (Progr. höherer Ord.)
Rekursionsmuster (fold)
Eingeschränkte Polymorphie (Typklassen)
Beispiele: Eq, Ord, Show, Listable (Testdatenerzeugung)
Striktheit, Bedarfsauswertung, Streams
Stream-Verarbeit.: (foldl), map, filter, bind
statische Typisierung \(\Rightarrow\)
findet Fehler zur Entwicklungszeit (statt Laufzeit)
effizienter Code (keine Laufzeittypprüfungen)
generische Polymorphie: flexibler und sicherer Code
Funktionen als Daten, F. höherer Ordnung \(\Rightarrow\)
ausdrucksstarker, modularer, flexibler Code
Programmierer(in) sollte
die abstrakten Konzepte kennen
sowie ihre Realisierung (oder Simulation) in konkreten Sprachen (er)kennen und anwenden.