7
\$\begingroup\$

I am an OOP programmer by trade (Java) who is extremely new to Haskell and the world of purely functional languages. To get my feet wet, I decided to try and write a little musical interval translator/calculator. I am immediately impressed with this language. The power of this allowed me to quickly define an algebra called PitchClass and define a default Isomorphism between many Enumerated types.

With this code I am able to write things like C +: P5 and get G (the perfect fifth of C) as an output in the GHCi REPL.

I can write

λ: cycleOfFifths = cycle $ map (iso . (P5*:)) [3 .. 12] :: [NoteName]
λ: take 5 cycleOfFifths

And it will output 5 notes from the circle of fifths [Gb,Db,Ab,Eb,Bb] (which, by the way, are all root notes in the chord progression of Dave Brubeck's jazz standard named... you guessed it... take 5 :D)

class (Enum a) => PitchClass a where

    iso :: (PitchClass b) => a -> b
    iso = toEnum . fromIntegral . abs . (flip mod 12) . fromIntegral . fromEnum

    fiso :: (PitchClass b) => (b -> b) -> a -> a
    fiso f = iso . f . iso

    (+:) :: (PitchClass b) => a -> b -> a
    (+:) x = iso . fiso ((iso x :: Int)+)

    (*:) :: (PitchClass b) => a -> b -> a
    (*:) x = iso . fiso ((iso x :: Int)*)

    (-:) :: (PitchClass b) => a -> b -> a
    (-:) x = iso . fiso ((iso x :: Int)-)

instance PitchClass Integer
instance PitchClass Int

data NoteName = A | Bb | B | C | Db | D | Eb | E | F | Gb | G | Ab deriving (Show, Eq, Enum, Bounded); 
instance PitchClass NoteName

data Solfege = Do | Di | Re | Ri | Mi | Fa | Fi | Sol | Si | La | Li | Ti deriving (Show, Eq, Enum, Bounded); 
instance PitchClass Solfege

data Interval = R | Mi2 | Ma2 | Mi3 | Ma3 | P4 | TT | P5 | Mi6 | Ma6 | Mi7 | Ma7 deriving (Eq, Enum, Show, Bounded); 
instance PitchClass Interval

I am a beginner in Haskell, and I was floored by how elegant this was. So I decided to take it further. I wanted to treat a PitchClass as an actual mathematical ring, and have Haskell treat it as such. From reading the doc, it seems that the Num type class is what I need to accomplish this, so I added the following:

instance Num Solfege where
    (+) = (+:)
    (*) = (*:)
    abs = fiso (abs :: Integer -> Integer)
    signum = fiso (signum :: Integer -> Integer)
    fromInteger = iso
    negate = fiso (negate :: Integer -> Integer)

instance Num Interval where
    (+) = (+:)
    (*) = (*:)
    abs = fiso (abs :: Integer -> Integer)
    signum = fiso (signum :: Integer -> Integer)
    fromInteger = iso
    negate = fiso (negate :: Integer -> Integer)

instance Num NoteName where
    (+) = (+:)
    (*) = (*:)
    abs = fiso (abs :: Integer -> Integer)
    signum = fiso (signum :: Integer -> Integer)
    fromInteger = iso
    negate = fiso (negate :: Integer -> Integer)

I am looking for any feedback on either of these two parts. Is there anything that can be made more elegant, or more idiomatic for Haskell programmers?

Here are some things that have been bugging me, but keep in mind I am open to any advice to make my code cleaner, easier to read, and more idiomatic (naming conventions, indention, etc.):

  1. The Num instancing works, but this is far less elegant than my PitchClass, and there are multiple things that I don't like about it. First of all, I repeated the same code three times. Is there a way that I can avoid this duplication?

  2. Secondly, in this case, it would make sense to instance Num on the PitchClass, as in the type of (+) should be something like Num PitchClass so that I can write C + P5. I don't think the Type System allows this, as PitchClass is a type class, not a type (this took me a lot of fiddling and research for my feeble OOP brain to grasp). However, I still can't shake the feeling that there is some design pattern that would allow this. If there isn't, I am assuming its bad from the perspective of Haskell's philosophy? If so, I'd love it if an answerer or commenter could explain why.

  3. Finally, a lot of the things I've tried have given me errors from the GHC compiler, and many of these Errors have suggested extensions. I am not used to the concept of "extensions" in languages. Are there extensions that I could use to make my code better that are still considered safe/stable/idiomatic?

\$\endgroup\$
  • \$\begingroup\$ I am planning on making the exact same thing in a different language. I would also like to add intervals to notes. I'm also using a ring. Perhaps you can get some ideas how to use modular arithmetic from this question: codereview.stackexchange.com/questions/223697/… \$\endgroup\$ – dfhwze Aug 7 at 4:27
  • \$\begingroup\$ By the way, if you want to able to do something like C+ P5, you can use C# and use operator overloads. Note c = "C"; Note g = c + "P5"; \$\endgroup\$ – dfhwze Aug 7 at 4:34
2
\$\begingroup\$

The first two points can be answered by DerivingVia. It is one of the newer extensions that allows to derive a class instance by delegating to an existing instance for a representationally equivalent type. You can do something like this:

newtype PitchWrapper a = PitchWrapper a

instance PitchClass a => Num (PitchWrapper a) where
    (PitchWrapper a) + (PitchWrapper b) = a +: b
    ... and do on

deriving via (PitchWrapper NoteName) instance Num NoteName
deriving via (PitchWrapper Solfege) instance Num Solfege
deriving via (PitchWrapper Interval) instance Num Interval

That said, even though this is possible, I must warn you that it is not a good idea.

This is a very common pattern that I see in newcomers (including myself): overload all the things, declare operators for everything, make everything as short as possible. The language makes it possible, so let's run wild with it.

Never works out well in practice. If you follow this pattern, you usually end up with spaghetti code that nobody can understand without looking up every single character.

I would even argue that in this case it doesn't make sense. Look: notes do not actually constitute a ring. You don't add two notes together to get another note. You add intervals to notes, but not notes to each other.


Now, about extensions.

Unlike most languages, which simply add new features in every new release, the Haskell designers chose to give the power to the user. When you install a new version of GHC, you don't just get all the new features. You get a choice to opt-in.

This is what most extensions are: merely an opt-in mechanism for new language features.

It is true that there are some problematic extensions, but those are few and far between. In most cases, you'll be just fine turning on everything you see in those error messages.

There are numerous works on the internet describing and/or recommending particular sets of extensions to use. Here's one example.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.