Alles ist eine Funktion: Rückblick auf David Beazleys SICP-Kurs
(ezzeriesa.notion.site)Rückblick auf David Beazley und den SICP-Kurs: Eine Woche Erfahrung
Hier werden die Erfahrungen mit David Beazleys SICP-Kurs Ende 2022 geteilt. Es gibt zwar viele kostenlose Materialien, aber Daves Kurs war besonders effektiv, weil er bestimmte Themen auswählte und sie tiefgehend erklärte.
Ausgangspunkt
Der SICP-Kurs wurde in der Sprache Scheme durchgeführt. Hier wurde zur Erklärung des grundlegenden Substitutionsmodells ein einfacher Scheme-Interpreter in Python implementiert.
Grundlagen der Sprache Scheme
- Primitive: Grundlegende Werte (z. B. Ganzzahlen)
- Operatoren: Grundoperationen wie
+,-,*,/werden in Präfixnotation verwendet - define: Variablendefinition
> (define x 2)
> (+ x 3) ; Ergebnis: 5
- if: Bedingungsausdruck
- lambda: Definition anonymer Funktionen
> ((lambda (x) (* x x)) 3) ; Ergebnis: 9
Scheme-Interpreter in Python
Mit Python wurde ein einfacher Interpreter implementiert, der Scheme-Code auswertet. Die Grundoperationen wurden als Python-Funktionen definiert.
definitions = {
"+": lambda x, y: x + y,
"*": lambda x, y: x * y,
}
Beispiel:
> evaluate(("+", 2, 3)) # Ergebnis: 5
Enthalten waren außerdem die Implementierung von define und lambda sowie die Verarbeitung des Bedingungsausdrucks if.
Substitutionsmodell
Das Substitutionsmodell ist eine einfache Art, Programme zu interpretieren: Variablen werden beim Auswerten durch Werte ersetzt. Sobald jedoch Zuweisung (assignment) hinzukommt, funktioniert dieses Modell nicht mehr.
Zustand
Ein Beispiel dafür, wie das Substitutionsmodell zusammenbricht, ist Zuweisung (assignment). Wenn man etwa den Kontostand eines Bankkontos modelliert, wird mit set! eine Variable aktualisiert.
(define balance 100)
(define (withdraw amount)
(set! balance (- balance amount))
balance)
In diesem Fall kann das Substitutionsmodell den Kontostand vor und nach der Änderung nicht unterscheiden.
Daher wird ein Umgebungsmodell (Environment) benötigt. Variablen werden innerhalb einer Umgebung definiert, und jede Prozedur besitzt ihre eigene Umgebung.
Streams
Eine weitere Möglichkeit, Zustand zu modellieren, sind Streams. Mit lazy evaluation können Streams auch zukünftige Werte modellieren.
Endlosschleifen und Auswertungsreihenfolge
Unterschiede in der Auswertungsreihenfolge: Die meisten Sprachen verwenden applicative-order evaluation und werten zuerst die Argumente aus.
> (square (+ 1 2)) ; Ergebnis: 9
Bei normal-order evaluation wird die Auswertung der Argumente jedoch verzögert, bis sie tatsächlich benötigt werden. Dadurch lassen sich Endlosschleifen vermeiden.
> (define (p) (p))
> (define (test x y) (if (= x 0) 0 y))
> (test 0 (p)) ; Bei normal-order wird 0 zurückgegeben, bei applicative-order entsteht eine Endlosschleife
Lambda-Kalkül und Church-Zahlen
Mit Church-Encoding lassen sich Zahlen als Prozeduren darstellen. Das ist ein wichtiges Konzept der funktionalen Programmierung.
(define (zero f) (lambda (x) x))
(define (increment n) (lambda (f) (lambda (x) (f ((n f) x)))))
zeroist eine Funktion, die ihr Argument unverändert zurückgibt (dieidentity-Funktion).incrementwendet einen Funktionsaufruf einmal zusätzlich an.
Beispiel
> ((zero (lambda (x) (+ x 1))) 0) ; Ergebnis: 0
> (((increment zero) (lambda (x) (+ x 1))) 0) ; Ergebnis: 1
Iteration vs. Rekursion
Scheme verwendet statt einer for-Schleife Rekursion, um wiederholte Aufgaben auszuführen.
Rekursionsbeispiel: Fakultät
(define (factorial n)
(if (= n 1)
1
(* n (factorial (- n 1)))))
Dieser rekursive Aufruf kann durch die Nutzung des Stacks viel Speicher verbrauchen.
Tail-Call-Optimierung
Scheme reduziert den Speicherverbrauch durch Tail-Call-Optimierung. Dadurch verhält sich der Ablauf wie ein iterativer Prozess.
(define (factorial n)
(define (iter product counter)
(if (> counter n)
product
(iter (* product counter) (+ counter 1))))
(iter 1 1))
Abschluss
David Beazleys Kurs behandelt ausgewählte Kernkonzepte aus SICP in großer Tiefe. Besonders hilfreich war er dabei, verschiedene Programmierparadigmen wie funktionale Programmierung, Lambda-Kalkül und Auswertungsreihenfolgen zu verstehen.
Zitat von Knuth
Wenn man nur Theorie studiert, bedeutet das, dass es Zeit ist, sich auf die praktische Seite zu konzentrieren; und wenn man nur praktisch arbeitet, bedeutet das, dass es Zeit ist, sich auf die theoretische Seite zu konzentrieren.
1 Kommentare
Meinungen auf Hacker News
Zur Warnung: Wer tief in SICP/Lisp/Scheme eintaucht, kann seine Art, über Programmierung nachzudenken, verändern – und solche intellektuellen Anregungen sind immer willkommen.
Wenn man diese Ideen jedoch vollständig auf eine objektorientierte Codebase überträgt, wirkt das meist kontraproduktiv oder stößt im Team auf Widerstand.
Nach Lisp kann man zum Beispiel versucht sein, jede
for-Schleife durchforEachzu ersetzen oder alles zumap/reduce-Ketten umzubauen. Wenn die Sprache funktionale Programmierung aber nicht wirklich vollständig annimmt, kann das sowohl Lesbarkeit als auch Performance schaden.Am Ende erdet einen die Erinnerung daran, dass veränderlicher Speicher und CPUs den Code ausführen. Heutzutage halte ich datenorientiertes Design und eine auf die Hardware-Realität abgestimmte „mechanische Empathie“ im Alltag für praktischer als abstrakte Konzepte wie Church-Zahlen.
Je weniger Seiteneffekte, desto vorhersehbarer ist der Code, und Objektorientierung wird oft auch dort eingesetzt, wo einfache Funktionen ausreichen würden.
In der Spieleentwicklung, wo es viele Berechnungen und riskanten Code gibt, passt Objektorientierung ziemlich gut. In der Webentwicklung wirkt funktionale Programmierung dagegen viel natürlicher, und bei SaaS ermöglichen Sprachen wie Elixir zuverlässigeren, fehlerärmeren und besser testbaren Code.
Außerdem gab es Sachen wie
forEachundmap/reducebereits in Smalltalk-Collections, und auch wenn sie keine erstklassige Syntax waren, wurden sie nach Object Pascal und C++ übernommen.Weil der zugrunde liegende Speicher veränderlich ist, haben auch Sprachen der ML-Familie Mechanismen für Mutation, wenn man sie wirklich braucht.
goto, abergotoist noch unbeliebter als Lisp.Noch merkwürdiger ist, dass Caches für Performance auf modernen CPUs unverzichtbar sind, es aber immer noch keine Sprache gibt, die die Speicherhierarchie als First-Class Citizen behandelt.
Am nächsten kommt dem wohl Linux-artiges C, in dem Unterstriche wie ein Meer überlaufen.
Es bietet einen guten Einstieg darin, Zustand mit reinen Funktionen zu codieren.
Tatsächlich gibt es noch viel mehr rein funktionale Codierungen für alle möglichen Daten wie Bäume, Integer, Summentypen/Produkttypen, Bilder, Monaden usw.
Die Codierung kann etwas verwirrend sein, ist aber zugleich elegant und klein.
Eine funktionale Implementierung der Maybe-Monade in JavaScript sieht zum Beispiel so aus:
Nothing = nothing => just => nothingJust = v => nothing => just => just(v)pure = Justbind = mx => f => mx(mx)(f)evalMaybe = maybe => maybe("Nothing")(v => "Just " + v)console.log(evalMaybe(bind(Nothing)(n => pure(n + 1)))) // Nothingconsole.log(evalMaybe(bind(Just(42))(n => pure(n + 1)))) // Just 43data Maybe a = Nothing | Just afoldMaybe :: (Unit -> r) -> (a -> r) -> Maybe a -> rDie beiden Higher-Order Functions, die man an
foldMaybeübergibt, entsprechen jeweilsNothingundJust.Allerdings ist die
Nothing-Seite der Genauigkeit halber in einer Form mit zusätzlichemUnit-Parameter gehalten.In der Typentheorie ist das ziemlich cool, für praktische Programmierung aber nicht besonders gut.
Ich habe mir früher die echte SICP-Vorlesung angesehen, also die MIT-OCW-Aufzeichnung von 1986.
Sie wird oft für ihre hohe Informationsdichte gelobt, aber tatsächlich geht ziemlich viel Zeit verloren: durch Fragen und Antworten mit Studierenden, dadurch, dass der Dozent Aufmerksamkeit auf Versuche lenkt, im Hörsaal eine „Multimedia“-Präsentation zu machen, und dadurch, dass der gesamte Unterrichtsplan im Voraus nicht vollständig durchstrukturiert war und Fragen daher nicht proaktiv abgefangen wurden.
Auch die Zeit, die fürs Schreiben an die Tafel draufgeht, summiert sich erheblich.
Natürlich kann man über die Reihenfolge des Materials beliebig streiten und sie neu anordnen, und irgendwann plane ich auch, selbst eine Videoreihe zu erstellen, die diese Inhalte so erklärt, wie es meinem eigenen Gefühl entspricht.
Ich finde es erfreulich, dass diese Vorlesung eine modernere Sprache wie Python verwendet und trotzdem die Wurzeln beizubehalten scheint; außerdem ist Python eine praktische Multi-Paradigma-Sprache, und ich denke, dass die Leute die Ausdruckskraft durch funktionale Idiome nicht ausreichend anerkennen, auch wenn es keine vollständige Reinheit ist.
Meiner Ansicht nach kann und sollte Python aus dieser Vorlesung herausgenommen werden.
Python hat eine sehr schwache Unterstützung für funktionale Programmierung.
Listen basieren nicht auf cons, Lambdas sind stark eingeschränkt, Pattern Matching ist furchtbar und nicht ausdrucksbasiert, und auch die Namespaces sind seltsam.
Python ist kaum als moderne Sprache zu betrachten, sondern steckt in den 1990ern fest; es ist nur gewachsen, weil es eine ordentliche C-API hat, leider auf Kosten besserer Sprachen.
Weil es keine Tail-Call-Optimierung gibt, brauchen einige Code-Übungen völlig andere Lösungen als in Scheme.
Wenn 1:1 übersetzter Code fehlschlägt, muss der Dozent den Studierenden dann sagen, sie sollten es einfach glauben, weil es wegen der gewählten Sprache so funktioniert, oder soll man alles als Problem betrachten, bei dem der Stack externalisiert werden muss, und es entsprechend lösen?
SICP gewaltsam in Python hineinzupressen wirkt ziemlich töricht.
Die Website ist inzwischen offline, aber auf archive.org kann man die Vorlesungen ansehen.
https://en.m.wikipedia.org/wiki/ArsDigita#ArsDigita_Foundati...
https://archive.org/details/arsdigita_01_sicp/
Sie verkauften einen USB-Stick mit dem gesamten Curriculum; es wäre wirklich schön, wenn jemand ein ISO davon hochladen würde.
https://web.archive.org/web/20190222145553/aduni.org/drives/
Dinge, die sich in Scheme einfach ausdrücken lassen, werden in anderen Sprachen zu komplizierten Übungsaufgaben.
Statt sich auf die zugrunde liegenden Konzepte zu konzentrieren, konzentrieren sich die Studierenden auf Details der Implementierungssprache, um die man sich in Scheme nicht kümmern müsste.
Als ich zum ersten Mal gesehen habe, wie
cons/car/cdrmit Lambdas implementiert wurden, wirkte das wie Magie.Letztlich sehe ich darin aber den Nachweis, dass die Sprach-Laufzeitumgebung Key/Value-Dictionaries implementiert und man diese Implementierung nutzen kann, um andere Datenstrukturen zu bauen.
In Elixir kann man von vorn so viel herausnehmen, wie man möchte.
Eine Closure ohne Verhalten ist nur ein Pointer auf die geschlossenen Variablen, und eine Closure mit zwei Pointern ist ein Paar, aus dem man
carundcdrerhalten kann.Die Laufzeitumgebung muss ermöglichen, dass die referenzierten Objekte auch außerhalb ihrer Definition verwendet werden können, daher braucht es Escape-Analyse, Garbage Collection usw., aber kein Dictionary.
Kürzlich bin ich beim Theorem Proving auf die Idee gestoßen, dass man, um Dinge wie
0 != 1zu beweisen, mit Church-Encoding allein nicht auskommt, sondern induktive Datentypen braucht.Ich habe dazu grob etwas hier abgelegt, zusammen mit einer separaten Kritik an SICP: https://intellec7.notion.site/Drinking-SICP-hatorade-and-why...
Ich möchte die Grenzen der Sichtweise „Alles ist einfach eine Funktion“ besser verstehen.
0 ≠ 1beweisen zu können.Man kann aus dem Theorem
f = g -> f x = g xfolgern, rechts eine Ungleichheitstatsache erzeugen und dann die Kontraposition nehmen.Die Aussage, dass sich Ungleichheit zwischen Church-Zahlen ohne andere Fakten über Ungleichheit nicht direkt beweisen lässt, scheint mir zu stimmen.
Bei induktiven Datentypen hingegen kann das Beweissystem bei zwei konkreten Instanzen desselben induktiven Typs die äußerste Anwendung des Konstruktors rekursiv entfernen und Gleichheit oder Ungleichheit direkt „beobachten“.
Ich frage mich, ob sich das mit Scott-Encoding oder Church-Encoding erreichen lässt.
Das Buch selbst wird hier bereits diskutiert: https://news.ycombinator.com/item?id=42157558
Ich frage mich, ob es einen Grund gibt, warum der Link nicht zum Anfang dieser Seite führt, sondern zu der Diskussion weiter unten.
Vielleicht könnte man diesen Beitrag mit der bestehenden Diskussion zusammenführen.
David Beazley ist in der Python-Welt eine ziemlich legendäre Figur, und diese Vorlesung wirkte zunächst wie eine überraschende Idee, aber nach etwa zwei Sekunden Nachdenken fühlte sie sich wie die perfekte Kombination an, also habe ich mich für die nächste Vorlesung angemeldet.
Der Kern ist, dass diese Form wohl das künftige Gesicht der kontinuierlichen Weiterbildung für Software Engineers sein wird.
Im Code des Abschnitts „the substitution model“ gibt es einen Tippfehler.
("+", ("fib", ("-", "n", 2)), ("fib", ("-", "n", 1))),Definiert ist
fibonacci, undfibist nicht definiert; daher sollten die beidenfib-Aufrufe eindeutigfibonaccisein.Der Code im tatsächlichen GitHub-Repository ist korrekt: https://github.com/savarin/pyscheme/blob/0f47292c8e5112425b5...
Ich halte SICP für großartig.
Allerdings bin ich, je mehr ich gelernt und mich mit Mathematik beschäftigt habe, immer näher zu dem Schluss gekommen, dass Relationen der grundlegendere primitive Begriff sind.
Jede Funktion lässt sich als eingeschränkte Form einer Relation ausdrücken, aber das Umgekehrte gilt nicht ohne erheblichen zusätzlichen Apparat.
Relationale Datenbanken und SQL sind zwar die bekanntesten und erfolgreichsten Beispiele für relationales Programmieren, doch dieses Gebiet halte ich weiterhin für weitgehend unerforscht.
Mein Interesse gilt derzeit weniger dem Entwurf von Programmiersprachen als vielmehr der Vermittlung mathematischer Grundlagen an sehr kleine Kinder.
Seltsamerweise ist es viel einfacher, Prädikate wie „groß“ als einstellige Relation und „größer als“ als zweistellige Relation zu lehren, als dieselben Konzepte über Funktionen zu erfassen.
Die Sichtweise „Alles ist eine Funktion“ ist mir zu stark vereinfacht und oft nicht hilfreich, daher mag ich sie nicht besonders.
Es gibt zum Beispiel Funktionen, die nicht in Cache, RAM, Festplatte usw. passen; Funktionen, bei denen die Big-O-Komplexität explodiert, etwa bei N-way-
JOINs oder bei Suche/Matching; und Funktionen mit Seiteneffekten einschließlich Nicht-Idempotenz.Kaum jemand denkt über Seitenkanalangriffe auf Funktionen nach.
Es gibt auch nichtdeterministische Funktionen, die von Datum, Uhrzeit, Dauer usw. abhängen, und Funktionen können mittendrin fehlschlagen oder auch nicht graceful failen.
Ebenso kann man schwer annehmen, dass sie keine Ressourcen verbrauchen, die andere „Funktionen“ beeinflussen, die einen gemeinsamen Ressourcenpool nutzen.
Funktionsargumente können beliebig groß oder komplex sein, aber in der Realität gibt es Grenzen; dann braucht man Pointer und danach wieder Remote-Referenzen wie Web oder Festplatte.
Sagt mir bitte, wann ich aufhören soll; ich könnte noch lange weitermachen.
In diesem Fall ist das Lambda-Kalkül-Modell die Grundlage des „Alles ist einfach eine Funktion“-Ansatzes und ein gutes Berechnungsmodell, weil es sehr einfach ist und sich im Vergleich zu Turingmaschinen usw. leichter handhaben und durchdenken lässt.
Deshalb liegt es den meisten computerbasierten Logik- und Beweissystemen zugrunde.
Wenn eine Funktion Ressourcen benötigt, lässt man sie diese Ressourcen verlangen; wenn sie von Datum/Uhrzeit abhängt, lässt man sie davon abhängen; wenn sie einen nichtdeterministischen Wert zurückgibt, lässt man sie einen nichtdeterministischen Wert zurückgeben.
Einer der Gründe, warum der funktionale Programmieransatz glänzt, ist, dass er einen zwingt, solche Dinge ernst zu nehmen.
Wenn man implizit gemeinsam genutzte Ressourcen verwenden will, muss man sie modellieren; und eine „Funktion“, die von implizit geteilten Ressourcen abhängt, wird ausdrücklich von einer echten Funktion unterschieden.
Es ist eher eine Prozedur.
Eine Funktion ist eine Prozedur, aber nicht jede Prozedur ist eine Funktion.