我不确定我是否过于雄心勃勃,但我正在尝试构建一个整数mod 12数据类型,用于音乐符号系统。如果可能的话,我希望在实践中可以简单地使用数字0-11来指定这种类型的值(例如,与编写“注11”相反),并且可以通过类型签名来推断Note类型。使用它的函数。我使用
创建了一个笨重的临时版本type Note = Int
然后简单地用mod 12函数组合任何作用于Notes的函数。这非常有效,但它是重复的。对于仿函数来说,它看起来是一个完美的位置,使用大致沿着
的方式instance Functor Note where
fmap f = (`mod` 12).f
我想如果我在使用数据类型时随时写下“Note 0”或其他内容,我就可以轻松地完成这项工作,但这与我目前正在做的事情完全相同(尽管可能是这样)稍微不那么容易出错)。最后我知道智能构造函数与我正在研究的内容是一个密切相关的主题,但到目前为止,我无法设法为我的管道梦想设置。有没有什么方法可以让我的蛋糕(用简单的整数0-11代表注释)并且也吃它(在这里定义fmap),或者我是否乐观?
提前致谢!
答案 0 :(得分:7)
您正在寻找的课程是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包modular-arithmetic,它提供Mod Int 12
类型并且已经可以使用这些操作。
答案 1 :(得分:4)
您无法在此处获得仿函数实例,因为您的类型太具体了。但不要绝望 - 你仍然可以得到你想要的东西!我们只使用一个执行相同操作的独立普通函数,而不是使用类型类。这是路线图:
首先,我们将定义我们的Note
类型并确保其安全:我们将使用智能构造函数来确保所有Note
在Int
之间保持0
{1}}和11
。
接下来,我们将讨论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
和Int
之间进行转换,因此我们可以导出Note
类型但不 {{1构造函数,以确保所有MkNote
都在0到11之间。
如果我们打开GHC扩展程序PatternSynonyms
,我们可以伪造普通数据类型:
Note
这定义了一个新模式pattern Note :: Int -> Note
pattern Note i <- MkNote i where
Note i = note i
,当模式匹配时,它对应于Note i
构造函数(MkNote
部分),并且当在表达式中使用时对应于{{1智能构造函数(<-
部分)。
现在,您希望在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
项,因此这与您的定义基本相同,而且这是它必须具有的类型:我们只能 { {1}}类型为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
。)
最后,我们可以使用它来支持数字文字。在Haskell中,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
表达式,其中fromInteger
来自Num
类型类。因此,您可以将nmap2
设为42
的实例:
fromInteger (42 :: Integer)
把所有这些放在一起,这里有一些我们可以写的例子。
A♯s列表:
Note
将Cs变为As但将其他音符保持不变的函数:
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'
依旧等等
最后,这就是所有代码放在一起的内容;请注意,模块标题不会导出aSharps :: [Note]
aSharps = [ note 1, note (-11), note 13
, 1, -11, 13
, Note 1, Note (-11), Note 13 ]
-- All elements are equal
以确保安全。
noCs :: Note -> Note
noCs 3 = 0
noCs n = n