我可以创建一个"看起来"像 int 的函子数据类型吗?



我不确定我在这里是否过于雄心勃勃,但我正在尝试构造一个整数 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类型并且已经提供了这些操作。

您无法在此处获取函子实例,因为您的类型太具体了。 但不要绝望——你仍然可以得到你想要的! 我们将只使用一个独立的普通函数来做同样的事情,而不是使用类型类。 以下是路线图:

  1. 首先,我们将定义我们的Note类型并使其安全:我们将使用智能构造函数来确保所有Note011之间保持Int

  2. 接下来,我们将讨论Functor以及为什么Note不是一个,以及如何用普通功能替换您想要的功能。

  3. 最后,我们将Note支持数字文字语法。

  4. 然后,我们将展示一些用法示例。

  5. 我们将以完整的代码结束。


因此,让我们定义我们的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

这些函数将使我们能够安全地在Notes 和Ints 之间进行转换,因此我们可以导出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)

如果我们想使用不返回Notes 的函数,我们可以这样做:

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类型类。 因此,您可以将Notes作为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'

最新更新