5

I have a music note datatype defined like so:

data Note = Ab | A | Bb | B | C | Db | D | Eb | E | F | Gb | G deriving (Eq, Ord)

How can i make it an instace of Enum so that succ G returns Ab ?

Atavixion
  • 53
  • 4
  • 4
    You should not implement this as an instance of `Enum`, as the documentation says: "*The calls `succ maxBound` and `pred minBound` should result in a runtime error.*", you can however use another function to define such cycle. – Willem Van Onsem Jan 09 '22 at 14:29
  • 4
    That's only for types that also implement `Bounded`. – chepner Jan 09 '22 at 14:33
  • 2
    @chepner Even so, surely an Enum instance should behave such that `succ x = toEnum (fromEnum x + 1)`? – amalloy Jan 09 '22 at 14:56
  • @amalloy For non-cyclic orderings, sure. – chepner Jan 09 '22 at 15:05
  • 1
    @amalloy Why is `succ x = toEnum (fromEnum x + 1)` in conflict with the question's request? – Daniel Wagner Jan 09 '22 at 18:29
  • 1
    @amalloy No: https://hackage.haskell.org/package/base-4.16.0.0/docs/Data-Fixed.html#t:Fixed – Joseph Sible-Reinstate Monica Jan 09 '22 at 21:06
  • 1
    Be careful with `Ord` if you use cyclic `Enum`. `succ G` is `Ab`, but `G > Ab`; I don't know if it's necessarily "wrong" (are there laws for this?), but seems ripe for confusion and bugs. – Ben Jan 10 '22 at 04:17
  • Yes. I'd say cyclic `Enum` is fairly sensible for this type, but `Ord` is probably a bad idea. Consider if you actually need it. (Perhaps `Hashable` is an alternative, if you need to use notes as container keys.) – leftaroundabout Jan 10 '22 at 10:18
  • BTW musically speaking I object to having all black keys as ♭ versions. F♯ is much more common than G♭. Ideally you would support both, and also e.g. E♯ and C♭, though that makes `Enum` and `Ord` even more delicate. If you support only one name for each piano-key-note, I'd suggest calling them B♭ C♯ E♭ F♯ G♯. – leftaroundabout Jan 10 '22 at 10:22
  • @Ben Sadly, there's precedent for that too. Look at the `Ord` and `Enum` instances for `Down` that got added in GHC 8.10. – Joseph Sible-Reinstate Monica Jan 23 '22 at 16:40
  • @JosephSible-ReinstateMonica That's at least consistently reversed though, and when `Down` is in play the reader is hopefully conscious of the reversal, since that's the whole point. I can definitely see code that is polymorphic in `Enum` and `Ord` being broken if it's handed a `Down` though, yes. If we want inter-class laws for `Ord`, `Down` probably needs to similarly alter those instances, or not have them. And indeed it looks like `Bounded` and `Enum` were added in base 4.14, then in 4.15 `Enum` was removed and `Bounded` now has a note saying it swaps `minBound` and `maxBound`. – Ben Jan 23 '22 at 21:47
  • @JosephSible-ReinstateMonica Yeah, after a closer look `Bounded` and `Enum` were added in 4.14 as simple derived instances, along with a whole bunch of other classes. I'm assuming someone just thought inheriting more functionality from the wrapped type was convenient. But by 4.15 it had been decided that having `Bounded` and `Enum` contradict `Ord` was a bad idea. So that example actually more reinforces the idea that the community expects `Enum` and `Ord` to be consistent, rather than being precedent for them to be inconsistent. – Ben Jan 23 '22 at 21:56

1 Answers1

6

You have to define the Enum instance yourself:

instance Enum Note where
    fromEnum note = case note of
        Ab -> 0
        A  -> 1
        ...
    toEnum n = case n `mod` 12 of
        0 -> Ab
        1 -> A
        ...

The "modulo 12" part in toEnum will cycle your notes.

sapanoia
  • 789
  • 4
  • 14