Ich bin in meinem Musikprojekt auf ein kleines ästhetisches Problem gestoßen, das mich schon seit einiger Zeit stört.
Ich habe einen Typ Daten Key = C | D | ...
und ich kann eine Scale
aus einem Key
und einem Mode
konstruieren. Der Mode
unterscheidet z.B. zwischen einer Dur- und einer Molltonleiter.
Ich kann den Typ Mode
als eine Funktion von Key
zu Scale
definieren. In diesem Fall haben die Modi Namen in Kleinbuchstaben (was in Ordnung ist) und ich kann eine Skala wie diese erhalten
aScale = major C
Aber Musiker sprechen nicht so. Sie bezeichnen diese Skala als _C-Dur-Tonleiter, nicht als C-Dur-Tonleiter.
Was ich will
Idealerweise würde ich schreiben wollen
aScale = C major
Ist das überhaupt möglich?
Was ich versucht habe
Ich kann Key
zu einer Funktion machen, die eine Scale
aus einem Mode
konstruiert, also kann ich schreiben
aScale = c Major
Aber ich kann Keys nicht auf die Konstruktion von Skalen beschränken. Sie werden auch für andere Dinge benötigt (z.B. für die Konstruktion von Akkorden). Auch Key
sollte eine Instanz von Show
sein.
Ich kann den Mode
nach dem Key
setzen, wenn ich eine zusätzliche Funktion (oder einen Wertkonstruktor) verwende:
aScale = scale C major
mit scale :: Tonart -> Modus -> Skala
Aber das zusätzliche Wort Skala sieht störend aus, und im Gegensatz zu seinem Namen hat Skala
nicht wirklich mit Skalen zu tun. Der intelligente Teil ist in Major
, Skala
ist eigentlich nur flip ($)
.
Die Verwendung eines newtype Mode = Major | Minor ...
ändert nicht wirklich viel, außer dass scale
intelligenter sein muss:
aScale = scale C Major
Lösung 1:
Verwenden Sie diese
data Mode = Major | Minor
data Scale = C Mode | D Mode | E Mode | F Mode | G Mode | A Mode | B Mode
Jetzt können Sie schreiben (mit großem C und großem M)
aScale = C Major
Lösung 2a:
Auch dies ist möglich
data Mode = Major | Minor
data Key = C | D | E | F | G | A | B
data Scale = Scale Key Mode
Jetzt schreiben Sie
aScale = Scale C Major
Lösung 2b:
Auch dies ist möglich
data Mode = Major | Minor
data Key = C | D | E | F | G | A | B
type Scale = (Key, Mode)
Jetzt schreiben Sie
aScale = (C, Major)
Hier ist eine skurrile Lösung, die ich nicht wirklich empfehle, die aber sehr "musikalisch" aussieht:
infix 8 ♮
(♮) :: Key -> Mode -> Scale
(♮) = (Data.Function.&)
-- ≡ flip ($)
Dann können Sie schreiben
> C♮ major :: Scale
Das Ziel ist natürlich, dass man auch F♯ minor
und B♭ major
usw. hat.
Wenn Sie nichts gegen einen zusätzlichen Operator haben, könnten Sie &
aus Data.Function
verwenden. Unter der Annahme, dass major
eine Funktion Key -> Scale
ist, könnten Sie C & major
schreiben. Das ergibt einen Scale
-Wert:
Prelude Data.Function> :t C & major
C & major :: Scale
Es gibt bereits mehrere gute Antworten, aber hier ist eine Lösung im Stil des Continuation-Passing-Stils, die hilfreich sein könnte (vielleicht nicht für dieses spezielle Beispiel, aber in anderen Kontexten, in denen eine Art Syntax für die umgekehrte Anwendung gewünscht wird).
Mit Standarddefinitionen für einige Problemdomänentypen:
data Mode = Major | Minor deriving (Show)
data Key = C | D | E | F | G | A | B deriving (Show)
data Semitone = Flat | Natural | Sharp deriving (Show)
data Note = Note Key Semitone deriving (Show)
data Scale = Scale Note Mode deriving (Show)
data Chord = Chord [Note] deriving (Show)
können Sie einen Continuation-Passing-Typ einführen:
type Cont a r = (a -> r) -> r
und schreibe die primitiven Notizbau-Typen, um `Content'-Typen wie diese zu bauen:
a, b, c :: Cont Note r
a = mkNote A
b = mkNote B
c = mkNote C
-- etc.
mkNote a f = f $ Note a Natural
flat, natural, sharp :: Note -> Cont Note r
flat = mkSemi Flat
natural = mkSemi Natural
sharp = mkSemi Sharp
mkSemi semi (Note k _) f = f $ Note k semi
Dann können die Tonleiter-, Noten- und Akkordbildungsfunktionen die Cont's in einfache Typen in jeder Postfix-Form auflösen (d.h. als Fortsetzungen, die an die
Cont's übergeben werden):
major, minor :: Note -> Scale
major n = Scale n Major
minor n = Scale n Minor
note :: Note -> Note
note = id
oder Präfix-Form (d.h. unter Verwendung von Content
als Argument):
chord :: [Cont Note [Note]] -> Chord
chord = Chord . foldr step []
where step f acc = f (:acc)
Jetzt können Sie schreiben:
> c sharp note
Note C Sharp
> c note
Note C Natural
> c major
Scale (Note C Natural) Major
> b flat note
Note B Flat
> c sharp major
Scale (Note C Sharp) Major
> chord [a sharp, c]
Chord [Note A Sharp,Note C Natural]
Beachten Sie, dass c
selbst keine Show'-Instanz hat, aber
c note` schon.
Mit einer Modifikation des Note
-Typs könnten Sie leicht doppelte Vorzeichen unterstützen (z.B. cis sharp
, verschieden von d
), usw.
Aber ich kann die Schlüssel nicht auf die Konstruktion von Skalen beschränken. Sie werden auch für andere Dinge benötigt (z.B. für die Konstruktion von Akkorden). Auch Key sollte eine Instanz von Show sein.
Sie können Typklassen verwenden, um das geschickt zu umgehen:
{-# LANGUAGE FlexibleInstances #-}
data Key = C | D | E | F | G | A | B deriving(Show)
data Mode = Major | Minor
data Scale = Scale Key Mode
class UsesKey t where
c, d, e, f, g, a, b :: t
instance UsesKey Key where
c = C
d = D
e = E
f = F
g = G
a = A
b = B
instance UsesKey (Mode -> Scale) where
c = Scale C
d = Scale D
e = Scale E
f = Scale F
g = Scale G
a = Scale A
b = Scale B
aScale :: Scale
aScale = c Major
Jetzt können Sie die Kleinbuchstaben auch für andere Typen verwenden, indem Sie entsprechende Instanzen definieren.