4

I want to store a list of "things" which can have some optional extra attributes attached to them. Each thing can have one or more attributes. And different attributes are of different types.

I want to be able to create literal lists of these things concisely in code. But I'm having trouble seeing how to get this past the type system because tuples allow mixtures of types but are fixed length, while lists are variable length but one type.

This is a toy example of what I want to be able to do :

things = [ Thing 1 RED, Thing 2 RED LARGE, Thing 3 BLUE SMALL, Thing 4 SMALL, Thing 5 BLUE DOTTED ] 

etc.

What's the right way to do this?

2
  • 1
    You you give a slightly less toy-ish example, at least roughly the application you're aiming at? Commented Mar 2, 2014 at 21:55
  • 1
    It's a music application. The "Things" are really Chords and the optional attributes are really hints at particular kinds of transformation (add a 7th, remove the root, spread across two octaves etc.) I didn't want to go into too much detail because I don't want people saying "to do music, use Haskore" etc. This is a learning exercise so I'm using it to get an understanding of the principles beyond any particular application. Commented Mar 2, 2014 at 22:01

3 Answers 3

7

Let's say Chords are collections of Notes.

data Note = A | Bb | B | C | ... 

But also with optional sets of Annotations

data Ann = Seventh | Sus2 | Split | ... 

We can model Chords thus as

data Chord = Chord { notes :: [Note] , anns :: [Ann] } 

We can build an entire vocabulary this way

maj :: Note -> Chord ann :: Ann -> Chord -> Chord transpose :: Int -> Note -> Note transposeChord :: Int -> Chord -> Chord 

And then build our list like so

chords = [ ann Seventh (maj C) , ann Split (ann Sus2 (maj A)) ] 
Sign up to request clarification or add additional context in comments.

Comments

4

Basically, rather than storing the attributes as given, you should store the resultant properties of a chord with these attributes. One simple (but not really nice, musically) solution would be, storing only the final pitches:

newtype Pitch = Pitch {midiNote :: Int} a, as, bb, b, bs, c, cs, db, d, ds, eb, e, es, f, fs, gb, g, gs, ab :: Pitch [ a, as,bb, b,bs, c,cs,db, d,ds,eb, e,es, f,fs,gb, g,gs,ab] = map Pitch [55,56,56,57,58,58,59,59,60,61,61,62,63,63,64,64,65,66,66] type Chord = [Pitch] minor :: Pitch -> Chord minor (Pitch fund) = map (Pitch . (fund+)) [0, 3, 7] seventh :: Pitch -> Chord seventh (Pitch fund) = map (Pitch . (fund+)) [0, 4, 7, 10] spread :: Chord -> Chord spread = sort . zipWith (\octShift (Pitch note) -> Pitch $ note + 12 * octShift) $ cycle [0,1] 

To be used as e.g.

chords :: [Chord] chords = [ minor e, seventh d, minor e, minor a, seventh b, spread $ minor e ] 

A more sophisticated approach might actually store the information about a chord in a more musically meaningful way:

data Chord = Chord { fundamental :: Pitch , gender :: Maybe ChordGender , ExtraNotes :: [AddNote] , OctaveShifts :: [Int] } data ChordGender = Major | Minor data AddNote = AddNote { roughInterval :: Int, intervalIsMajor :: Bool } major :: Pitch -> Chord major fund = Chord fund (Just Major) [] [] sus4 :: Pitch -> Chord sus4 fund = Chord fund Nothing [AddNote 4 False] [] spread :: Chord -> Chord spread ch@(Chord _ _ _ shifts) = ch{shifts = cycle [0,1]} 

This can be used in much the same way, but is more versatile.

If you don't like giving the attributes as prefix functions, you can do as the diagrams package, with

infixl 8 # (#) :: a -> (a -> b) -> b (#) = flip ($) 

to write

chords = [ c # major , g # sus4 , g # major , a # minor , f # major # spread , g # sus4 # spread , g # major # spread , c # major # spread ] 

Comments

3

There are various ways to implement heterogeneous collections in Haskell, but using a list of arbitrary values of arbitrary types is probably more flexibility than you need, and more trouble than it's worth. You're probably better off creating one type with an expressive set of possible values, and using a homogeneous collection of that.

Your example attributes appear to fall into a set of known categories: color (e.g. RED, BLUE), size (e.g. LARGE, SMALL), and what I'll call "texture" (e.g. DOTTED). A Thing doesn't necessarily have an attribute in each category, but I'll assume that it shouldn't have more than one attribute in the same category — it doesn't make sense for a single Thing to be both LARGE and SMALL.

You could represent these categories as algebraic data types:

data Color = Red | Blue data Size = Large | Small data Texture = Dotted 

and combine them with a data structure:

data ThingAttributes = ThingAttributes { thingColor :: Maybe Color, thingSize :: Maybe Size, thingTexture :: Maybe Texture } 

Now you can just associate a single ThingAttributes value with each Thing.

If you do want to allow multiple attributes in the same category (e.g. having a Thing that's both LARGE and SMALL), you can use another algebraic data type to bring all the category types together:

data ThingAttribute = ColorAttribute Color | SizeAttribute Size | TextureAttribute Texture 

and then associate a Set ThingAttribute — or simply a [ThingAttribute] if you don't mind duplicates of the same attribute — with each Thing.

1 Comment

Thanks. I think I'm going with the other solution but I learned a lot from this.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.