我不确定我在这里是否过于雄心勃勃,但我正在尝试构造一个整数 mod 12 数据类型以用于音乐符号系统。如果可能的话,我希望在实践中仅使用数字 0-11 即可指定这种类型的值(例如,而不是编写"Note 11"),并通过使用它的函数的类型签名来推断 Note 类型。我创建了一个笨拙的临时版本,使用
type Note = Int
然后简单地使用 mod 12 函数组合任何作用于 Notes 的函数。这非常有效,但它是重复的。它看起来像是函子的理想位置,使用大致类似于
instance Functor Note where
fmap f = (`mod` 12).f
我想如果我在任何时候使用数据类型时写"Note 0"或类似的东西,我可以很容易地完成这项工作,但这与我目前正在做的打字量完全等同(尽管不可否认它可能稍微不容易出错)。最后,我知道智能构造函数与我正在研究的主题密切相关,但到目前为止,我无法为我的白日梦设置使其工作。有什么办法可以让我的蛋糕(用普通整数 0-11 表示笔记)并在这里吃它(为 Notes 定义 fmap),还是我很乐观?
提前感谢!
您要查找的类是Num
:Haskell中0
的文字语法隐式调用fromInteger
。所以你可以写一些类似的东西
newtype Note = Note Int
instance Num Note where
fromInteger n = Note (fromInteger (n `mod` 12))
Note a + Note b = Note ((a + b) `mod` 12)
以此类推,用于其他Num
操作。您可能还想使用 Hackage 包模块化算术,它提供了Mod Int 12
类型并且已经提供了这些操作。
您无法在此处获取函子实例,因为您的类型太具体了。 但不要绝望——你仍然可以得到你想要的! 我们将只使用一个独立的普通函数来做同样的事情,而不是使用类型类。 以下是路线图:
-
首先,我们将定义我们的
Note
类型并使其安全:我们将使用智能构造函数来确保所有Note
在0
和11
之间保持Int
。 -
接下来,我们将讨论
Functor
以及为什么Note
不是一个,以及如何用普通功能替换您想要的功能。 -
最后,我们将
Note
支持数字文字语法。 -
然后,我们将展示一些用法示例。
-
我们将以完整的代码结束。
因此,让我们定义我们的Note
类型。 我们希望它是整数周围的薄包装器,因此我们将使用newtype
:
newtype Note = MkNote Int deriving Eq
instance Show Note where
show (MkNote 0) = "A"
show (MkNote 1) = "A♯/B♭"
show (MkNote 2) = "B"
show (MkNote 3) = "C"
show (MkNote 4) = "C♯/D♭"
show (MkNote 5) = "D"
show (MkNote 6) = "D♯/E♭"
show (MkNote 7) = "E"
show (MkNote 8) = "F"
show (MkNote 9) = "F♯/G♭"
show (MkNote 10) = "G"
show (MkNote 11) = "G♯/A♭"
show (MkNote _) = error "internal error: invalid `Note'"
现在,为了确保Note
始终为 0–11,我们使用智能构造函数:
note :: Int -> Note
note = MkNote . (`mod` 12)
我们还提供了一个析构函数:
getNote :: Note -> Int
getNote (MkNote i) = i
这些函数将使我们能够安全地在Note
s 和Int
s 之间进行转换,因此我们可以导出Note
类型,但不能导出MkNote
构造函数,以确保所有Note
都在 0 到 11 之间。
如果我们打开 GHC扩展PatternSynonyms
,我们可以伪造具有普通数据类型:
pattern Note :: Int -> Note
pattern Note i <- MkNote i where
Note i = note i
这将定义一个新的模式Note i
,当模式匹配时,对应于MkNote
构造函数(<-
部分),而在表达式中使用时对应于note
智能构造函数(where …
部分)。
现在,您希望在Int
函数和Note
函数之间移动,并询问有关创建Functor
实例的问题。 好吧,Functor
并不是您在这里真正想要的。 让我们看一下类型类:
class Functor f where
fmap :: (a -> b) -> (f a -> f b)
嗯。。。我们看到f
正在应用于那里的另一种类型。 但是我们不能用Note
做到这一点! 我们说Note
有种类(一种类型的类型)*
,这是所有类型的类型;但是f
有善良的* -> *
,也就是说它是一个类型级函数,将类型带到类型。 这就是为什么我们可以说
instance Functor Maybe where …
-- fmap :: (a -> b) -> (Maybe a -> Maybe b)
但不是
-- instance Functor Note where …
-- fmap :: (a -> b) -> (Note a -> Note b)
那么我们能做些什么呢? 好吧,仅仅因为类型类不起作用并不意味着我们不能定义自己的映射函数:
nmap :: (Int -> Int) -> Note -> Note
nmap f = note . f . getNote
由于note
做了你想要(`mod` 12)
的事情,这基本上与你的定义相同,这是它必须具有的类型:我们只能nmap
类型Int -> Int
的函数。 这是fmap
不起作用的另一个原因——它必须支持所有功能。
我们也可以使用这种技术来定义用于处理多参数函数的函数:
nmap2 :: (Int -> Int -> Int) -> Note -> Note -> Note
nmap2 f n1 n2 = note $ f (getNote n1) (getNote n2)
如果我们想使用不返回Note
s 的函数,我们可以这样做:
nuse :: (Int -> a) -> Note -> a
nuse f = f . getNote
nuse2 :: (Int -> Int -> a) -> Note -> Note -> a
nuse2 f = f `on` getNote
-- `on` is from "Data.Function"
(请注意,nmap = note . nuse
,同样适用于nmap2
。
最后,我们可以使用它来支持数字文字。 在 Haskell 中,像42
这样的数字文字等价于表达式fromInteger (42 :: Integer)
,其中fromInteger
来自Num
类型类。 因此,您可以将Note
s作为Num
的实例:
instance Num Note where
(+) = nmap2 (+)
(-) = nmap2 (-)
(*) = error "Can't multiply `Note`s" -- Or @nmap2 (*)@
negate = nmap negate
abs = id -- No-op; equivalent to @nmap abs@
signum = nmap signum
fromInteger = note . fromInteger -- 'Integer' → 'Int' → 'Note'
综上所述,这里有一些我们可以写的东西的例子。
A♯s的列表:
aSharps :: [Note]
aSharps = [ note 1, note (-11), note 13
, 1, -11, 13
, Note 1, Note (-11), Note 13 ]
-- All elements are equal
将 C 转换为 As 但保留其他注释不变的函数:
noCs :: Note -> Note
noCs 3 = 0
noCs n = n
以及编写该函数的不同方式:
noCs' :: Note -> Note
noCs' 15 = 0
noCs' n = n
等等等等
最后,这是所有代码放在一起的样子;请注意,为了安全起见,模块标头不会导出MkNote
。
{-# LANGUAGE PatternSynonyms #-}
module Note (
Note(), pattern Note, note, getNote,
nmap, nmap2, nuse, nuse2
) where
import Data.Function
newtype Note = MkNote Int deriving Eq
instance Show Note where
show (MkNote 0) = "A"
show (MkNote 1) = "A♯/B♭"
show (MkNote 2) = "B"
show (MkNote 3) = "C"
show (MkNote 4) = "C♯/D♭"
show (MkNote 5) = "D"
show (MkNote 6) = "D♯/E♭"
show (MkNote 7) = "E"
show (MkNote 8) = "F"
show (MkNote 9) = "F♯/G♭"
show (MkNote 10) = "G"
show (MkNote 11) = "G♯/A♭"
show (MkNote _) = error "internal error: invalid `Note'"
note :: Int -> Note
note = MkNote . (`mod` 12)
getNote :: Note -> Int
getNote (MkNote i) = i
pattern Note :: Int -> Note
pattern Note i <- MkNote i where
Note i = note i
nmap :: (Int -> Int) -> Note -> Note
nmap f = note . f . getNote
nmap2 :: (Int -> Int -> Int) -> Note -> Note -> Note
nmap2 f n1 n2 = note $ f (getNote n1) (getNote n2)
nuse :: (Int -> a) -> Note -> a
nuse f = f . getNote
nuse2 :: (Int -> Int -> a) -> Note -> Note -> a
nuse2 f = f `on` getNote
instance Num Note where
(+) = nmap2 (+)
(-) = nmap2 (-)
(*) = error "Can't multiply `Note`s" -- Or @nmap2 (*)@
negate = nmap negate
abs = id -- No-op; equivalent to @nmap abs@
signum = nmap signum
fromInteger = note . fromInteger -- 'Integer' → 'Int' → 'Note'