Deutsches Lilypond Forum (Archiv)

Allgemein => Allgemeine Diskussion => Thema gestartet von: jps am Samstag, 5. März 2016, 11:43

Titel: Kleine Scheme-Etüde
Beitrag von: jps am Samstag, 5. März 2016, 11:43
Hallo in die Runde!

Erst vor wenigen Tagen bin ich auf Lilypond gestoßen.

Zu meiner Schande muss ich gestehen (und ich kann es nachträglich kaum fassen), dass mir die Existenz dieses Programms mit seinen offenbar weitreichenden Möglichkeiten bisher vollkommen verborgen geblieben war - trotz der sich für einen Berufsmusiker häufig ergebenden Notwendigkeit, eigene Noten zu erzeugen, und trotz meines bereits seit den uralten Tagen von Basic und Pascal vorhandenen (wenn auch kaum praktisch umgesetzten) Interesses an Programmierstrukturen.

Von Scheme&Co. habe ich allerdings bisher überhaupt keinen blassen Schimmer Ahnung gehabt.

Als erste Scheme-„Fingerübung“ habe ich mir deshalb mal die hier
https://liarchiv.joonet.de/index.php?topic=2188.msg12145#msg12145 (https://liarchiv.joonet.de/index.php?topic=2188.msg12145#msg12145)
im Forum gefundene kleine Aufgabenstellung vorgenommen und dazu unterschiedliche Lösungsalternativen zusammengebastelt, denn zu einer begrenzten Aufgabenstellung unterschiedliche Varianten durchzuspielen ist das, was mir persönlich am meisten hilft, die Logik und Syntax von unbekannten Strukturen zu verstehen.

Nun wäre ich natürlich dankbar für kritische Begutachtung meines „Opus 1“ durch die Forumsexperten.

\version "2.19.37"


% Als erste Scheme-Fingeruebung hier eine Gegenueberstellung mehrerer unterschiedlicher
% Anwendungen der Scheme-Syntax auf eine im Prinzip immer gleiche Funktion.

% Zunaechst zwei gegensaetzliche, sehr extreme Varianten.
% In der Praxis wuerde man sicherlich eine Mischung von beiden verwenden,
% z.B. so wie in der 3. Variante dargestellt?

% Alle Alternativen sollen einfach nur den Abstand zweier Toene
% auf der Konsole (oktavbereinigt) als Intervallnamen ausgeben.

% Leider - wie man schon an der folgenden Intervalltabelle erkennen kann - nur diatonisch,
% aber fuer die Musikalische Frueherziehung sollte das ja erst mal so reichen


#(define intervalltabelle '((0 . "Prim")
                            (1 . "Sekund")
                            (2 . "Terz")
                            (3 . "Quart")
                            (4 . "Quint")
                            (5 . "Sext")
                            (6 . "Sept")
                            (7 . "Oktav")))

% Da das erste Element eines jeden Paares dem Index in der Intervallliste entspricht,
% koennte man hier natuerlich genauso gut eine einfache Liste nehmen -
% so wie die folgende (die aufwaendigere Version wird nur uebungshalber verwendet):

#(define satzbausteine '("\nDas Intervall von "
                         " nach "
                         " ist eine "
                         " (ggf. + Oktavversetzungen)"))


% Variante 1 ------------------------------------------------------------

% In dieser Version werden keinerlei lokale Variablen spendiert,
% deshalb ergibt sich die typische Schachtelung des Codes
% und einige Ausdruecke werden mehrfach ausgewertet.

% Zum Verstaendnis des Codes ist es sicherlich am besten, sich von innen
% (d.h. von den am weitesten eingerueckten Zeilen) nach aussen vorzuarbeiten,
% oder ihn schrittweise mit Variante 2 zu vergleichen.

% ---------------------------------------------------------------------------


variante_eins =
#(define-scheme-function (p q) (ly:pitch? ly:pitch?)
   
       (display
         (string-append
           (list-ref satzbausteine 0)
           ; Name des 1. Tons als String
           (substring
             (object->string p)
             8
             (string-rindex
               (object->string p)
               #\sp))
           (list-ref satzbausteine 1)
           ; Name des 2. Tons als String
           (substring
             (object->string q)
             8
             (string-rindex
               (object->string q)
               #\sp))
           (list-ref satzbausteine 2)
           ; Name des Intervalls als String
           (cdr
             (assoc
               (modulo
                 (-
                   (max
                     (ly:pitch-steps p)
                     (ly:pitch-steps q))
                   (min
                     (ly:pitch-steps p)
                     (ly:pitch-steps q)))
                 7)
               intervalltabelle))
           (list-ref satzbausteine 3))))


% Variante 2 -------------------------------------------------------------

% Hier wird ganz ohne Schachtelung vorgegangen, indem das Ergebnis
% jedes Schritts mit "let" in eine lokale Variable geschrieben wird.

% Da es dabei zum Teil auf die Reihenfolge der Auswertung ankommt,
% muss das "let" mit einem "*" versehen werden

% (das einfache "let" wuerde erst versuchen, alle Ausdruecke auszuwerten,
% bevor die Zuordnung zu den einzelnen Variablen vorgenommen wuerde).

% ---------------------------------------------------------------------------


variante_zwei =
#(define-scheme-function (p q) (ly:pitch? ly:pitch?)
   
   (let* (
           
         (als_string (lambda (x) (object->string x)))
         (string_bis_zum_letzten_leerzeichen (lambda (x) (string-rindex (als_string x) #\sp)))
         (tonname (lambda (x) (substring (als_string x) 8 (string_bis_zum_letzten_leerzeichen x))))
 
         (schritthoehe (lambda (x) (ly:pitch-steps x)))
         (hoeherer_ton (max (schritthoehe p) (schritthoehe q)))
         (tieferer_ton (min (schritthoehe p) (schritthoehe q)))
         
         (abstand_der_beiden_toene (- hoeherer_ton tieferer_ton))
         (abstand_oktavbereinigt (modulo abstand_der_beiden_toene 7))
         (passender_listeneintrag (assoc abstand_oktavbereinigt intervalltabelle))
         (intervallname (cdr passender_listeneintrag))
         
         (ausgabetext (string-append (list-ref satzbausteine 0)
                                     (tonname p)
                                     (list-ref satzbausteine 1)
                                     (tonname q)
                                     (list-ref satzbausteine 2)
                                     intervallname
                                     (list-ref satzbausteine 3))))
 
       (display ausgabetext)))


% Variante 3 -------------------------------------------------------------

% Hier werden nur fuer einige Ausdruecke lokale Variablen eingesetzt.

% Trotzdem besteht auch hier eine Abhaengigkeit; in diesem Fall wurde jedoch
% anstelle von "let*" eine Verschachtelung von zweimal "let" gewaehlt.

% ------------------------------------------------------------------------


variante_drei =
#(define-scheme-function (p q) (ly:pitch? ly:pitch?)
   
   (let ((als_string (lambda (x) (object->string x)))
         (schritthoehe (lambda (x) (ly:pitch-steps x))))
       
     (let ((tonname (lambda (x) (substring (als_string x) 8 (string-rindex (als_string x) #\sp)))))
         
       (display
         (string-append
           (list-ref satzbausteine 0)
           (tonname p)
           (list-ref satzbausteine 1)
           (tonname q)
           (list-ref satzbausteine 2)
           (cdr
             (assoc
               (modulo
                 (-
                   (max (schritthoehe p)(schritthoehe q))
                   (min (schritthoehe p)(schritthoehe q)))
                 7)
               intervalltabelle))
           (list-ref satzbausteine 3))))))


% Variante 4 -------------------------------------------------------------

% Hier uebernimmt die Hauptfunktion nur den Zusammenbau des Ausgabetextes.
% Der Rest ist in zwei Helper-Funktionen ausgelagert.

% Die beiden mit x benannten Parameter in den verschachtelten Funktionen
% "intervallname" und "schritthoehe" stoeren sich gegenseitig nicht.
% Das "innere" x verdeckt noetigenfalls das "aeussere".
% Dies hier nur zu Demozwecken. Besser lesbar waere die Sache sicherlich
% mit unterschiedlich benannten Parametern.

% ------------------------------------------------------------------------


variante_vier =
#(define-scheme-function (p q) (ly:pitch? ly:pitch?)
  (display
    (string-append
      (list-ref satzbausteine 0)
      (tonname p)
      (list-ref satzbausteine 1)
      (tonname q)
      (list-ref satzbausteine 2)
      (intervallname p q)
      (list-ref satzbausteine 3))))

#(define (tonname x)
   (let ((als_string (object->string x)))
     (substring als_string 8 (string-rindex als_string #\sp))))
   
#(define (intervallname x y)
  (let ((schritthoehe (lambda (x) (ly:pitch-steps x))))
    (cdr
     (assoc
       (modulo
         (-
           (max (schritthoehe x) (schritthoehe y))
           (min (schritthoehe x) (schritthoehe y)))
         7)
     intervalltabelle))))


% Kleiner Test fuer alle 3 Varianten:-------------------------------------

\variante_eins a'''' f,,,,
\variante_zwei gis,, fis'''
\variante_drei c' d'
\variante_vier ees''' aes,,,

#(display "\n\nScheme macht Spass!\n")

Danke und herzliche Grüße
Jost
Titel: Re: Kleine Scheme-Etüde
Beitrag von: fugenkomponist am Samstag, 5. März 2016, 12:13
Hallo Jost,

willkommen im Forum! Mir sind ein paar Sachen aufgefallen, die eigentlich allesamt die Lesbarkeit des Codes betreffen:

• Du benennst sehr oft Zwischenergebnisse, was selten nötig und (meiner Meinung nach) nur in Maßen sinnvoll ist. Beispiel: Variante 2 definiert nacheinander abstand_der_beiden_toene, abstand_oktavbereinigt, passender_listeneintrag und intervallname. Ich würde hier intervallname einfach als(cdr
  (assoc
    (modulo (- hoeher_ton tieferer_ton) 7)
    intervalltabelle))
definieren. Das ist auch noch lesbar (zumindest mit sinnvoller Einrückung).
• Eine Funktion umzubenennen ohne zusätzliche Funktionalität zu ergänzen halte ich für vollkommen unnötig. Beispiele sind als_string und schritthoehe in Variante 2. Ich würde einfach die originalen Namen verwenden. (Abgesehen davon könnte man (let ((als_string object->string))) statt (let ((als_string (lambda (x) (object->string x))))) schreiben.
• Mir persönlich gefällt Variante 3 am besten (allerdings ohne umbenannte Funktionen); Variante 4 ist auch noch ok, meiner Meinung nach wird da aber relativ viel Aufwand betrieben ohne dass sich an der Lesbarkeit deutlich was verbessert.
• Die Strings der Ausgabe würde ich hartkodieren. In einem echten Anwendungsfall würde ja jeder dieser Strings nur einmal gebraucht; außerdem spart man sich dann Kommentare, weil die Ausgabe der Funktion quasi die Funktion selbst erklärt.

Vielleicht fällt mir noch was auf, dann meld ich mich nochmal.

Edit: In Variante 3 ist als_string eine Funktion, in Variante 4 ein String. Grundsätzlich würde ich für verschiedene Dinge auch verschiedene Namen verwenden. (Ich muss allerdings zugeben, dass ich hier (https://liarchiv.joonet.de/index.php?topic=2033.msg11372#msg11372) mal einer music-property und einer Funktion den gleichen Namen „until“ gegeben habe.)
Titel: Re: Kleine Scheme-Etüde
Beitrag von: jps am Samstag, 5. März 2016, 12:29
Danke für die superschnelle erste Antwort!

Variante 1 und 2 hatte ich auch mal nur als extreme Pole ausprobiert, und ich würde auch Variante 3 als die sinnvollste ansehen. Variante 4 habe ich auch nur aufgeschrieben, um mal so alles mögliche durchzuspielen (solche Helper-Funktionen außerhalb zu schreiben wäre ja sicher nur dann sinnvoll, wenn man sie noch anderweitig recyclen könnte).

Danke auch für den let/lambda-Hinweis. Dass das mit dem lambda im let doppeltgemoppelt sein müsste, hatte ich mir auch schon überlegt, aber komischerweise hatte es auf die geradlinige Art und Weise nicht funktioniert. Da hatte ich mich wohl verklammert.

Herzliche Grüße
Jost

Edit: Zu deinem Edit: Da man ja sowieso im richtigen Leben nicht 4 Funktionen für die gleiche Sache in einen Code schreiben würde, habe ich bewusst nicht auf solche Inkonsistenzen zwischen den 4 Funktionen geachtet (das betrifft auch noch andere Dinge). Dumme Anfängerfrage: Wie hast du den Link hinter das "hier" bekommen? Danach hatte ich schon gesucht, aber eine Suche nach dem Wort "Link" nutzt nicht so viel.
Titel: Forum: Zugang Quelltext
Beitrag von: ingmar am Samstag, 5. März 2016, 12:50
Zitat
Wie hast du den Link hinter das "hier" bekommen?
Klick in dem Beitrag, der dich interessiert, rechts oben auf das Wort "Zitat", dann siehst du den Quelltext des Beitrags.

--ingmar
Titel: Re: Kleine Scheme-Etüde
Beitrag von: fugenkomponist am Samstag, 5. März 2016, 13:05
Ergänzend zu ingmars Hinweis: Auf einen bestimmten Post in einem Thread kann man verweisen, indem man den Titel dieses Posts anklickt und dann erst die URL kopiert (sie enthält dann einen Verweis auf msg<nummer>, den Post).
[url=http://example.com/]Beispiellink[/url]
Titel: Re: Kleine Scheme-Etüde
Beitrag von: jps am Samstag, 5. März 2016, 13:14
Danke. Meine Frage zielte nur darauf ab, wie man es macht, dass sichtbarer Text und Link unterschiedlich sind. Aber das ist durch das Codebeispiel jetzt klar geworden. Hätte man auch selber drauf kommen können ;)
Titel: Re: Kleine Scheme-Etüde
Beitrag von: harm6 am Samstag, 5. März 2016, 13:45
Hallo Jost,

Du verwendest häufig (list-ref ...) und (substring ...).
Meiner Erfahrung nach ist das häufig nicht robust genug.
Bei "#<Pitch a,, >" ist es noch am wenigsten wahrscheinlich, daß Probleme mit substring auftauchen.

Aber list-ref auf 'satzbausteine' dauernd anzuwenden halte ich für problematisch. Nun gut, die Listen-Einträge werden hart codiert angesteuert. Aber was wenn Du 'satzbausteine' mal grundsätzlich anders gestalten möchtest. Dann hast Du viel Arbeit...

Statt (cdr (assoc ...)) kannst Du einfach assoc-get gebrauchen.


Ich hab' mich auch mal an ein coding versucht.
Die Notennamen werden aus 'language-pitch-names' ausgelesen. Allerdings mit dem Nachteil, daß man die Oktave erst richten muß und die Oktavierungszeichen müssen dann neu geschaffen werden.
Ist auch nicht das Gelbe vom Ei, aber der Vollständigkeit halber poste ich es.

Der finale Output wird durch eine void-function geregelt, die (format ...) benutzt.

\version "2.19.36"

#(define intervalltabelle '((0 . "Prim")
                            (1 . "Sekund")
                            (2 . "Terz")
                            (3 . "Quart")
                            (4 . "Quint")
                            (5 . "Sext")
                            (6 . "Sept")
                            (7 . "Oktav")))
                           
#(define (get-german-pitch-name p)
  (let ((normalize-pitch
          (lambda (pitch)
            (ly:make-pitch
              -1
              (ly:pitch-notename pitch)
              (ly:pitch-alteration pitch)))))
    (assoc-get
      (normalize-pitch p)
        (map
          reverse-interval
          (assoc-get 'deutsch language-pitch-names)))))
         
#(define (out-put-text p1 p2 interval)
  (let* ((p-oct (lambda (p) (1+ (ly:pitch-octave p))))
         (oct-insert
           (lambda (p) 
             (make-string (abs (p-oct p)) (if (negative? (p-oct p)) #\, #\')))))
  (format #t
    "Das Intervall von ~a~a nach ~a~a ist eine ~a (ggf. + Oktavversetzungen)"
    (get-german-pitch-name p1)
    (oct-insert p1)
    (get-german-pitch-name p2)
    (oct-insert p2)
    interval)))
       
variante_harm =
#(define-void-function (p q) (ly:pitch? ly:pitch?)
  (out-put-text
    p
    q
    (assoc-get
      (modulo (abs (- (ly:pitch-steps p) (ly:pitch-steps q))) 7)
      intervalltabelle)))

\variante_harm a,, fis

Gruß,
  Harm
Titel: Re: Kleine Scheme-Etüde
Beitrag von: harm6 am Samstag, 5. März 2016, 14:00
Zitat
#(display "\n\nScheme macht Spass!\n")

:D
Titel: Re: Kleine Scheme-Etüde
Beitrag von: jps am Samstag, 5. März 2016, 20:10
Dann werde ich assoc-get mal in meinen Wortschatz aufnehmen, und auch das mit den germanischen Pitchnamen werde ich mir merken. Vielen Dank auch für diese Tipps.
Titel: Re: Kleine Scheme-Etüde
Beitrag von: harm6 am Samstag, 5. März 2016, 21:10
Falls es jemand interessiert, hier noch eine Fassung für Chromatik, Vierteltöne hab ich mir aber gespart:

\version "2.19.36"
     
#(define intervalltabelleII
  '((ceses . "d. v. Prim")
    (ces . "v. Prim")
    (c . "r. Prim")
    (cis . "ü. Prim")
    (cisis . "d. ü. Prim")
   
    (deses . "v. Sekund")
    (des . "kl. Sekund")
    (d . "gr. Sekund")
    (dis . "ü. Sekund")
    (disis . "d. ü. Sekund")
   
    (eses . "v. Terz")
    (es . "kl. Terz")
    (e . "gr. Terz")
    (eis . "ü. Terz")
    (eisis . "d. ü. Terz")
   
    (feses . "d. v. Quart")
    (fes . "v. Quart")
    (f . "r. Quart")
    (fis . "ü. Quart")
    (fisis . "d. ü. Quart")
   
    (geses . "d v. Quint")
    (ges . "v. Quint")
    (g . "r. Quint")
    (gis . "ü. Quint")
    (gisis . "d. ü. Quint")
   
    (asas . "v. Sext")
    (as . "kl. Sext")
    (a . "gr. Sext")
    (ais . "ü. Sext")
    (aisis . "d. ü.. Sext")
   
    (heses . "v. Septim")
    (b . "kl. Septim")
    (h . "gr. Septime")
    (his . "ü. Septim")
    (hisis . "d. ü. Septim")))
   
#(define (get-german-pitch-name p)
  (let* ((normalize-pitch
           (lambda (pitch)
             (ly:make-pitch
               -1
               (ly:pitch-notename pitch)
               (ly:pitch-alteration pitch)))))
    (assoc-get
      (normalize-pitch p)
      (map reverse-interval (assoc-get 'deutsch language-pitch-names)))))
         
#(define (out-put-text p1 p2)
  (let* ((oct-insert
           (lambda (p) 
             (make-string
               (abs (1+ (ly:pitch-octave p)))
               (if (negative? (1+ (ly:pitch-octave p))) #\, #\'))))
         (pitch-diff 
           (if (ly:pitch<? p1 p2)
               (ly:pitch-diff p2 p1)
               (ly:pitch-diff p1 p2)))
         (interval
           (assoc-get (get-german-pitch-name pitch-diff) intervalltabelleII)))
   (format #t
     "\nDas Intervall von ~a~a nach ~a~a ist eine ~a (ggf. + Oktavversetzungen)"
     (get-german-pitch-name p1)
     (oct-insert p1)
     (get-german-pitch-name p2)
     (oct-insert p2)
     interval)))
       
variante-harm =
#(define-void-function (p q) (ly:pitch? ly:pitch?) (out-put-text p q))

\variante-harm a aeses,     
\variante-harm a aes,
\variante-harm a a,
\variante-harm a ais,

\variante-harm a bes,
\variante-harm a b,
\variante-harm a bis,

\variante-harm a ces,
\variante-harm a c,
\variante-harm a cis,

\variante-harm a des,
\variante-harm a d,
\variante-harm a dis,

\variante-harm a ees,
\variante-harm a e,
\variante-harm a eis,

\variante-harm a fes,
\variante-harm a f,
\variante-harm a fis,

\variante-harm a ges,
\variante-harm a g,
\variante-harm a gis,

#(display "\n\nScheme macht Spass!\n")

Gruß,
  Harm