Ho incontrato un piccolo problema estetico nel mio progetto musicale e mi ha dato fastidio per qualche tempo.
Ho un tipo data Key = C | D | ...
e posso costruire una Scale
da una Key
e un Mode
. Il Mode
distingue tra, per esempio, una scala maggiore e una minore.
Posso definire il tipo Mode
come una funzione da Key
a Scale
. In questo caso i modi avranno nomi minuscoli (il che va bene) e posso ottenere una scala come questa
aScale = major C
Ma i musicisti non parlano così. Si riferiscono a questa scala come la scala C major, non la scala major C.
Cosa voglio
Idealmente vorrei scrivere
aScale = C major
È possibile?
Cosa ho provato
Posso fare di Key
una funzione che costruisce una Scala
da un Mode
, quindi posso scrivere
aScale = c Major
Ma non posso limitare le chiavi alla costruzione di scale. Sono necessarie anche per altre cose (per esempio costruire accordi). Anche Key
dovrebbe essere un'istanza di Show
.
Posso mettere il Mode
dopo il Key
quando uso una funzione extra (o un costruttore di valori):
aScale = scala C major
con scale :: Key -> Mode -> Scale
.
Ma la parola extra scala sembra rumorosa e, contrariamente al suo nome, scale
non si occupa veramente di scale. La parte intelligente è in major
, scale
è davvero solo flip ($)
.
Usare un newtype Mode = Major | Minor ...
non cambia molto, tranne che scala
deve essere più intelligente:
aScale = scale C Major
Soluzione 1:
Usare questo
data Mode = Major | Minor
data Scale = C Mode | D Mode | E Mode | F Mode | G Mode | A Mode | B Mode
Ora puoi scrivere (con la C maiuscola e la M maiuscola)
aScale = C Major
Soluzione 2a:
Anche questo è possibile
data Mode = Major | Minor
data Key = C | D | E | F | G | A | B
data Scale = Scale Key Mode
Ora si scrive
aScale = Scale C Major
Soluzione 2b:
Anche questo è possibile
data Mode = Major | Minor
data Key = C | D | E | F | G | A | B
type Scale = (Key, Mode)
Ora si scrive
aScale = (C, Major)
Ecco una soluzione stravagante che non consiglio, ma che sembra molto "musicale":
infix 8 ♮
(♮) :: Key -> Mode -> Scale
(♮) = (Data.Function.&)
-- ≡ flip ($)
Poi si può scrivere
> C♮ major :: Scale
Naturalmente, dove questo è veramente mirato è che si avrebbe anche F♯ minore
e B♭ maggiore
ecc.
Se non ti dispiace un operatore extra, potresti usare &
da Data.Function
. Assumendo che major
sia una funzione Key -> Scale
, potresti scrivere C & major
. Questo produce un valore Scale
:
Prelude Data.Function> :t C & major
C & major :: Scale
Ci sono già diverse buone risposte, ma ecco una soluzione di stile che può essere utile (forse non per questo particolare esempio, ma in altri contesti in cui si vuole una sorta di sintassi di applicazione inversa).
Con definizioni standard per alcuni tipi di dominio problematici:
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)
è possibile introdurre un tipo di passaggio continuo:
type Cont a r = (a -> r) -> r
e scrivere i primitivi tipi di costruzione di note per costruire i tipi di "Contenuti" in questo modo:
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
Quindi, le funzioni di costruzione di scale, note e accordi possono risolvere i Cont's a tipi semplici in entrambe le forme postfix (cioè, come continuazioni da passare al
Cont's):
major, minor :: Note -> Scale
major n = Scale n Major
minor n = Scale n Minor
note :: Note -> Note
note = id
o forma di prefisso (cioè, prendendo come argomenti `Cont's):
chord :: [Cont Note [Note]] -> Chord
chord = Chord . foldr step []
where step f acc = f (:acc)
Ora, puoi scrivere:
> 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]
Si noti che c
in sé non ha un'istanza di Show
, ma c note
lo fa.
Con una modifica al tipo Note
, si potrebbero facilmente supportare doppi incidenti (ad esempio, c sharp sharp
, distinta da d
), ecc.
Ma non posso limitarmi a costruire le Chiavi per costruire le Scale. Sono necessarie anche per altre cose (ad esempio per costruire accordi). Anche la chiave dovrebbe essere un'istanza di Show.
Si possono usare le classi tipografiche per aggirare abilmente questo problema:
{-# 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
Ora, è possibile utilizzare le lettere minuscole anche per altri tipi definendo le istanze appropriate.