W moim projekcie muzycznym natknąłem się na mały problem estetyczny, który od pewnego czasu mnie podsłuchiwał.
Mam typ klucza danych = C |D | ...
i mogę skonstruować skalę
z Klucza
i Trybu
. `Mode
rozróżnia np. skalę durową i małą.
Mogę zdefiniować typ Trybu
jako funkcję od Klucza
do Skali
. W takim przypadku tryby będą miały nazwy małych liter (co jest w porządku) i mogę uzyskać taką skalę jak ta
aScale = major C
Ale muzycy nie mówią w ten sposób. Mówią o tej skali jako o skali C major, a nie o skali major C.
To czego chcę
Idealnie byłoby, gdybym chciał napisać
aScale = C major
Czy to w ogóle możliwe?
To, czego próbowałem
Mogę zrobić Klucz
funkcję, która konstruuje skalę
z Trybu
, więc mogę napisać
aScale = c Major
Ale nie mogę ograniczyć kluczy do konstruowania Wagi. Są one potrzebne także do innych rzeczy (np. do konstruowania chordów). Również Klucz
powinien być przykładem Pokazuj
.
Mogę umieścić Tryb
za Kluczem
, gdy używam dodatkowej funkcji (lub konstruktora wartości):
aSkala = skala C-dur
z skalą :: Klawisz -> Tryb -> Skala
: aSkala
Ale dodatkowe słowo skala wygląda na hałaśliwe i w przeciwieństwie do swojej nazwy, skala
nie dotyczy tak naprawdę wag. Inteligentna część jest w głównej
, skala
jest tak naprawdę tylko przesunięciem ($)
.
Użycie nowego trybu = Major |Moll... ``nie zmienia się wiele, z wyjątkiem tego, że
skala` musi być bardziej inteligentna:
aScale = scale C Major
Rozwiązanie 1:
Użyj tego
data Mode = Major | Minor
data Scale = C Mode | D Mode | E Mode | F Mode | G Mode | A Mode | B Mode
Teraz możesz pisać (z kapitałem C i M)
aScale = C Major
Rozwiązanie 2a:
To jest również możliwe
data Mode = Major | Minor
data Key = C | D | E | F | G | A | B
data Scale = Scale Key Mode
Teraz piszesz
aScale = Scale C Major
Rozwiązanie 2b:
Jest to również możliwe
data Mode = Major | Minor
data Key = C | D | E | F | G | A | B
type Scale = (Key, Mode)
Teraz piszesz
aScale = (C, Major)
Oto jedno kapryśne rozwiązanie, którego tak naprawdę nie polecam, ale wygląda bardzo "muzycznie":
infix 8 ♮
(♮) :: Key -> Mode -> Scale
(♮) = (Data.Function.&)
-- ≡ flip ($)
Wtedy możesz napisać
> C♮ major :: Scale
Oczywiście, tam gdzie jest to naprawdę zamierzone, jest to, że miałbyś również F♯ minor
i B♭ major
etc...
Jeśli nie masz nic przeciwko dodatkowemu operatorowi, możesz użyć &
z Data.Function
. Zakładając, że większa
jest funkcją Key -> Skala
, można napisać C & major
. To daje wartość `skali
:
Prelude Data.Function> :t C & major
C & major :: Scale
Jest już kilka dobrych odpowiedzi, ale oto rozwiązanie w stylu kontynuacji przejścia, które może być pomocne (może nie dla tego konkretnego przykładu, ale w innych kontekstach, gdzie pożądany jest rodzaj odwrotnej składni aplikacji).
Ze standardowymi definicjami dla niektórych typów domen problemowych:
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)
można wprowadzić typ kontynuacji:
type Cont a r = (a -> r) -> r
i napisać prymitywny notatnik typów do budowania Cont
typów jak tak:
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
Następnie, funkcje skalowania, notatki i budowania akordów mogą rozwiązać Cont
s do typów prostych w formie postfix (tj. jako kontynuacje, które mają być przekazywane do Cont
):
major, minor :: Note -> Scale
major n = Scale n Major
minor n = Scale n Minor
note :: Note -> Note
note = id
lub formę przedrostkową (tj. przyjmującą jako argumenty Cont
s):
chord :: [Cont Note [Note]] -> Chord
chord = Chord . foldr step []
where step f acc = f (:acc)
Teraz możesz pisać:
> 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]
Zauważ, że c
sama nie ma pokazu' instancji, ale
c note` ma.
Z modyfikacją typu Note
, można łatwo obsługiwać podwójne wypadki (np. c sharp
, różne od d
), itp.
Ale nie mogę ograniczyć kluczy do konstruowania Wagi. Są one potrzebne także do innych rzeczy (np. do konstruowania akordów). Również Key powinien być przykładem Show.
Możesz użyć klas pism, aby sprytnie to obejść:
{-# 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
Teraz możesz używać małych liter również dla innych typów, definiując odpowiednie instancje.