我可以创建一个看起来像Int的Functor数据类型吗?

时间:2016-05-23 22:13:00

标签: haskell types

我不确定我是否过于雄心勃勃,但我正在尝试构建一个整数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),或者我是否乐观?

提前致谢!

2 个答案:

答案 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)

您无法在此处获得仿函数实例,因为您的类型太具体了。但不要绝望 - 你仍然可以得到你想要的东西!我们只使用一个执行相同操作的独立普通函数,而不是使用类型类。这是路线图:

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

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

    < / LI>
  3. 最后,我们将Note支持数字 - 文字语法。

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

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

  6. 所以让我们定义我们的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
    

    这些功能可让我们安全地在NoteInt之间进行转换,因此我们可以导出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