1 Punkte von GN⁺ 2024-10-18 | 1 Kommentare | Auf WhatsApp teilen
  • Douglas Hofstadters Text von 1983 stellt Lisp, das in der KI-Forschung weit verbreitet war, als elegante und flexible Sprache vor, die auf einem kleinen mathematischen Kern aufbaut
  • Die Interaktivität von Lisp zeigt sich in der read-eval-print loop: Der Benutzer gibt einen Ausdruck ein, der Interpreter liest ihn, wertet ihn aus, gibt das Ergebnis aus und wartet dann auf die nächste Eingabe
  • Lisp-Objekte bestehen vor allem aus Atomen (atoms) und Listen (lists), wobei nil eine Sonderstellung einnimmt, da es sowohl die leere Liste als auch ein Atom ist
  • quote, eval, car, cdr, cons, setq, set, lambda, def, cond behandeln Code und Daten in derselben Listenstruktur und ermöglichen so, neue Funktionen schrittweise aufzubauen
  • Funktionskomposition, Bedingungsausdrücke, Seiteneffekte und applicative style sowie rekursives Denken werden anhand von Beispielen verknüpft; die abschließende Funktion power und die Metapher vom „porpuquine“ deuten auf Lisp-artige Rekursion hin

Die Stellung von Lisp in der KI-Forschung

  • KI war 1983 ein Forschungsgebiet, das Verhaltensweisen wie Flexibilität, gesunden Menschenverstand, Einsicht, Kreativität, Selbstwahrnehmung und Humor in Computern umsetzen wollte
  • In den USA gab es etwa 2.000 Personen, die professionell mit KI befasst waren, und im Ausland eine ähnliche Zahl von Forschern
  • KI-Forscher waren sich über den besten Weg zur KI stark uneinig, verwendeten bei der Wahl der Programmiersprache aber fast durchgängig Lisp
  • Der Name Lisp stammt von „list processing“ und ist kein vollständiges Akronym
  • Die Attraktivität von Lisp liegt darin, dass es eine „crisp“ und „elegant“ wirkende Sprache ist
    • Viele Sprachen enthalten zahlreiche willkürliche Features, während einige Sprachen wie Lisp und Algol um einen natürlichen Kern herum aufgebaut sind, fast wie ein Teilgebiet der Mathematik
    • Der Kern von Lisp besitzt eine „crystalline purity“, und diese Reinheit trägt nicht nur zur Ästhetik, sondern auch zur Flexibilität bei

Von der mathematischen Logik zur Listenverarbeitungssprache

  • Die tiefen Wurzeln von Lisp liegen in der mathematischen Logik
    • Ideen, die Thoralf Skolem, Kurt Godel und Alonzo Church in den 1920er- und 1930er-Jahren zur Logik beitrugen, wurden später in Lisp aufgenommen
  • Eigentliche Computerprogrammierung begann in den 1940er-Jahren, Hochsprachen wie Lisp kamen in den 1950ern auf
  • Die erste Listenverarbeitungssprache war nicht Lisp, sondern IPL (Information Processing Language)
    • IPL wurde Mitte der 1950er von Herbert Simon, Allen Newell und J. C. Shaw entwickelt
  • John McCarthy entwickelte 1956–1958 auf Basis vorhandener Ideen Lisp als algebraische Listenverarbeitungssprache
    • Lisp verbreitete sich schnell unter jungen Forschern rund um das MIT Artificial Intelligence Project
    • Es wurde auf dem IBM 704 implementiert und breitete sich zu anderen KI-Gruppen aus
    • Es entstanden viele Dialekte, die jedoch denselben eleganten Kern teilten

Interaktives Lisp und das REPL

  • Im Unterschied zu vielen anderen Hochsprachen wurde Lisp als interaktive Sprache präsentiert
  • Der Benutzer startet Lisp im Terminal, sieht einen Prompt und gibt einen Ausdruck ein
    • Beispiel: Gibt man (plus 2 2) ein, wird 4 ausgegeben und ein neuer Prompt erscheint
  • Das zentrale Verhalten des Lisp-Interpreters ist die read-eval-print loop
    • Einen Ausdruck lesen
    • Den Ausdruck auswerten
    • Einen passenden Wert ausgeben
    • Signalisieren, dass der nächste Ausdruck gelesen werden kann
  • Durch diese Struktur kann der Benutzer seine „Wünsche“ einzeln eingeben und das Ergebnis sofort sehen
  • In vielen nicht interaktiven Sprachen muss dagegen erst ein vollständiges Programm geschrieben und dann ausgeführt werden; dabei müssen viele Schritte perfekt ineinandergreifen, und Fehlerkorrektur ist indirekt und teuer
  • Lisp ermöglicht durch die Ausführung eines Ausdrucks nach dem anderen schrittweise Entwicklung und Debugging

Polish notation und Klammern

  • Arithmetische Ausdrücke in Lisp verwenden Polish notation, bei der der Operator vor den Operanden steht
    • Beispiel: (times (plus 6 3) (difference 6 3)) wird zu 27 ausgewertet
  • Diese Notation wurde schon vor dem Computerzeitalter vom polnischen Logiker Jan Lukasiewicz entwickelt
  • In Lisp-Ausdrücken kommen viele Klammern vor
    • Längere Ausdrücke enden häufig mit mehreren schließenden Klammern
    • Das wirkt anfangs abschreckend, aber mit Gewöhnung wird die logische Struktur intuitiv sichtbar
  • Die Beispiele werden in Form von pretty printing mit Einrückung dargestellt, um die Struktur zu zeigen

Atome, Listen und nil

  • Das Herzstück von Lisp sind manipulierbare Strukturen; alle Programme funktionieren dadurch, dass sie Strukturen erzeugen, verändern und zerstören
  • Es gibt zwei Arten von Strukturen
    • Atome (atoms): grundlegende Objekte, die nicht weiter zerlegt werden können
    • Listen (lists): Strukturen aus mehreren Elementen in einer bestimmten Reihenfolge
  • Jedes Lisp-Objekt ist entweder ein Atom oder eine Liste; die einzige Ausnahme ist nil
    • nil ist sowohl Atom als auch Liste
    • nil steht für die leere Liste
    • () und nil bedeuten dasselbe, aber nil wird häufiger verwendet
  • Elemente einer Liste können Atome oder Listen sein
    • Beispiel: (zonk blee strill (croak flonk)) ist eine Liste mit vier Elementen, wobei das letzte Element wiederum eine Liste mit zwei Elementen ist
  • Auch Lisp-Sätze selbst sind Listen
    • (plus 2 2) ist ebenfalls eine Liste
    • Der Lisp-Interpreter kann Listen und Atome manipulieren, um neue „Befehle“ zu bilden und auszuwerten

Werte, quote, eval

  • Atome können Werte haben
    • Zahlenatome haben dauerhaft sich selbst als Wert
    • nil hat den Wert nil
    • t ist ebenfalls ein spezielles Atom, das dauerhaft sich selbst als Wert hat
  • setq weist einem Atom einen Wert zu
    • (setq pie 4) setzt den Wert von pie auf 4
    • (setq pie (plus 2 2)) setzt den Wert von pie ebenfalls auf 4
  • Der Wert eines Atoms muss nicht numerisch sein, sondern kann jedes Lisp-Objekt sein
    • also ein Atom oder eine Liste
  • quote verhindert die Auswertung und behandelt eine Liste oder ein Atom direkt als Daten
    • (setq pie (plus 2 2)) wertet den inneren Ausdruck aus und speichert 4 in pie
    • (setq pi '(plus 2 2)) speichert die Liste (plus 2 2) selbst in pi
  • eval wertet einen als Wert gespeicherten Ausdruck erneut aus
    • Wenn der Wert von pi (plus 2 2) ist, liefert (eval pi) den Wert 4 zurück
    • Der Interpreter wertet normalerweise nur eine Stufe aus, aber mit eval kann man eine zusätzliche Auswertung anfordern

car, cdr, cons

  • Jede Liste außer nil hat mindestens ein Element; das erste Element heißt car
    • Das car von (eval pi) ist eval
    • Das car von (plus 2 2) ist plus
  • Das car muss nicht unbedingt ein Funktionsname sein
    • Das car von ((1)(2 2) (3 3 3)) ist (1)
  • Entfernt man das car, erhält man den Rest der Liste, den cdr
    • Der cdr von (a b c d) ist (b c d)
    • Danach folgen (c d), (d) und nil
  • nil hat weder car noch cdr; der Versuch, car oder cdr davon zu nehmen, sollte einen Fehler auslösen
  • cons setzt vor eine bestehende Liste ein neues car und erzeugt so eine neue Liste
    • Wenn x den Wert (cake cookie) hat, erzeugt (cons 'pie x) die Liste (pie cake cookie)
    • Das ursprüngliche x wird dabei nicht verändert
  • Das Ergebnis hängt davon ab, ob quote verwendet wird oder nicht
    • Wenn pie den Wert 4 hat, liefert (cons pie x) (4 cake cookie)
    • (cons 'pie x) setzt dagegen den Atomnamen pie selbst an den Anfang

Der Unterschied zwischen setq und set

  • Der Lisp-Interpreter wertet nicht immer alle Argumente aus; setq ist ein typisches Beispiel für eine Ausnahme
  • Das erste Argument von setq wird nicht ausgewertet, sondern direkt als Variablenname behandelt
    • Das q in setq steht für quote; das erste Argument wird also behandelt, als wäre es quotiert
  • set ähnelt setq, wertet aber auch das erste Argument aus
    • Wenn x den Wert k hat, ändert (set x 7) nicht x, sondern setzt den Wert von k auf 7
  • Als Beispiel wird folgende Folge gezeigt
    • (setq a 'b)
    • (setq b 'c)
    • (setq c 'a)
    • (set a c)
    • (set c b)
  • Nach dieser Ausführung haben a, b und c jeweils alle den Wert a
  • set wird nicht oft verwendet, und solche Verwirrung kommt ebenfalls nicht häufig vor

Funktionsdefinition und lambda

  • Eine große Stärke des Programmierens liegt darin, neue zusammengesetzte Operationen aus vorhandenen Operationen zu definieren und dies immer weiter zu wiederholen, sodass ein immer komplexeres Repertoire entsteht
  • In Lisp definiert man neue Funktionen mit bereits bekannten Funktionen
  • rac ist ein Beispiel für eine Funktion, die das letzte Element einer Liste zurückgibt
    • Um das letzte Element zu erhalten, kann man die Liste umdrehen und dann car darauf anwenden
    • Definition: (def rac (lambda (lyst) (car (reverse lyst))))
  • (lyst) nach lambda bezeichnet den Parameter der Funktion, also eine Dummy-Variable
  • Nach der Definition mit def kann rac wie car als Funktion verwendet werden
    • (rac '(your brains)) liefert brains zurück
  • Die Funktion readers-digest-condensed-version erzeugt aus einer langen Liste eine kurze Liste, die nur das erste und das letzte Element enthält
    • Wenn man James Joyces Finnegans Wake als Wortliste behandelt, erhält man als Beispiel (riverrun the)
  • Eine Umkehrfunktion namens rejoyce wäre eine imaginäre Funktion, die einen Roman erzeugt, dessen Anfang und Ende durch zwei Wörter festgelegt sind; ihre Implementierung bleibt dem Leser als Übung überlassen

Rückgabewerte, Seiteneffekte, applicative style

  • Manche sehen in Lisp und verwandten Sprachen zwei Ziele als wünschenswert und erreichbar an
    • Jeder Satz liefert einen Wert zurück
    • Die Wirkung eines Satzes entsteht ausschließlich über diesen Rückgabewert
  • Die im Text behandelten Lisp-Dialekte erfüllen die erste Bedingung, aber nicht zwingend die zweite
  • (reverse x) verändert x selbst nicht, sondern erzeugt und liefert eine neue Liste zurück, die dieselben Elemente in umgekehrter Reihenfolge enthält
    • genauso wie (plus 2 2) den Wert von 2 nicht verändert
  • cons verändert bestehende Listen ebenfalls nicht, sondern gibt eine neue Liste zurück
  • setq ist dagegen ein Beispiel für einen Befehl mit Seiteneffekt, weil er Variablenbindungen verändert
    • Zu Seiteneffekten können neben Änderungen von Variablenbindungen auch Ein- und Ausgabe gehören
  • Anhänger des applicative programming bevorzugen ausschließlich eine Art des Programmierens, bei der Funktionen Werte ohne Seiteneffekte berechnen und weitergeben
  • Die einzige in diesem Stil erlaubte Bindung ist die vorübergehende lambda binding während eines Funktionsaufrufs
    • Nach Abschluss der Berechnung verschwinden die Bindungen der Dummy-Variablen wieder
  • Der Sprecher des Textes hält den applicative style für elegant, aber beim Bau großer Programme im Stil der KI nicht für praktikabel
  • In streng applicative programming wäre sogar def ein extremer Grenzfall, weil es dauerhafte Funktionsdefinitionen im Speicher ablegt

Bedingungsausdruck cond und Entscheidung

  • Damit Lisp interessantere Dinge tun kann, muss es abhängig davon, was im Zwischenprozess geschieht, Entscheidungen treffen können; dazu braucht es Bedingungsausdrücke
  • Beispiel: (cond ((eq x 1) 'land) ((eq x 2) 'sea))
    • Wenn x 1 ist, wird land zurückgegeben
    • Wenn x 2 ist, wird sea zurückgegeben
    • Sonst wird nil zurückgegeben
  • eq ist eine Lisp-Funktion, die t zurückgibt, wenn die Werte ihrer beiden Argumente gleich sind, sonst nil
  • Ein cond-Ausdruck beginnt mit dem Funktionsnamen cond und enthält mehrere Klauseln
    • Jede Klausel ist eine Liste mit zwei Elementen: einer Bedingung und einem Ergebnis
    • Die Bedingungen werden der Reihe nach geprüft; sobald die erste Bedingung einen Wert ungleich nil liefert, wird das zugehörige Ergebnis ausgewertet und als Wert des gesamten cond zurückgegeben
    • Die späteren Klauseln werden dann nicht mehr geprüft
  • Fügt man am Ende eine catch-all-Klausel mit t als Bedingung ein, kann man statt nil ein Standardergebnis zurückgeben
    • Beispielhaft wird ein cond gezeigt, das air zurückgibt, wenn weder die Bedingung für land noch die für sea erfüllt ist

Potenzen und rekursive Struktur

  • Gegen Ende des Textes erscheinen Funktionsdefinitionen mit einem klaren Muster
    • square ist k * k
    • cube ist k * square(k)
    • 4th-power ist k * cube(k)
    • und so weiter im selben Stil
  • Es wird die Frage gestellt, ob sich dieses gesamte Muster durch eine einzige Funktion power mit zwei Parametern erfassen lässt
    • (power 9 3) soll 729 ergeben
    • (power 7 4) soll 2.401 zurückgeben
  • Alle nötigen Werkzeuge wurden im Text bereits vorgestellt; nun ist die Geistesgegenwart des Lesers gefragt
  • Die abschließende Geschichte vom Glazunkian porpuquine behandelt rekursive Struktur in Form einer Parabel
    • Die Stacheln eines porpuquine sind jeweils kleinere porpuquines
    • In Outer Glazunkia hat jeder immer 9 Stacheln, in Inner Glazunkia immer 7
    • Ein porpuquine von 0 Zoll hat keine Stacheln und beendet so den unendlichen Regress
  • Die „buying power“ oder „power“ dieses Tiers steht mit der Zahl der darin enthaltenen kleinen Nasen von 0-Zoll-porpuquines in Verbindung und bildet die abschließende Metapher zum zuvor gestellten power-Problem

Kontext der Wiederveröffentlichung des Originaltexts

  • Im vorderen Teil des Gist wird erklärt, dass es sich bei diesem Text um Hofstadters Lisp-Einführung handelt, die der Autor Mitte der 1980er in einer älteren Ausgabe von Scientific American gefunden hatte
  • Die Beispiele lassen sich in Emacs noch ausführen, wenn man einige Aliase installiert
    • plus+
    • quotient/
    • times*
    • difference-
  • Am Ende heißt es, wer diesen eigenwilligen Lisp-Leitfaden von Hofstadter mochte, finde in seinem Buch Metamagical Themas weitere ähnliche Texte

1 Kommentare

 
GN⁺ 2024-10-18
Hacker-News-Kommentare
  • Die Funktionsnamen "oval" und "snot" im Beispiel haben mich verwirrt, aber nach ein paar Sekunden wurde mir klar, dass es Tippfehler für "eval" bzw. "snoc" sind.
    Der ursprüngliche Code sollte sich so lesen: (cond ((eq (eval pi) pie) (eval (snoc pie pi))) (t (eval (snoc (rac pi) pi))))
    Ich habe mir den originalen Scientific-American-Artikel unter https://www.jstor.org/stable/24968822 geholt und nachgesehen: Tatsächlich waren oval/snot Tippfehler für eval/snoc.

    • Korrektur der Korrektur: "oval" und "snot" sind tatsächlich Tippfehler für "eval" und "snoc".
      Und snoc scheint ein umgedrehtes cons zu sein, rac ein umgedrehtes car.
    • Es könnte auch ein OCR-Fehler sein.
  • Dieser Text zeigt erneut, dass ein Hauptgrund für die geringe Popularität von Lisp die Art war, wie Lisp erklärt wurde.
    Solche Lisp-Texte haben mir als Kind nichts gezeigt, was ich tatsächlich hätte benutzen können, und auch nicht, wie sehr eine Aufgabe X im Vergleich zu Assembler/C/Pascal usw. leichter würde.
    Ich heute hätte gezeigt: „Eine Rechtschreibprüfung, die in Assembler sieben Monate dauert? Auf einem Mikrocomputer mit Bank-Switching-Speicher ist das in Lisp fast trivial, und selbst auf einer miserablen CPU lässt sich Garbage Collection deterministisch machen.“
    Zahllose Lisp-Artikel und Lehrbücher wiederholten nur Listen, Rekursion und KI, ohne zu zeigen, wie man nützliche Dinge erledigt; dadurch fühlt sich die Zeit verschwendet an, als hätte man mit Pinzette, Reiskörnern und Klebstoff programmiert.

    • Common Lisp: A Gentle Introduction to Symbolic Computation könnte in diesem Kontext nützlich sein: https://www.cs.cmu.edu/~dst/LispBook/
    • Es gibt auch Practical Common Lisp: https://gigamonkeys.com/book/
    • In Hofstadters späterem Artikel gab es interessantere Beispiele.
      In der ersten Byte-Ausgabe, die ich als Kind sah, gab es Code für symbolische Differentiation und algebraische Vereinfachung in Lisp; er war schwer nachzuvollziehen, aber ich spürte, dass daran etwas Faszinierendes war.
      In Basic wäre es sicher nicht leichter gewesen, und später merkte ich, dass der Code nicht gerade hervorragend war; aber erst als ich Ende der 80er auf dem PC XLisp und SICP kennenlernte, verfiel ich Lisp so richtig.
  • Ich mag Hofstadters Schreibstil wirklich sehr. Er fängt gut ein, was ich empfand, als ich Lisp entdeckte.
    Ich war ein Kind, das in den 80ern Programmieren lernte, und hatte in der Highschool und zu Beginn des Studiums schon ein wenig BASIC, Fortran, Pascal und COBOL gemacht; trotz Unterschieden hatten sie im Kern etwas Gemeinsames.
    Doch der erste Informatikkurs an der UC Berkeley wurde in Scheme, einem Lisp-Dialekt, unterrichtet, und das war ein völliger Schock.
    Wie Hofstadter sagt, fühlte es sich der Mathematik am nächsten an, erinnerte mich stark an Kurse in mathematischer Theorie und war die erste schöne Sprache, die ich entdeckte.
    Besonders gefiel mir das Zitat: „Lisp und Algol sind um Kerne herum gebaut, die so natürlich sind wie ein Zweig der Mathematik. Im Kern von Lisp liegt eine kristalline Reinheit, die nicht nur den Sinn für Ästhetik anspricht, sondern es auch viel flexibler macht als die meisten Sprachen.“

    • Ich frage mich, ob du Haskell ausprobiert hast. Für mich fühlt es sich viel mathematischer an, weil es aus Definitionen statt aus Prozeduren besteht, und auch äußerlich sieht es wie Mathematik aus.
    • Auf die Gefahr hin, etwas vom ursprünglichen Thema abzuweichen: Ich glaube, wenn man das „Mathematik“ nennt, kann es die Sache eher vernebeln.
      Selbst aus der Perspektive von jemandem, der algebraischer Topologe werden wollte, ist Scheme eher eine elegante und minimale Darstellung eines Programmierstils, der in Sprachen mit dynamischer Typisierung und Garbage Collection allgemein verbreitet ist.
      Es wirkt vollständig und in dem Sinne wie „Theorie“, dass man in Scheme über eine Problemlösung nachdenken und sie anschließend in eine andere dynamisch typisierte Sprache übertragen kann, um zu einer eleganten Lösung zu gelangen.
      Scheme war im Vergleich zum traditionellen Lisp seiner Zeit eine vereinfachte und aufgeräumte Sprache, etwa durch lexikalischen Scope und einen einheitlichen Namensraum für Funktionen und Variablen.
  • 1983 lag der Beginn dieses Felds etwa 20 Jahre zurück, und hier ist von KI die Rede, die Computer so programmieren soll, dass sie „Flexibilität, gesunden Menschenverstand, Einsicht, Kreativität, Selbstbewusstsein und Humor“ besitzen.
    Es ist ziemlich amüsant, weil sich diese Liste wie eine Liste der Dinge liest, in denen LLMs wirklich schlecht sind.
    Trotzdem hat es in diese Richtung zumindest mehr als null Fortschritt gegeben.

    • Verglichen mit gewöhnlichen Menschen, denen ich begegne, scheinen LLMs gesunden Menschenverstand, Einsicht, Kreativität, Selbstbewusstsein und Humor mindestens auf dem Niveau eines durchschnittlichen Menschen zu zeigen.
      Vielleicht empfinde ich sogar, dass sie mehr davon zeigen, aber wenn ich das sage, klingt es selbst für mich etwas merkwürdig.
    • Hofstadters Forschungsgruppe hat eine lange Geschichte darin, sich mit solchen Problemen zu befassen.
      Auch wenn vieles davon noch nicht zu Ergebnissen geführt hat, ist es interessant zu lesen.
  • Die beiden Begleitartikel, die zusammen mit diesem Text erwähnt wurden, stehen in Hofstadters Buch Metamagical Themas in den Kapiteln 17 bis 19; dort finden sich auch weitere Texte aus derselben Kolumne im Scientific American.
    [0]: https://www.goodreads.com/book/show/181239.Metamagical_Thema...
    Der Buchtitel stammt vom Titel der Kolumne, einem Anagramm von Martin Gardners „Mathematical Games“ im Scientific American; Hofstadter übernahm diese Kolumne.

    • Ich liebe dieses Buch wirklich. Wer Hofstadters Klassiker Gödel, Escher, Bach mag, dem kann ich es sehr empfehlen.
    • Dieses Buch ist beim Lesen wie beim Anschauen wunderschön und enthält viele interessante Illustrationen.
  • Im Artikel heißt es, dass das Anwenden von car oder cdr auf nil einen Fehler wie eine Division durch 0 auslösen sollte; in modernen Lisp-Dialekten ist das aber oft nicht mehr der Fall.
    In dem ursprünglichen Lisp, das John McCarthy definierte, waren CAR und CDR für NIL nicht definiert: <https://dl.acm.org/doi/pdf/10.1145/367177.367199>
    Common Lisp und Emacs Lisp definieren (car nil) und (cdr nil) jedoch als nil: <https://www.lispworks.com/documentation/HyperSpec/Body/f_car...>, <https://www.gnu.org/software/emacs/manual/html_node/elisp/Li...>

    • Weil ich wissen wollte, wie es in Maclisp ist, habe ich mich per Telnet mit Lars Brinkhoffs öffentlichem ITS verbunden und es ausprobiert.
      In LISP 2156 gab (status lispversion) /2156 zurück, und (car nil) sowie (cdr nil) lieferten beide NIL.
    • Es ist erstaunlich, dass in die Entstehung einer mathematisch entworfenen Sprache so konkrete Implementierungsdetails wie car und cdr als zentrale Elemente eingegangen sind.
      Die grundlegendsten und bekanntesten Operatoren des „List Processor“ waren nicht für Listen gemacht, sondern für cons, ein bestimmtes Element der Maschinenrepräsentation, das Lisp zum Aufbau von Datenstrukturen verwendet.
      Ein cons wird auch nicht immer als Liste interpretiert, und die äußerst wichtige Liste, die den Basisfall rekursiver Listenfunktionen bildet, wird nicht einmal als cons dargestellt.
      Auch 60 Jahre später sind die meisten Lisp-Programme voller cons-Operationen; der genauere Name wäre vielleicht „Cons Processor“ gewesen.
      Das erinnert daran, dass Lisp in einer Zeit entstand, in der Sprache und Implementierung eng ineinandergreifen mussten, und dadurch wirkt die Leistung, eine Computersprache auf mathematischer Logik zu gründen, noch erstaunlicher.
    • Bei Scheme ist das nicht so. Wenn man CAR oder CDR von nil nimmt, ist das ein Fehler.
    • Leider ist car/cdr von nil in Scheme ein Fehler, sodass sich der Code für Anfänger wie mich ziemlich unergonomisch anfühlt.
      Trotzdem bevorzuge ich Guile Scheme gegenüber Common Lisp, was es umso bedauerlicher macht.
    • Ich frage mich, ob es einen Begriff für die Sprachdesign-Entscheidung gibt, dass das Anwenden von etwas auf nil ohne Fehler wieder nil zurückgibt.
      SQL kommt mir ebenfalls in den Sinn, und weil ich das persönlich für keine gute Entscheidung halte, würde ich es, falls es keinen anderen Begriff gibt, gern „bleeding nils/NULLs“ nennen.
      Besonders schlecht ist es, wenn nil in booleschen Vergleichen nicht dasselbe ist wie false.
      In Ruby und Elixir wird nil wie false behandelt; Elixir bietet getrennt and, das nur reine Boolesche Werte akzeptiert, und &&, das nil als false behandelt.
      Ein solches Design kann Code anfangs sauberer machen, aber ein falsch behandeltes nil, das eigentlich ein Fehler ist, kann erst viele Call-Stack-Ebenen später an einer ganz anderen Stelle sichtbar werden und das Debugging deutlich erschweren.
  • Die Lisp-Informationen in diesem Artikel kannte ich schon, aber ich habe ihn trotzdem mit Vergnügen gelesen. Hofstadters Sprachwitz ist wirklich reizvoll.
    Besonders lustig fand ich den Scherz, die inverse Operation zu readers-digest-condensed-version namens rejoyce zu erfinden, sodass beim Ausführen von (rejoyce 'Stately 'Yes) eine Lisp-Fee den gesamten Ulysses, wie James Joyce ihn hätte schreiben können, von Anfang an erzeugt.
    Es hat eine Weile gedauert, aber letztlich sind wir wohl an diesem Punkt angekommen; auch wenn die KI des Jahres 2024 nicht völlig dem entspricht, was er sich 1983 vorgestellt hat, passt das Reproduzieren von Text aus einem kurzen Seed heute ziemlich gut zu KI.

  • Lisp ist meiner Ansicht nach nach wie vor eine der wichtigsten Sprachen, mit denen sich Hofstadters Konzept der seltsamen Schleifen ausdrücken lässt.
    Lisp ist nicht die einzige homoikonische Sprache, gehört aber unter den Sprachen, die Menschen tatsächlich beherrschen, zu den größten, bei denen eval keinen zu parsenden String entgegennimmt.
    Ich mag es nicht, wenn Leute Lisp kurzerhand mit funktionaler Programmierung insgesamt gleichsetzen.
    Ich habe nichts gegen funktionale Programmierung, aber der symbolische Charakter von Lisp ist viel interessanter; und dass man Code schreiben kann, der über (go tag)-Abschnitte läuft, wodurch auch GOTO-artige Programmierung sehr einfach möglich ist, finde ich unendlich faszinierend.

    • Eine weitere, wirklich funktionale homoikonische Sprache, die in den 2000er-Jahren kurzzeitig eine gewisse Mainstream-Verbreitung hatte, war XSLT.
      Metaprogrammierung, bei der man mit XSLT und XML XSLT erzeugte, statt repetitiven Code direkt zu schreiben, wurde recht breit genutzt.
      Allerdings war die Syntax ein noch größeres Problem als bei Lisp.
      Es ist nicht leicht, eine Sprache zu schaffen, deren Syntax sich im Alltag gut verwenden lässt und die zugleich als abstrakter Syntaxbaum nicht zu schwer handhabbar ist; Lisp ist eines der wenigen Beispiele, bei denen das vergleichsweise gut gelungen ist.
    • Besonders wenn man bis zur fortsetzungsbasierten Programmierung in Scheme geht, verschwimmt die Grenze zwischen funktionaler und nicht-funktionaler Programmierung fast bis zur Bedeutungslosigkeit.
    • Mich interessiert, was genau die tiefgehende homoikonische/symbolische Unterstützung ist, die Programmierer im Jahr 2024 aus Lisp bekommen und die sich mit Pythons functools nicht gut abbilden lässt: https://docs.python.org/3/library/functools.html
      Ich baue eine symbolische AGI ohne Lisp und würde gern Hinweise von Experten hören.
      So wie ich die Python-Funktionen verstehe, sind das Funktionen wie filter(), map() und reduce(), die andere Funktionen auf iterierbare Objekte anwenden; Wrapper wie @singledispatch, die Aufrufe routen; Funktionen wie @cache und partial(), die bei Ablaufsteuerung oder Performance helfen; sowie Möglichkeiten wie wraps(), Funktionen beliebig zu umhüllen.
      Das wirkt größtenteils wie Komfortfunktionen, um Funktionen auf ungewöhnliche Weise aufzurufen, und scheint nicht an das Niveau der Lisp-Lobpreisungen heranzureichen, die „Selbstreflexion“ als Anliegen erster Klasse behandeln.
      Ich möchte wissen, was Lisp tatsächlich bietet, das in den oben genannten Funktionen fehlt.
    • Das liegt auch daran, dass viele Menschen Lisp nur über die akademische Welt kennenlernen und die Wissenschaftler, die Lisp lehren, kein Interesse daran haben, tatsächlich etwas in Lisp zu entwickeln.
      Sie verwenden Lisp als Mittel zur Vermittlung von Konzepten, und diese Konzepte drehen sich meist um funktionale Rekursion.
      Auch Scheme und die Kultur darum herum tragen dazu bei.
      Scheme ist zwar nicht nur eine funktionale Sprache, betont aber stärker als frühere Lisp-Dialekte reine Programmierung; die Basissprache stellt statt Iterationskonstrukten Strukturen für Tail Recursion bereit und verlangt von Implementierungen Tail-Call-Optimierung.
  • Auch in Common Lisp kann man defalias als Makro definieren und es wie Emacs’ defalias verwenden: https://stackoverflow.com/questions/24252539/defining-aliase...

  • Ich mochte Hofstadters Lisp-Artikel in Metamagical Themas und habe den Code aus dem letzten Beitrag dieser Reihe für eine Studiengruppe bei der Arbeit nach Clojure übertragen und aufbereitet.
    [1] http://johnj.com/posts/oodles/

    • Mich würde auch interessieren, ob es eine Übersetzung in ein modernes Lisp gibt.