37

I encountered a small aesthetic issue in my music project and it has been bugging me for some time.

I have a type data Key = C | D | ... and I can construct a Scale from a Key and a Mode. The Mode distinguishes between e.g. a major and a minor scale.

I can define the Mode type as a function from Key to Scale. In that case the modes will have lowercase names (which is fine) and I can get a Scale like this

aScale = major C

But musicians don't talk like this. They refer to this scale as the C major scale, not the major C scale.

What I want

Ideally I'd want to write

aScale = C major

Is this possible at all?

What I tried

I can make Key a function that constructs a Scale from a Mode, so I can write

aScale = c Major

But I cannot confine Keys to constructing Scales. They are needed for other things as well (e.g. constructing chords). Also Key should be an instance of Show.


I can put the Mode after the Key when I use an extra function (or value constructor):

aScale = scale C major with scale :: Key -> Mode -> Scale

But the extra word scale looks noisy and contrary to its name, scale isn't really concerned with scales. The intelligent part is in major, scale is really just flip ($).


Using a newtype Mode = Major | Minor ... doesn't really change much, except scale needs to be more intelligent:

aScale = scale C Major
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Martin Drautzburg
  • 5,143
  • 1
  • 27
  • 39
  • 3
    I've myself found myself wanting extremely similar syntax in the past, but TBH it's not worth it. Just go with `major C`. – leftaroundabout Jan 31 '20 at 11:43
  • 4
    Just as a musical quibble: “Key” is a misleading name for that datatype, since e.g. C major and C minor are different keys in standard terminlogy. “PitchClass” would be a more accurate name for the type. – PLL Jan 31 '20 at 19:18
  • 2
    @PLL Indeed, I am having trouble finding a good name for C,C#,D ... I know Euterpea uses PitchClass. It is more correct than Key, but not "musical" at all. Right now I am playing with the idea of calling it Root or Tonic though that suggests only Chords and Scales. What the hell do musicians call that thing - a Note without an Octave? – Martin Drautzburg Feb 01 '20 at 12:26
  • 4
    @MartinDrautzburg: I wouldn’t say pitch class is un-musical — it’s not just programmer-speak by any means, it’s been established in music theory as meaning “a note without an octave” since at least around the mid 20th century. It’s not very commonly outside technical music-theory contexts, but that’s just because the precise distinction between “a pitch” and “a pitch without an octave” isn’t really needed often in everyday use, and when it is needed, it’s usually clear from context. But “Root” or “Tonic” sound fine as slightly more familiar terms, if less precise. – PLL Feb 01 '20 at 12:43
  • 1
    No because it doesn't work vice versa, a programmer going into music – Emobe Feb 01 '20 at 21:41

5 Answers5

29

Solution 1:

Use this

data Mode  = Major | Minor
data Scale = C Mode | D Mode | E Mode | F Mode | G Mode | A Mode | B Mode 

Now you can write (with capital C and capital M)

aScale = C Major

Solution 2a:

This is also possible

data Mode  = Major | Minor
data Key   = C | D | E | F | G | A | B 

data Scale = Scale Key Mode  

Now you write

aScale = Scale C Major

Solution 2b:

This is also possible

data Mode  = Major | Minor
data Key   = C | D | E | F | G | A | B 

type Scale = (Key, Mode)  

Now you write

aScale = (C, Major)
Elmex80s
  • 3,428
  • 1
  • 15
  • 23
  • IMO going with solution 2 will serve you well. Surrender to haskell's syntax and make in it a clean model of your domain. Things can be do pleasant if you do – luqui Feb 02 '20 at 09:25
16

Here's one whimsical solution that I don't really recommend, but looks very “musical”:

infix 8 ♮
(♮) :: Key -> Mode -> Scale
(♮) = (Data.Function.&)
 -- ≡ flip ($)

Then you can write

> C♮ major :: Scale

Of course, where this is really aimed is that you would also have F♯ minor and B♭ major etc..

leftaroundabout
  • 117,950
  • 5
  • 174
  • 319
  • 1
    I wonder if there's anything like non-breaking space that's allowed as an operator :) – chepner Jan 31 '20 at 13:11
  • 26
    @chepner actually yes: `⠀` U+2800 BRAILLE PATTERN BLANK can be used as an infix. Needless to say, this is a horrible idea... All the actual _space_ characters are forbidden as infixes, but unsurprisingly Unicode contains something which can be hacked into the abuse purpose. – leftaroundabout Jan 31 '20 at 13:52
11

If you don't mind an extra operator, you could use & from Data.Function. Assuming that major is a function Key -> Scale, you could write C & major. That produces a Scale value:

Prelude Data.Function> :t C & major
C & major :: Scale
Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
4

There are already several good answers, but here's a continuation passing style solution that may be helpful (maybe not for this particular example, but in other contexts where a sort of reverse-application syntax is wanted).

With standard definitions for some problem domain types:

data Mode = Major | Minor                 deriving (Show)
data Key = C | D | E | F | G | A | B      deriving (Show)
data Semitone = Flat | Natural | Sharp    deriving (Show)

data Note = Note Key Semitone             deriving (Show)
data Scale = Scale Note Mode              deriving (Show)
data Chord = Chord [Note]                 deriving (Show)

you can introduce a continuation-passing type:

type Cont a r = (a -> r) -> r

and write the primitive note-building types to build Cont types like so:

a, b, c :: Cont Note r
a = mkNote A
b = mkNote B
c = mkNote C
-- etc.
mkNote a f = f $ Note a Natural

flat, natural, sharp :: Note -> Cont Note r
flat    = mkSemi Flat
natural = mkSemi Natural
sharp   = mkSemi Sharp
mkSemi semi (Note k _) f = f $ Note k semi

Then, the scale, note, and chord building functions can resolve the Conts to plain types in either postfix form (i.e., as continuations to be passed to the Cont):

major, minor :: Note -> Scale
major n = Scale n Major
minor n = Scale n Minor

note :: Note -> Note
note = id

or prefix form (i.e., taking Conts as arguments):

chord :: [Cont Note [Note]] -> Chord
chord = Chord . foldr step []
  where step f acc = f (:acc)

Now, you can write:

> c sharp note
Note C Sharp
> c note
Note C Natural
> c major
Scale (Note C Natural) Major
> b flat note
Note B Flat
> c sharp major
Scale (Note C Sharp) Major
> chord [a sharp, c]
Chord [Note A Sharp,Note C Natural]

Note that c itself doesn't have a Show instance, but c note does.

With a modification to the Note type, you could easily support double accidentals (e.g., c sharp sharp, distinct from d), etc.

K. A. Buhr
  • 45,621
  • 3
  • 45
  • 71
  • Nice. I actually did try to solve my problem with `Cont` however, I tried to stick it to the constructors `A | B | C ...` instead of using functions. I couldn't get this to work and I still don't understand why, given that value constructors are just functions. If I can stick a function in front of my Keys, many things become possible. If the function is `flip ($)` then I get your pattern `flip ($) B :: Cont Key r`. My original `aScale = scale C Major` is not much different. – Martin Drautzburg Feb 01 '20 at 14:29
3

But I cannot confine Keys to constructing Scales. They are needed for other things as well (e.g. constructing chords). Also Key should be an instance of Show.

You can use typeclasses to cleverly work around that:

{-# LANGUAGE FlexibleInstances #-}

data Key = C | D | E | F | G | A | B deriving(Show)

data Mode = Major | Minor

data Scale = Scale Key Mode

class UsesKey t where
  c, d, e, f, g, a, b :: t

instance UsesKey Key where
  c = C
  d = D
  e = E
  f = F
  g = G
  a = A
  b = B

instance UsesKey (Mode -> Scale) where
  c = Scale C
  d = Scale D
  e = Scale E
  f = Scale F
  g = Scale G
  a = Scale A
  b = Scale B

aScale :: Scale
aScale = c Major

Now, you can use the lowercase letters for other types too by defining appropriate instances.