Ausgezeichnetes Lisp-Programmierstil-Tutorial: Kapitel 4 (Übersetzung)

Ursprünglicher Link: https://norvig.com/luv-slides.ps

4. Abstraktion

Alle Programmiersprachen ermöglichen Entwicklern die Definition von Abstraktionen. Alle modernen Sprachen bieten Unterstützung für:

  • Datenabstraktion (abstrakter Datentyp)
  • Funktionsabstraktion (Funktion, Prozedur)

Lisp und andere Sprachen mit Schließungen (z. B. ML, Sather) unterstützen:

  • Kontrollflussabstraktion (definiert Iteratoren und andere Kontrollflusskonstrukte)

Lisp ist einzigartig in der Unterstützung von:

  • Syntaktische Abstraktion (Makro, völlig neue Sprache)

Design: Wo Stil beginnt

„Der wichtigste Teil beim Schreiben eines Programms ist das Entwerfen der Datenstrukturen, der zweitwichtigste Teil ist das Zerlegen in Codeblöcke“ – Bill Gates

„Professionelle Ingenieure schichten komplexe Designs. … Die auf jeder Ebene konstruierten Teile werden als Grundelemente in der nächsten Schicht verwendet. Jede Schicht eines mehrschichtigen Designs kann als eine spezielle Sprache betrachtet werden, die auf dieser Detailebene über verschiedene Grundelemente und Kombinationsmethoden verfügt. „ – Harold Abelson und Gerald Sussman

„Entscheidungen so weit wie möglich herunterbrechen. Aspekte aussortieren, die unabhängig erscheinen. Entscheidungen, die Präsentationsdetails betreffen, so lange wie möglich aufschieben.“ – Niklaus Wirth

Lisp unterstützt alle folgenden Methoden:

  • Datenabstraktion: Klassen, Strukturen,deftype
  • Funktionale Abstraktion: Funktionen, Methoden
  • Schnittstellenabstraktion: Paket, Abschluss
  • Objektorientiert: CLOS, Schließung
  • Mehrschichtiges Design: Verschlüsse, alles oben Genannte
  • Verzögerte Entscheidungen: Laufzeitversand

Design: Explodiert

„Ein Lisp-Verfahren ist wie ein Absatz“ – Deborah Tatar

„Sie sollten in der Lage sein, jedes Modul in einem Satz zu erklären“ – Wayne Ratliff

  • Streben Sie ein einfaches Design an
  • Das Problem in Teile zerlegen
    Nützliche Unterteile entwerfen (Schichtung)
    Spekulativ; Verwendung vorhandener Werkzeuge
  • Abhängigkeiten identifizieren
    Modul neu gestalten, um Abhängigkeiten zu reduzieren
    Entwerfen Sie zuerst die am stärksten abhängigen Teile

Wir werden die folgenden Abstraktionen einführen:

  • Datenabstraktion
  • Funktionsabstraktion
  • Kontrollflussabstraktion
  • Syntaktische Abstraktion

4.1 Datenabstraktion

Schreiben Sie Code basierend auf den Datentypen des Problems, nicht auf den Datentypen in der Implementierung.

  • Verwenden Siedefstructoder für den Datensatztypdefclass
  • Verwenden Sie Inline-Funktionen als Aliase (keine Makros)
  • verwendendeftype
  • Verwenden Sie für Effizienz und/oder Dokumentation Deklarationen und:typeSlots
  • Variablennamen stellen informelle Typinformationen bereit

Sehr gut: Geben Sie einige Typinformationen an

(defclass event ()
  ((starting-time :type integer)
   (location :type location)
   (duration :type integer :initform 0)))

Besser: Fragenspezifische Typinformationen

(deftype time () "Time in seconds" 'integer)

(defconstant +the-dawn-of-time+ 0
  "Midnight, January 1, 1900")

(defclass event ()
  ((starting-time :type time :initform +the-dawn-of-time+)
   (location :type location)
   (duration :type time :initform 0)))
Verwenden Sie abstrakte Datentypen

Einführung abstrakter Datentypen mit Accessoren

Breaking: mehrdeutige Zugriffsmethoden und Auswertung

(if (eval (cadar rules)) ...)

Besser: Namen für Accessoren einführen

(declaim (inline rule-antecedent))
(defun rule-antecedent (rule) (second rule))

(if (holds? (rule-antecedent (first rules))) ...)

normalerweise am besten : erstklassige Datentypen einführen

(defstruct rule
  name antecedent consequent)

oder

(defstruct (rule (:type list))
  name antecedent consequent)

oder

(defclass rule ()
  (name antecedent consequent))
Implementieren Sie abstrakte Datentypen

Erfahren Sie, wie Sie gängige abstrakte Datentypen auf Lisp-Implementierungen abbilden.

  • Sammlungen: Liste, Bitvektor, Ganzzahl, jeder Tabellentyp
  • Reihenfolge: Liste, Vektor, verzögerter Auswertungsstrom
  • Stapel: Liste, Vektor (mit Füllzeiger)
  • Warteschlange: tconc, Vektor (mit Füllzeiger)
  • Tabelle: Hash-Tabelle, Alist, Plist, Vektor
  • Baum, Diagramm: Nachteile, Struktur, Vektor, Adjazenzmatrix

Verwenden Sie bereits unterstützte Implementierungen (z. B. Union, Schnittmenge, Länge für Listensammlungen; Logior, Logand, Logcount für Ganzzahlsammlungen).

Wenn die Analyse Engpässe aufdeckt, scheuen Sie sich nicht, eine neue Implementierung zu erstellen. (Wenn die Hash-Tabellen von Common Lisp für Ihre Anwendung zu ineffizient sind, sollten Sie erwägen, eine spezielle Hash-Tabelle in Lisp zu erstellen, bevor Sie eine spezielle Hash-Tabelle in C erstellen.)

Vom Datentyp erben

Wiederverwendung durch Vererbung und direkte Nutzung

  • Die Struktur unterstützt die Einzelvererbung
  • Die Klasse unterstützt Mehrfachvererbung
  • Sie alle unterstützen das Umschreiben
  • Klassen unterstützen Mixins

Betrachten Sie eine Klasse oder Struktur, die im gesamten Programm verwendet wird

  • Beseitigt das Durcheinander globaler Variablen
  • Thread-Sicherheit
  • Kann vererbt und geändert werden

4.2 Funktionsabstraktion

Jede Funktion sollte Folgendes haben:

  • ein unabhängiger und spezifischer Zweck
  • Wenn möglich, ein allgemein nützlicher Zweck
  • Ein aussagekräftiger Name (ein Name wierecurse-aux weist auf ein Problem hin)
  • eine leicht verständliche Struktur
  • Eine einfache, aber allgemein genug Schnittstelle
  • Haben Sie so wenig Abhängigkeiten wie möglich
  • ein Dokumentstring
abbauen

Zerlegen Sie Algorithmen in einfache, sinnvolle und nützliche Funktionen.

Das Beispiel in comp.lang.lisp diskutiert den Vergleich von loop und map.

(defun least-common-superclass (instances)
  (let ((candidates
         (reduce #'intersection
                (mapcar #'(lambda (instance)
                            (clos:class-precedence-list
                             (class-of instance)))
                        instances)))
        (best-candidate (find-class t)))
    (mapl
     #'(lambda (candidates)
         (let ((current-candidate (first candidates))
               (remaining-candidates (rest candidates)))
           (when (and (subtypep current-candidate
                                best-candidate)
                      (every
                       #'(lambda (remaining-candidate)
                           (subtypep current-candidate
                                     remaining-candidate))
                       remaining-candidates))
             (setf best-candidate current-candidate))))
     candidates)
    best-candidate))

Sehr gut: Chris Riesbeck

(defun least-common-superclass (instances)
  (reduce #'more-specific-class
          (common-superclasses instances)
          :initial-value (find-class 't)))

(defun common-superclasses (instances)
  (reduce #'intersection
          (superclass-lists instances)))

(defun superclass-lists (instances)
  (loop for instance in instances
        collect (clos:class-precedence-list
                 (class-of instance))))

(defun more-specific-class (class1 class2)
  (if (subtypep class2 class1) class2 class1))
  • Jede Funktion ist leicht zu verstehen
  • Klare Kontrollstruktur: zweireduce, eine Kreuzung und eine Schleife/Sammeln
  • Die Wiederverwendbarkeit ist jedoch recht gering

Genauso gut: mehr Wiederverwendbarkeit

(defun least-common-superclass (instances)
  "Find a least class that all instances belong to."
  (least-upper-bound (mapcar #'class-of instances)
                     #'clos:class-precedence-list
                     #'subtypep))

(defun least-upper-bound (elements supers sub?)
  "Element of lattice that is a super of all elements."
  (reduce #'(lambda (x y)
              (binary-least-upper-bound x y supers sub?))
          elements))

(defun binary-least-upper-bound (x y supers sub?)
  "Least upper bound of two elements."
  (reduce-if sub? (intersection (funcall supers x)
                                (funcall supers y))))

(defun reduce-if (pred sequence)
  "E.g. (reduce-if #'> numbers) computes maximum"
  (reduce #'(lambda (x y) (if (funcall pred x y) x y))
          sequence))
  • Eine einzelne Funktion ist noch verständlich
  • immer noch zweireduce, einer kreuzt sich und einermapcar
  • Das mehrschichtige Design bietet weitere nützliche Funktionen
Prinzipien der englischen Übersetzung

Stellen Sie sicher, dass Sie sagen, was Sie meinen:

  1. Beginnen Sie mit einer englischen Beschreibung des Algorithmus
  2. Schreiben Sie den Code gemäß der Beschreibung
  3. Übersetzen Sie den Code zurück ins Englische
  4. Vergleichen Sie die Ergebnisse von Punkt 3 mit Punkt 1

Beispiel:

  1. „Bestimmen Sie anhand einer Liste von Monstern, wie viele davon Schwärme sind.“

(defun count-swarm (monster-list)
  (apply '+
         (mapcar
          #'(lambda (monster)
              (if (equal (object-type
                          (get-object monster))
                         'swarm)
                  1
                  0))
          monster-list)))
  1. „Nehmen Sie die Liste der Monster und geben Sie eine 1 für ein Monster aus, dessen Typ Schwarm ist, und eine 0 für die anderen. Dann addieren Sie die Zahlenliste.“
ein besserer:
  1. „Bestimmen Sie anhand einer Liste von Monstern, wie viele davon Schwärme sind.“

(defun count-swarms (monster-names)
  "Count the swarms in a list of monster names."
  (count-if #'swarm-p monster-names :key #'get-object))

oder

(count 'swarm monster-names :key #'get-object-type)

oder

(loop for name in monster-names
      count (swarm-p (get-object monster)))
  1. „Zählen Sie anhand einer Liste mit Monsternamen die Anzahl
    , die Schwärme sind.“
Verwenden Sie Bibliotheksfunktionen

Bibliotheksfunktionen haben Zugriff auf effiziente Hacks auf niedriger Ebene, und häufig werden neue Optimierungen vorgenommen.

Sie können jedoch zu allgemein und daher unwirksam sein.

Wenn Effizienz zum Problem wird, schreiben Sie spezifische Versionen.

Gut: Spezifisch und prägnant

(defun find-character (char string)
  "See if the character appears in the string."
  (find char string))

Gut: Effizient

(defun find-character (char string)
  "See if the character appears in the string."
  (declare (character char) (simple-string string))
  (loop for ch across string
        when (eql ch char) return ch))

Gegebenes abuild1, es wirdn einerListe von zugeordnet :nx
(build1 4)) -> (x x x x)

Termin:Verwendungbuild-itVerwendung:

(build-it '(4 0 3))) -> ((x x x x) () (x x x))

Sehr zerbrechlich:

(defun round3 (x)
  (let ((result '()))
    (dotimes (n (length x) result)
      (setq result (cons (car (nthcdr n x)) result)))))

(defun build-it (arg-list)
  (let ((result '()))
    (dolist (a (round3 arg-list) result)
      (setq result (cons (build1 a) result)))))

Frage:

  • round3ist nur ein anderer Name für reverse
  • (car (nthcdr n x))das ist(nth n x)
  • ZareridolistVerhältnisdotimesmehr
  • Hierpushpasst
  • (mapcar #'build1 numbers)Wir können alles machen

Abstraktion der Kontrollstruktur

Die meisten Algorithmen können wie folgt beschrieben werden:

  • Suche (einige finden Find-If-Diskrepanz)
  • Sortieren (sortieren, zusammenführen, entfernen-Duplikate)
  • Filter (entfernen, entfernen, wenn Mapcan)
  • Kartierung (Karte Mapcar Mapc)
  • Vergleichen (Mapcan reduzieren)
  • zählen (zählen, zählen-wenn)

Diese Funktionen abstrahieren gemeinsame Kontrollmuster. Der Code, um sie zu verwenden, ist:

  • Prägnant
  • Beschreibung der Selbstdokumentation
  • einfach zu verstehen
  • Oft wiederverwendbar
  • Normalerweise effizient (besser als eine Nicht-Tail-Rekursion)

Die Einführung eigener Kontrollabstraktionen ist ein wichtiger Bestandteil des mehrschichtigen Designs.

Rekursion vs. Iteration

Rekursion ist für rekursive Datenstrukturen von Vorteil. Viele Leute ziehen es vor, die Liste als Sequenz zu behandeln und sie zu iterieren, wobei sie die Implementierungsdetails der Liste, die in Kopf und Rest aufgeteilt wird, ignorieren.

Als Ausdrucksstil wird die Schwanzrekursion oft als elegant angesehen. Common Lisp garantiert jedoch nicht die Eliminierung der Tail-Rekursion und sollte daher nicht als Ersatz für die Iteration in vollständig portablem Code verwendet werden. (In Scheme gibt es kein Problem.)

Das Common Lisp do-Makro kann als syntaktischer Zucker für die Schwanzrekursion betrachtet werden, wobei der Anfangswert der Variablen der Parameterwert des ersten Funktionsaufrufs ist und der Schrittwert der Wert nachfolgender Funktionsaufrufe. Parameterwert.

doBietet eine Abstraktion auf niedriger Ebene, ist jedoch generisch und verfügt über ein einfaches, explizites Ausführungsmodell.

Breaking: (in Common Lisp)

(defun any (lst)
  (cond ((null lst) nil)
        ((car lst) t)
        (t (any (cdr lst)))))

Besser: Üblich, prägnant

(defun any (list)
  "Return true if any member of list is true."
  (some #'not-null list))

oder

(find-if-not #'null lst)

oder

(loop for x in list thereis x)

oder (explizit)

(do ((list list (rest list)))
    ((null list) nil)
  (when (first list))
    (return t))

Am besten: Effizient, das einfachste in diesem Beispiel

Unnötiges Tuningany!

Verwenden Sie(some p list) anstelle von(any (mapcar p list))

SCHLEIFE

„Bleiben Sie bei einem Thema, wie einem Brief an Ihren Senator.“ – Judy Anderson

Common Lisploop-Makros ermöglichen es Ihnen, Redewendungen prägnant auszudrücken. Allerdings sind seine Syntax und Semantik oft viel komplexer als seine Alternativen.

Ob man Makros verwenden sollloopist ein kontroverses Thema, fast ein Religionskrieg. Die Ursache dieses Konflikts ist die folgende etwas paradoxe Beobachtung:

  • loopEs ist für naive Programmierer attraktiv, weil es englisch aussieht und weniger Programmierkenntnisse zu erfordern scheint als seine Alternativen.
  • loopNicht Englisch; seine Syntax und Semantik weisen subtile Komplexitäten auf, die die Ursache für viele Programmierfehler sind. Normalerweise wird es am besten von denjenigen verwendet, die sich die Zeit genommen haben, es zu studieren und zu verstehen (normalerweise keine naiven Programmierer).

Nutzen Sie die einzigartigen Funktionenloop (z. B. verschiedene Arten paralleler Iterationen)

Einfache Iteration

Schlecht: ausführliche, unklare Kontrollstruktur

(LOOP
  (SETQ *WORD* (POP *SENTENCE*))  ;get the next word
  (COND
   ;; if no more words then return instantiated CD form
   ;; which is stored in the variable *CONCEPT*
   ((NULL *WORD*)
    (RETURN (REMOVE-VARIABLES (VAR-VALUE '*CONCEPT*))))
   (T (FORMAT T "~%~%Processing ~A" *WORD*)
      (LOAD-DEF)   ; look up requests under
                   ; this word
      (RUN-STACK)))) ; fire requests
  • Keine Notwendigkeit für globale Variablen
  • Der Endtest ist irreführend
  • Es ist nicht klar, was mit jedem Wort gemacht wird

Gut: üblich, prägnant, klar

(mapc #'process-word sentence)
(remove-variables (var-value '*concept*))

(defun process-word (word)
  (format t "~2%Processing ~A" word)
  (load-def word)
  (run-stack))
Kartierung

Schlecht: ausführlich

; (extract-id-list 'l_user-recs) ------------- [lambda]
; WHERE: l_user-recs is a list of user records
; RETURNS: a list of all user id's in l_user-recs
; USES: extract-id
; USED BY: process-users, sort-users

(defun extract-id-list (user-recs)
  (prog (id-list)
  loop
    (cond ((null user-recs)
           ;; id-list was constructed in reverse order
           ;; using cons, so it must be reversed now:
           (return (nreverse id-list))))
    (setq id-list (cons (extract-id (car user-recs))
                        id-list))
    (setq user-recs (cdr user-recs)) ;next user record
    (go loop)))

Gut: üblich, prägnant

(defun extract-id-list (user-record-list)
  "Return the user ID's for a list of users."
  (mapcar #'extract-id user-record-list))
zählen

Schlecht: ausführlich

(defun size ()
  (prog (size idx)
    (setq size 0 idx 0)
   loop
     (cond ((< idx table-size)
            (setq size (+ size (length (aref table idx)))
                  idx (1+ idx))
            (go loop)))
     (return size)))

Gut: üblich, prägnant

(defun table-count (table) ; Formerly called SIZE
  "Count the number of keys in a hash-like table."
  (reduce #'+ table :key #'length))

Außerdem schadet es nicht, Folgendes hinzuzufügen:

(deftype table ()
  "A table is a vector of buckets, where each bucket
  holds an alist of (key . values) pairs."
  '(vector cons))
Filter

Schlecht: ausführlich

(defun remove-bad-pred-visited (l badpred closed)
  ;;; Returns a list of nodes in L that are not bad
  ;;; and are not in the CLOSED list.
  (cond ((null l) l)
        ((or (funcall badpred (car l))
             (member (car l) closed))
         (remove-bad-pred-visited
          (cdr l) badpred closed))
        (t (cons (car l)
                 (remove-bad-pred-visited
                  (cdr l) badpred closed)))))

Gut: üblich, prägnant

(defun remove-bad-or-closed-nodes (nodes bad-node? closed)
  "Remove nodes that are bad or are on closed list"
  (remove-if #'(lambda (node)
                 (or (funcall bad-node? node)
                     (member node closed)))
             nodes))
Kontrollfluss: Halten Sie es einfach

Die nicht-lokale Kontrolle ist heute schwer zu verstehen

Schlecht: ausführlich, Verletzung der referenziellen Transparenz

(defun isa-test (x y n)
  (catch 'isa (isa-test1 x y n)))

(defun isa-test1 (x y n)
  (cond ((eq x y) t)
        ((member y (get x 'isa)) (throw 'isa t))
        ((zerop n) nil)
        (t (any (mapcar
                #'(lambda (xx)
                    (isa-test xx y (1- n)) )
                (get x 'isa) ))) ) )

Frage:

  • catch/throwEs gibt keinen Grund
  • memberTests können hilfreich sein oder auch nicht
  • mapcarMüll erzeugen
  • anyTesten zu spät;throwDer Versuch, das Problem zu beheben, führt dazu, dassanynie angerufen wird

Einige Vorschläge zur Verwendungcatch und throw:

  • Verwenden Sie catch und throw als Unterprimitive, wenn Sie abstraktere Kontrollstrukturen in Form von Makros implementieren, jedoch nicht in normalem Code.
  • Wenn Sie ein Capture erstellen, muss Ihr Programm manchmal dessen Existenz testen. In diesem Fall ist möglicherweise ein Neustart sinnvoller.

OK:

(defun isa-test (sub super max-depth)
  "Test if SUB is linked to SUPER by a chain of ISA
  links shorter than max-depth."
  (and (>= max-depth 0)
       (or (eq sub super)
           (some #'(lambda (parent)
                     (isa-test parent super
                               (- max-depth 1)))
                 (get sub 'isa)))))

Auch gut: Werkzeuge verwenden

(defun isa-test (sub super max-depth)
  (depth-first-search :start sub :goal (is super)
                      :successors #'get-isa
                      :max-depth max-depth))

„Schreiben Sie klar | seien Sie nicht zu schlau.“ – Kernighan & Plauger

Realisieren:

Verändert das „Verbessern“ von etwas die Semantik? Ist es wichtig?

Vermeiden Sie komplexe Lambda-Ausdrücke

Wenn Funktionen höherer Ordnung komplexe Lambda-Ausdrücke erfordern, ziehen Sie andere Optionen in Betracht:

  • dolistoderloop
  • Generieren Sie Zwischensequenzen (Müll).
  • Serie
  • Makro oder Lesemakro
  • lokale Funktion

– Spezifisch: klar, wo die Funktion verwendet wird
– Der globale Namespace wird nicht überladen
– Lokale Variablen müssen keine Parameter sein
– Aber: Einige Debugging-Tools funktionieren nicht

Ermitteln Sie die Summe der Quadrate ungerader Zahlen in einer Folge ganzer Zahlen:

Alles ist gut:

(reduce #'+ numbers
        :key #'(lambda (x) (if (oddp x) (* x x) 0)))

(flet ((square-odd (x) (if (oddp x) (* x x) 0)))
  (reduce #'+ numbers :key #'square-odd))

(loop for x in list
      when (oddp x) sum (* x x))

(collect-sum (choose-if #'oddp numbers))

Bedenken Sie auch: (kann manchmal angebracht sein)

;; Introduce read macro:
(reduce #'+ numbers :key #L(if (oddp _) (* _ _) 0))

;; Generate intermediate garbage:
(reduce #'+ (remove #'evenp (mapcar #'square numbers)))
Funktionaler Stil vs. zwingender Stil

Einige argumentieren, dass es schwieriger sei, über Programme im imperativen Stil nachzudenken. Hier ist ein Fehler, der von imperativen Methoden herrührt:

Aufgabe: Schreiben Sie eine Version der integrierten Funktionfind.

Schlecht: Falsch

(defun i-find (item seq &key (test #'eql) (test-not nil)
               (start 0 s-flag) (end nil)
               (key #'identity) (from-end nil))
  (if s-flag (setq seq (subseq seq start)))
  (if end (setq seq (subseq seq 0 end)))
  ...)

Frage:

  • Das Abrufen von Teilsequenzen erzeugt Müll
  • Stellt keine Listen-/Vektorunterschiede dar
  • Wenn sowohl start als auch end angegeben sind, tritt aufgrund der Aktualisierung von seq ein Fehler auf a>
Beispiel: Vereinfachen

Aufgabe: Vereinfachung logischer Ausdrücke

(simp '(and (and a b) (and (or c (or d e)) f)))
-> (AND A B (OR C D E) F)

Nicht lang, aber nicht perfekt

(defun simp (pred)
  (cond ((atom pred) pred)
        ((eq (car pred) 'and)
         (cons 'and (simp-aux 'and (cdr pred))))
        ((eq (car pred) 'or)
         (cons 'or (simp-aux 'or (cdr pred))))
        (t pred)))

(defun simp-aux (op preds)
  (cond ((null preds) nil)
        ((and (listp (car preds))
              (eq (caar preds) op))
         (append (simp-aux op (cdar preds))
                 (simp-aux op (cdr preds))))
        (t (cons (simp (car preds))
                 (simp-aux op (cdr preds))))))

Frage:

  • bedeutungsloser Namesimp-aux
  • keine wiederverwendbaren Teile
  • Kein Datenzugriffsmechanismus
  • (and)(and a)vorstellbar und vereinfacht

Besser: Verfügbare Tools

(defun simp-bool (exp)
  "Simplify a boolean (and/or) expression."
  (cond ((atom exp) exp)
        ((member (op exp) '(and or))
         (maybe-add (op exp)
                    (collect-args
                     (op exp)
                     (mapcar #'simp-bool (args exp)))))
        (t exp)))

(defun collect-args (op args)
  "Return the list of args, splicing in args
  that have the given operator, op. Useful for
  simplifying exps with associate operators."
  (loop for arg in args
        when (starts-with arg op)
        nconc (collect-args op (args arg))
        else collect arg))
Erstellen Sie wiederverwendbare Werkzeuge
(defun starts-with (list element)
  "Is this a list that starts with the given element?"
  (and (consp list)
       (eql (first list) element)))

(defun maybe-add (op args &optional
                     (default (get-identity op)))
  "If 1 arg, return it; if 0, return the default.
  If there is more than 1 arg, cons op on them.
  Example: (maybe-add 'progn '((f x))) ==> (f x)
  Example: (maybe-add '* '(3 4)) ==> (* 3 4).
  Example: (maybe-add '+ '()) ==> 0,
  assuming 0 is defined as the identity for +."
  (cond ((null args) default)
        ((length=1 args) (first args))
        (t (cons op args))))

(deftable identity
  :init '((+ 0) (* 1) (and t) (or nil) (progn nil)))

4.4 Syntaktische Abstraktion

eine vereinfachte Sprache

Aufgabe: ein Vereinfacher für alle folgenden Ausdrücke:

(simplify '(* 1 (+ x (- y y)))) ==> x
(simplify '(if (= 0 1) (f x))) ==> nil
(simplify '(and a (and (and) b))) ==> (and a b)

Die syntaktische Abstraktion definiert eine neue, für das Problem geeignete Sprache.

Dies ist ein problemorientierter (im Gegensatz zum Code-orientierter) Ansatz.

Definieren Sie eine Sprache, die die Regeln vereinfacht, und schreiben Sie:

(define-simplifier exp-simplifier
  ((+ x 0) ==> x)
  ((+ 0 x) ==> x)
  ((- x 0) ==> x)
  ((- x x) ==> 0)
  ((if t x y) ==> x)
  ((if nil x y) ==> y)
  ((if x y y) ==> y)
  ((and) ==> t)
  ((and x) ==> x)
  ((and x x) ==> x)
  ((and t x) ==> x)
  ...)
Gestalten Sie Ihre Sprache sorgfältig

„Die Fähigkeit, Notationen zu ändern, stärkt den Menschen.“ – Scott Kim

Schlecht: ausführlich, fragil

(setq times0-rule '(
  simplify
  (* (? e1) 0)
  0
  times0-rule
  ) )

(setq rules (list times0-rule ...))
  • unzureichende Abstraktion
  • Name muss dreimal angegeben werdentimes0-ruledreimal
  • Führen Sie unnötige globale Variablen ein
  • Unkonventionelle Einrückung

Manchmal ist es nützlich, Ihre Regeln wie folgt zu benennen:

(defrule times0-rule
  (* ?x 0) ==> 0)

(Obwohl ich es in diesem Fall nicht empfehlen würde.)

Ein Dolmetscher zur Vereinfachung

Jetzt schreiben wir einen Interpreter (oder einen Compiler):

(defun simplify (exp)
  "Simplify expression by first simplifying components."
  (if (atom exp)
    exp
    (simplify-exp (mapcar #'simplify exp))))

(defun-memo simplify-exp (exp)
  "Simplify expression using a rule, or math."
  ;; The expression is non-atomic.
  (rule-based-translator exp *simplification-rules*
    :rule-pattern #'first
    :rule-response #'third
    :action #'simplify
    :otherwise #'eval-exp))

Diese Lösung ist besser, weil:

  • Vereinfachte Regeln sind einfach zu schreiben
  • Der Kontrollfluss ist (meistens) abstrahiert
  • Es ist leicht zu überprüfen, ob die Regeln korrekt sind
  • Das Programm ist schnell einsatzbereit

Wenn die Methode ausreicht, sind wir fertig. Wenn diese Methode nicht ausreicht, sparen wir Zeit. Wenn es nur langsam ist, können wir diese Tools verbessern und andere Anwendungen dieser Tools werden davon profitieren.

ein Dolmetscher für die Übersetzung

„Erfolg entsteht dadurch, dass man immer wieder das Gleiche tut; Jedes Mal lernt man ein bisschen und macht es beim nächsten Mal ein bisschen besser.“ – Jonathan Sachs

Zusammenfassung des regelbasierten Übersetzers:

(defun rule-based-translator
  (input rules &key (matcher #'pat-match)
         (rule-pattern #'first) (rule-response #'rest)
         (action #identity) (sub #'sublis)
         (otherwise #'identity))
  "Find the first rule that matches input, and apply the
  action to the result of substituting the match result
  into the rule's response. If no rule matches, apply
  otherwise to the input."
  (loop for rule in rules
    for result = (funcall matcher
                   (funcall rule-pattern rule) input)
    when (not (eq result fail))
    do (RETURN (funcall action
                 (funcall sub result
                   (funcall rule-response rule))))
    finally (RETURN (funcall otherwise input))))

Wenn diese Implementierung zu langsam ist, können wir sie besser indizieren oder kompilieren.

Manchmal erfolgt die Wiederverwendung auf einer informellen Ebene: Wenn man sich ansieht, wie man ein gemeinsames Tool erstellt, entwickeln Programmierer benutzerdefinierte Tools durch Ausschneiden und Einfügen.

Doppelte Arbeit speichern: defun-memo

Ein extremerer Ansatz als das Entwerfen einer völlig neuen Sprache besteht darin, Lisp mit neuen Makros zu erweitern.

defun-memo bewirkt, dass eine Funktion alle von ihr durchgeführten Berechnungen speichert. Dies geschieht durch die Verwaltung einer Hash-Tabelle mit Eingabe-/Ausgabepaaren. Wenn der erste Parameter nur der Funktionsname ist, passiert eines von zwei Dingen: [1] Wenn es nur einen Parameter gibt und es sich nicht um einen &rest-Parameter handelt, wird er für diesen Parameter erstellt EineeqlTabelle. [2]Andernfalls wird eine equalTabelle über die gesamte Argumentliste generiert.

Sie können fn-name auch durch (name :test ... :size ... :key-exp ...) ersetzen. Dadurch wird eine Tabelle mit den angegebenen Tests und der angegebenen Größe generiert, indiziert nach key-exp. Die Hash-Tabelle kann mit der Funktionclear-memo gelöscht werden.

Beispiel:

(defun-memo f (x)              ;; eql table keyed on x
  (complex-computation x))

(defun-memo (f :test #'eq) (x) ;; eq table keyed on x
  (complex-computation x))

(defun-memo g (x y z)          ;; equal table
  (another-computation x y z)) ;; keyed on on (x y . z)

(defun-memo (h :key-exp x) (x &optional debug?)
                               ;; eql table keyed on x
...)

(defmacro defun-memo (fn-name-and-options (&rest args)
                                          &body body)
  ;; Documentation string on previous page
  (let ((vars (arglist-vars args)))
    (flet ((gen-body (fn-name &key (test '#'equal)
                              size key-exp)
             `(eval-when (load eval compile)
               (setf (get ',fn-name 'memoize-table)
                (make-hash-table :test ,test
                 ,@(when size `(:size ,size))))
               (defun ,fn-name ,args
                 (gethash-or-set-default
                  ,key-exp
                  (get ',fn-name 'memoize-table)
                  (progn ,@body))))))
      ;; Body of the macro:
      (cond ((consp fn-name-and-options)
             ;; Use user-supplied keywords, if any
             (apply #'gen-body fn-name-and-options))
            ((and (= (length vars) 1)
                  (not (member '&rest args)))
             ;; Use eql table if it seems reasonable
             (gen-body fn-name-and-options :test '#'eql
                       :key-exp (first vars)))
            (t ; Otherwise use equal table on all args
             (gen-body fn-name-and-options :test '#'equal
                       :key-exp `(list* ,@vars)))))))
Weitere Makros
(defmacro with-gensyms (symbols body)
  "Replace the given symbols with gensym-ed versions,
  everywhere in body. Useful for macros."
  ;; Does this everywhere, not just for "variables"
  (sublis (mapcar #'(lambda (sym)
                      (cons sym (gensym (string sym))))
                  symbols)
          body))

(defmacro gethash-or-set-default (key table default)
  "Get the value from table, or set it to the default.
  Doesn't evaluate the default unless needed."
  (with-gensyms (keyvar tabvar val found-p)
    `(let ((keyvar ,key)
           (tabvar ,table))
      (multiple-value-bind (val found-p)
          (gethash keyvar tabvar)
        (if found-p
            val
            (setf (gethash keyvar tabvar)
                  ,default))))))
Verwenden Sie Makros angemessen

(Siehe Allan Wechslers Tutorial)

Makrodesign:

  • Entscheiden Sie, ob Sie wirklich ein Makro benötigen
  • Wählen Sie eine klare und konsistente Syntax für Makros
  • Finden Sie die richtige Erweiterung
  • Verwenden Siedefmacro und `, um die Zuordnung zu implementieren
  • Stellen Sie in den meisten Fällen auch eine funktionale Schnittstelle bereit (nützlich und manchmal einfacher zu ändern und fortzusetzen).

Dinge, die man beachten muss:

  • Wenn die Verwendung von Funktionen ausreicht, verwenden Sie keine Makros
  • Stellen Sie sicher, dass Sie beim Erweitern (meistens) nichts unternehmen.
  • Argumente werden von links nach rechts einmal für jedes Argument ausgewertet (falls vorhanden).
  • Kein Konflikt mit der Benutzerbenennung (mit-gensyms verwenden)
Makroproblem

Defekt: Inline-Funktionen sollten verwendet werden

(defmacro name-part-of (rule)
  `(car ,rule))

Schlecht: Sollte eine Funktion sein

(defmacro defpredfun (name evaluation-function)
  `(push (make-predfun :name ,name
          :evaluation-function ,evaluation-function)
         *predicate-functions*))

Breaking: Funktioniert, wenn es erweitert wird

(defmacro defclass (name &rest def)
  (setf (get name 'class) def)
  ...
  (list 'quote name))

Fehler: Makro sollte keine Argumente auswerten

(defmacro add-person (name mother father sex
                           unevaluated-age)
  (let ((age (eval unevaluated-age)))
    (list (if (< age 16) ... ...) ...)))

(add-person bob joanne jim male (compute-age 1953))

Was wäre, wenn dieser Aufruf jetzt kompiliert und Jahre später geladen würde?

Besser: Lassen Sie die Compilerkonstante falten

(declaim (inline compute-age))

(defmacro add-person (name mother father sex age)
  `(funcall (if (< ,age 16) ... ...) ...))

Sehr schlecht: (Was ist, wenn das Inkrement n ist?)

(defmacro for ((variable start end &optional increment)
               &body body)
  (if (not (numberp increment)) (setf increment 1))
  ...)

(for (i 1 10) ...)
Makros dienen zur Steuerung von Strukturen

Gut: Füllt ein Loch in der Orthogonalität von CL

(defmacro dovector ((var vector &key (start 0) end)
                    &body body)
  "Do body with var bound to each element of vector.
  You can specify a subrange of the vector."
  `(block nil
    (map-vector #'(lambda (,var) ,@body)
                ,vector :start start :end end)))

(defun map-vector (fn vector &key (start 0) end)
  "Call fn on each element of vector within a range."
  (loop for i from start below (or end (length vector))
        do (funcall fn (aref vector-var index))))
  • Iterieren Sie über öffentliche Datentypen
  • Kompatible Standardsprache (dolist, dotimes)
  • Befolgen Sie die Aussage, kehren Sie zurück
  • Erweitern Sie die etablierte Syntax mit Schlüsselwörtern
  • Eines ist schlecht: Es gibt kein Ergebnis wiedolist,dotimes
Makro-Hilfsfunktionen

Die meisten Makros sollten zu Funktionsaufrufen erweitert werden.

Die eigentliche Arbeit eines Makrosdovector wird von einer Funktion erledigtmap-vector, weil:

  • es ist einfacher zu patchen
  • Es ist einzeln aufrufbar (nützlich für Programme)
  • Der generierte Code ist kleiner
  • Wenn Sie möchten, können Sie diese Hilfsfunktion inline machen (normalerweise nützlich, um die Konstruktion von Abschlüssen zu vermeiden).
(dovector (x vect) (print x))

Das Makro wird wie folgt erweitert:

(block nil
  (map-vector #'(lambda (x) (print x)) vect
              :start 0 :end nil))

Es erweitert sich inline auf (ungefähr):

(loop for i from 0 below (length vect)
        do (print (aref vect i)))
setf-Methode

Wie bei Makros müssen wir sicherstellen, dass jede Ausdrucksform nur einmal von links nach rechts ausgewertet wird.

Stellen Sie sicher, dass die Makroerweiterung in der richtigen Umgebung durchgeführt wird (macroexpand, get-setf-method).

(defmacro deletef (item sequence &rest keys
                        &environment environment)
  "Destructively delete item from sequence."
  (multiple-value-bind (temps vals stores store-form
                              access-form)
      (get-setf-method sequence environment)
    (assert (= (length stores) 1))
    (let ((item-var (gensym "ITEM")))
      `(let* ((,item-var ,item)
              ,@(mapcar #'list temps vals)
              (,(first stores)
               (delete ,item-var ,access-form ,@keys)))
        ,store-form))))

おすすめ

転載: blog.csdn.net/zssrxt/article/details/134104234