Emacs Lisp Schnelleinstieg
Themen
Warum Elisp?
Man kann Emacs benutzen, ohne jemals Elisp Code zu schreiben. Die Konfiguration ist aber einfacher, wenn man sich etwas im Runde-Klammer-Dschungel orientieren kann.
Es gibt tolle Tutorials und eine hervorragende Dokumentation, beides braucht aber mehr Zeit als die meisten Nutzer/innen haben. Darum teile ich hier das Wesentliche.
Listen
- In Lisp ist alles eine Liste. Listen stehen in runden Klammern. Reine Datenlisten werden mit einem vorangestellten Hochkomma markiert. Leere Datenliste:
'()
- Listen bestehen aus Atomen, die mit Leerzeichen voneinander getrennt sind. Liste mit drei Atomen:
'(42 Hello 3.14)
- In Emacs kann eine Liste mit
C-x C-e
dem Interpreter übergeben werden. Der Curser muss dabei hinter der letzten Klammer stehen. - In Lisp steht alles in Prefix- oder polnischer Notation:
(+ 2 3)
- Fehlt das vorangestellte Hochkomma, erwartet der Interpreter als erstes Atom einen Funktionsnamen, dahinter die Parameter. Hier eine geschachtelte Liste mit zwei Funktionsaufrufen (
message
undsqrt
) und formatierter Ausgabe:
(print (format "Die Wurzel aus zwei ist etwa %f." (sqrt 2)))
"Die Wurzel aus zwei ist etwa 1.414214."
formatierte Ausgabe:
%s
String%d
Ganzzahl%f
Fließkommazahl%e
Exponentialform%g
Exponentialform, wenn der Betrag des Exponenten 3 übersteigt, sonst Fließkommazahl
Variablen
- Die Funktion
setq
erwartet einen Variablennamen und, natürlich, eine Liste mit Daten, oder sogar eine Liste von Name-Wert-Listen Paaren. 1
(setq art_periods '(realism impressionism expressionism cubism) artists '(Courbet Monet Munch Picasso)) (print art_periods) (print artists)
(realism impressionism expressionism cubism) (Courbet Monet Munch Picasso)
- Mit
set
definierte Variablen sind global. - Mit
let
definierte Variablen sind lokal. Das macht vor allem Sinn innerhalb von Funktionen.
Funktionen
Auch Funktionsdefinitionen sind nichts anderes als eine Liste, mit der Funktion defun
am Anfang, danach einem Funktionsnamen, einer Liste von Parametern und schließlich einer (beliebig geschachtelten) Liste als Funktionsrumpf.
(defun log10 (arg) "Logarithm to the base of 10." (/ (log arg) (log 10)) ) (print (log10 1000))
2.9999999999999996
Verzweigungen
Konsequenterweise wird auch die if
Verzweigung als geschachtelte Liste umgesetzt:
(setq a 2 b 3) (if (>= a b) (message "A ist größer als oder gleich B.") ; then (message "B ist größer als A.")) ; else
B ist größer als A.
Lisp interpretiert nil
als Falsch, alles andere als Wahr.
Schleifen
Im Beispiel wird die Schleife wiederholt, bis list
leer, also nil
ist. Die Funktion car
2 gibt jeweils das ersten Listenelement zurück, welches hier mit print
ausgegeben wird. Den Rest der Liste, ohne das erste Element erhält man mit cdr
3, welches der Liste erneut zugewiesen wird, so dass sich die Liste pro Durchgang um eine Element verkürzt.
(setq artists '(Courbet Monet Munch Picasso)) (defun print_list (list) "Print elements of a list." (while list (print (car list)) (setq list (cdr list)))) (print_list artists)
Courbet Monet Munch Picasso
Mit dolist
kann die gleiche Aufgabe etwas einfacher umgesetzt werden. Die Funktion iteriert im ersten Parameter (hier var
) über die Elemente der übergebenen Liste (hier list
).
(setq artists '(Courbet Monet Munch Picasso)) (defun print_list (list) "Print elements of a list." (dolist (var list) (print var))) (print_list artists)
Courbet Monet Munch Picasso
OOP
Objektorientierte Programmierung (OOP) kann mit der Bibliothek EIEIO (Enhanced Implementation of Emacs Interpreted Objects) umgesetzt werden.
Grundlegenden Konzepte:
- Klassen werden mit
defclass
definiert. - Objekte werden mit
make-instance
erstellt. - Methoden können mit
defmethod
definiert werden.
Beispiel:
(require 'eieio) (defclass person () ((name :initarg :name :initform "empty") (age :initarg :age :initform 0))) (defmethod greet ((p person)) (message "Mein Name ist %s und ich bin %d Jahre alt." (slot-value p 'name) (slot-value p 'age))) ;; Erstellen eines Objekts (setq p1 (make-instance 'person :name "Max" :age 30)) ;; Methode aufrufen (greet p1)
Mein Name ist Max und ich bin 30 Jahre alt.
Projekt
Dieses Grundwissen genügt, um ChatGPT darum zu bitten, den Code für einen Vokabeltrainer zu generieren, den Code zu verstehen, zu korrigieren und zu vereinfachen.
Der Vokabeltrainer soll so ähnlich wie das klassische Karteikasten-System funktionieren:
- richtige Antwort: Die Karte rückt ein Fach nach hinten (score +1)
- falsche Antwort: Die Karte rückt ein Fach nach vorne (score –1)
Pro Durchgang werden die 12 vordersten Karten gelernt.
Die Vokabeln stehen nach dem folgenden Muster in einem Buffer:
Muttersprache | Fremdsprache | Score
Beispiel:
hello | hola | 0 world | mundo | 0 thank you | gracias | 0 ...
Mit M-x start-vocab-trainer
wird ein Durchlauf gestartet.
Die Funktionen, die man dabei kennenlernt sind entweder selbsterklärend oder schnell in der Dokumentation nachgeschlagen. Die lambda
Funktion ist eine anonyme Funktion, genau wie in vielen anderen Programmiersprachen auch. Sie wird hier verwendet, um für jede Vokabel den score als Sortierschlüssel zurückzugeben.
(require 'eieio) ;; class for single phrase (defclass vocab-entry () ((native :initarg :native :initform "" :type string :documentation "phrase in native lang") (foreign :initarg :foreign :initform "" :type string :documentation "phrase in foreign lang") (score :initarg :score :initform 0 :type integer :documentation "score"))) ;; class for trainer (defclass vocab-trainer () ((entries :initarg :entries :initform nil :type list :documentation "list of phrases"))) (defmethod load-vocab ((trainer vocab-trainer)) "Reading phrases from current buffer and saving as objecst." (with-current-buffer (current-buffer) (oset trainer entries nil) (goto-char (point-min)) (while (not (eobp)) (let* ((line (thing-at-point 'line t)) (parts (split-string line " | " t)) (native (nth 0 parts)) (foreign (nth 1 parts)) (score (string-to-number (nth 2 parts)))) (push (vocab-entry :native native :foreign foreign :score score) (oref trainer entries))) (forward-line 1)))) (defmethod save-vocab ((trainer vocab-trainer)) "Saving phrases with actual scores back to current buffer." (with-current-buffer (current-buffer) (erase-buffer) (dolist (entry (oref trainer entries)) (insert (format "%s | %s | %d\n" (oref entry native) (oref entry foreign) (oref entry score)))) (save-buffer))) (defmethod select-weakest ((trainer vocab-trainer)) "Selecting phrases with lowest score." (cl-subseq (cl-sort (copy-sequence (oref trainer entries)) #'< :key (lambda (e) (oref e score))) 0 12)) (defmethod train ((trainer vocab-trainer)) "Start training." (let ((words (select-weakest trainer))) (while words (let* ((entry (car words)) (user-input (read-string (format "Translate: %s → " (oref entry native))))) (if (string= user-input (oref entry foreign)) (progn (message "Correct!") (sit-for 1) (oset entry score (1+ (oref entry score))) (setq words (cdr words))) (progn (message "Wrong! Correct answer: %s (press key...)" (oref entry foreign)) (read-char) (oset entry score (1- (oref entry score)))))))) (save-vocab trainer)) ;; Launch trainer. (defun start-vocab-trainer () (interactive) (let ((trainer (vocab-trainer))) (load-vocab trainer) (train trainer)))
Aktuelle Version: https://git.fh-muenster.de/pv238554/vocab_trainer
Gerne forken und verbessern!
Fußnoten:
Das q in setq
steht für quoted. Der Befehl set
würde das Symbol versuchen auszuwerten. Beispiel: (setq a 4)
weist der Variablen a
den Wert 4 zu. (set a 4)
generiert eine Fehlermeldung. Korrekt wäre (set 'a 4)
.
CAR – Contents of the Address part of Register
CDR – Contents of the Decrement part of Register