我在我的音乐项目中遇到了一个小的审美问题,它已经困扰了我一段时间了。
我有一个类型data Key = C | D | ...
,我可以从Key
和Mode
构建一个Scale
。模式 "可以区分大调和小调音阶。
我可以将 "模式 "类型定义为一个从 "键 "到 "音阶 "的函数。在这种情况下,模式将有小写的名字(这很好),我可以得到一个像这样的音阶
aScale = major C
但音乐家们不会这样说话。他们把这个音阶称为_C大调_音阶,而不是_C大调_音阶。
我想的是
理想情况下,我想写的是
aScale = C major
这到底有没有可能?
我所尝试的
我可以让Key
成为一个函数,从Mode
构建一个Scale
,所以我可以写出
aScale = c Major
但我不能把钥匙局限于构造音阶。其他事情也需要它们(例如构造和弦)。另外,"键 "应该是 "显示 "的一个实例。
当我使用一个额外的函数(或值构造器)时,我可以把Mode
放在Key
后面:
aScale = scale C major
与scale ::Key -> Mode -> Scale
。
但额外的词scale看起来很吵,与它的名字相反,scale
并不真正关心音阶。聪明的部分是在大调
中,音阶
实际上只是翻转($)
。
使用newtype Mode = Major | Minor ...除了
scale'需要更智能外,并没有什么变化:
aScale = scale C Major
解决方案1:
使用这个
data Mode = Major | Minor
data Scale = C Mode | D Mode | E Mode | F Mode | G Mode | A Mode | B Mode
现在你可以写(大写C和大写M)了
aScale = C Major
解决方案2a:
这也是可能的
data Mode = Major | Minor
data Key = C | D | E | F | G | A | B
data Scale = Scale Key Mode
现在你写
aScale = Scale C Major
解决方案2b:
这也是可能的
data Mode = Major | Minor
data Key = C | D | E | F | G | A | B
type Scale = (Key, Mode)
现在你写
aScale = (C, Major)
这里有一个异想天开的解决方案,我不太推荐,但看起来很有 "音乐感":
infix 8 ♮
(♮) :: Key -> Mode -> Scale
(♮) = (Data.Function.&)
-- ≡ flip ($)
然后你可以写
> C♮ major :: Scale
当然,这真正的目的是,你也会有F♯小调
和降B大调
等等。
如果你不介意多一个运算符,你可以使用Data.Function
中的&
。假设major'是一个
Key -> Scale'的函数,你可以写C & major'。这将产生一个
音阶'值:
Prelude Data.Function> :t C & major
C & major :: Scale
已经有好几个好的答案了,但这里有一个延续传递风格的解决方案,可能会有帮助(也许不适合这个特殊的例子,但在其他想要某种反向应用语法的情况下)。
用一些问题域类型的标准定义。
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)
你可以引入一个延续传递类型。
type Cont a r = (a -> r) -> r
并写出原始的笔记构建类型来构建Cont
类型,就像这样。
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
然后,音阶、音符和和弦构建函数可以将Cont
s解析为后缀形式的纯类型(即作为连续型传递给Cont
)。
major, minor :: Note -> Scale
major n = Scale n Major
minor n = Scale n Minor
note :: Note -> Note
note = id
或前缀形式(即以Cont
s作为参数)。
chord :: [Cont Note [Note]] -> Chord
chord = Chord . foldr step []
where step f acc = f (:acc)
现在,你可以写了。
> 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]
请注意,"c "本身没有 "显示 "实例,但 "c note "有。
通过修改Note
类型,你可以很容易地支持双意外符(例如,c sharp sharp
,与d
不同),等等。
但我不能把音键局限于音阶的构造。它们也需要用于其他的事情(例如构造和弦)。同时Key应该是Show的一个实例。
你可以使用类型类来巧妙地解决这个问题。
{-# 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
现在,你也可以通过定义适当的实例将小写字母用于其他类型。