Ik ben een klein esthetisch probleem tegengekomen in mijn muziekproject en het zit me al een tijdje dwars.
Ik heb een type data Key = C | D | ...
en ik kan een Scale
construeren uit een Key
en een Mode
. De Mode
maakt onderscheid tussen bijvoorbeeld een majeur en een mineur toonladder.
Ik kan het Mode
type definiëren als een functie van Key
naar Scale
. In dat geval hebben de modi kleine letters (wat prima is) en kan ik een toonladder als volgt krijgen
aScale = major C
Maar muzikanten praten niet zo. Zij verwijzen naar deze toonladder als de C majeur toonladder, niet als de majeur C toonladder.
Wat ik wil
Idealiter zou ik willen schrijven
aScale = C major
Is dit überhaupt mogelijk?
Wat ik geprobeerd heb
Ik kan van Key
een functie maken die een Scale
construeert uit een Mode
, zodat ik kan schrijven
aScale = c Major
Maar ik kan Keys niet beperken tot het construeren van schalen. Ze zijn ook voor andere dingen nodig (b.v. het construeren van akkoorden). Ook Key
zou een instantie van Show
moeten zijn.
Ik kan de Mode
na de Key
zetten als ik een extra functie (of waarde constructor) gebruik:
aScale = scale C major
met scale :: Key -> Mode -> Scale
Maar het extra woord schaal ziet er lawaaierig uit en in tegenstelling tot zijn naam, houdt schaal
zich niet echt bezig met toonladders. Het intelligente deel is in majeur
, schaal
is eigenlijk gewoon flip ($)
.
Het gebruik van een newtype Mode = Major | Minor ...
verandert er niet echt veel, behalve dat scale
intelligenter moet zijn:
aScale = scale C Major
Oplossing 1:
Gebruik deze
data Mode = Major | Minor
data Scale = C Mode | D Mode | E Mode | F Mode | G Mode | A Mode | B Mode
Nu kan je schrijven (met hoofdletter C en hoofdletter M)
aScale = C Major
Oplossing 2a:
Dit is ook mogelijk
data Mode = Major | Minor
data Key = C | D | E | F | G | A | B
data Scale = Scale Key Mode
Nu schrijf je
aScale = Scale C Major
Oplossing 2b:
Dit is ook mogelijk
data Mode = Major | Minor
data Key = C | D | E | F | G | A | B
type Scale = (Key, Mode)
Nu schrijf je
aScale = (C, Major)
Hier's een grillige oplossing die ik niet echt aanbeveel, maar er heel "muzikaal" uitziet:
infix 8 ♮
(♮) :: Key -> Mode -> Scale
(♮) = (Data.Function.&)
-- ≡ flip ($)
Dan kun je schrijven
> C♮ major :: Scale
Waar dit natuurlijk echt op gericht is, is dat je ook F♯ minor
en B♭ major
etc. zou hebben.
Als je een extra operator niet erg vindt, zou je &
van Data.Function
kunnen gebruiken. Ervan uitgaande dat majeur
een functie Key -> Scale
is, zou je C & majeur
kunnen schrijven. Dat levert een Schaal
waarde op:
Prelude Data.Function> :t C & major
C & major :: Scale
Er zijn al een aantal goede antwoorden, maar hier is een voortzetting van de passerende stijl oplossing die nuttig kan zijn (misschien niet voor dit specifieke voorbeeld, maar in andere contexten waar een soort van omgekeerde toepassing syntaxis is gewenst).
Met standaard definities voor sommige probleemdomeintypes:
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)
kunt u een doorlopende variant invoeren:
type Cont a r = (a -> r) -> r
en schrijf de primitieve notitie-gebouw types om Cont
types te bouwen zoals dit:
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
Dan kan de schaal, de noot en de koordbouwfuncties de Cont
s oplossen tot gewone types in een postfix vorm (d.w.z. als voortzettingen die aan de Cont
moeten worden doorgegeven):
major, minor :: Note -> Scale
major n = Scale n Major
minor n = Scale n Minor
note :: Note -> Note
note = id
of voorvoegselvorm (d.w.z. met Cont
s als argument):
chord :: [Cont Note [Note]] -> Chord
chord = Chord . foldr step []
where step f acc = f (:acc)
Nu kan je schrijven:
> 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]
Merk op dat c
zelf geen Show
instantie heeft, maar c note
wel.
Met een aanpassing van het type Note
kunt u gemakkelijk dubbele ongelukken ondersteunen (bijv. c scherp
, verschillend van d
), enz.
Maar ik kan me niet beperken tot het bouwen van weegschalen. Ze zijn ook nodig voor andere dingen (bijv. het maken van akkoorden). Ook Key zou een voorbeeld van Show moeten zijn.
Je kunt met behulp van typeklassen daar slim omheen werken:
{-# 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
Nu kunt u de kleine letters ook voor andere typen gebruiken door de juiste instanties te definiëren.