brooks94
brooks94

Reputation: 3936

Haskell: How to put multiple instances in the same module?

Let's say that I have the following code:

import Data.List.Ordered

data Person = Person String String
     deriving (Show, Eq)

main :: IO ()
main = print . show . sort $ [(Person "Isaac" "Newton"), (Person "Johannes" "Kepler")]

And in the same module, I want to be able to sort the list by both first name and by last name. Obviously I can't do this:

instance Ord Person where
         compare (Person _ xLast) (Person _ yLast) = compare xLast yLast

instance Ord Person where
         compare (Person xFirst _) (Person yFirst _) = compare xFirst yFirst

So, what are my options?

This page mentions "You can achieve this by wrapping the type in a newtype and lift all required instances to that new type." Can someone give an example of that?

Is there a better way?

Upvotes: 4

Views: 2023

Answers (4)

huon
huon

Reputation: 102166

The newtype method would be:

newtype ByFirstname = ByFirstname { unByFirstname :: Person }

instance Ord ByFirstname where
  -- pattern matching on (ByFirstname (Person xFirst _)) etc
  compare [...] = [...]

newtype ByLastname = ByLastname { unByLastname :: Person }

instance Ord ByLastname where
  -- as above

Then the sorting function would be something like:

sortFirstname = map unByFirstname . sort . map ByFirstname

and similarly for ByLastname.


A better way is to use sortBy, compare and on, with functions for retrieving the first and last names. i.e.

sortFirstname = sortBy (compare `on` firstName)

(On that note, it might be worth using a record type for Person, i.e. data Person = Person { firstName :: String, lastName :: String }, and one even gets the accessor functions for free.)

Upvotes: 19

Luis Casillas
Luis Casillas

Reputation: 30237

You don't want to define multiple Ord instances just to sort by different orders. You just need to use the sortBy function, which takes an explicit comparison function as its argument.

Neat trick: if you use record types to define your Person type, import Data.Function (which gives you the on function) and Data.Monoid, you can use some neat tricks to make this much briefer and easy:

import Data.Function (on)
import Data.Monoid (mappend)
import Data.List (sortBy)

data Person = Person { firstName :: String, lastName :: String }

instance Show Person where
    show p = firstName p ++ " " ++ lastName p

exampleData = [ Person "Mary" "Smith"
              , Person "Joe" "Smith"
              , Person "Anuq" "Katig"
              , Person "Estanislao" "Martínez"
              , Person "Barack" "Obama" ]
-- 
-- The "on" function allows you to construct comparison functions out of field
-- accessors:
--
--     compare `on` firstName :: Person -> Person -> Ordering
--     
-- That expression evaluates to something equivalent to this:
--
--    (\x y -> compare (firstName x) (firstName y))
--
sortedByFirstName = sortBy (compare `on` firstName) exampleData
sortedByLastName = sortBy (compare `on` lastName) exampleData

--
-- The "mappend" function allows you to combine comparison functions into a
-- composite one.  In this one, the comparison function sorts first by lastName,
-- then by firstName:
--
sortedByLastNameThenFirstName = sortBy lastNameFirstName exampleData
    where lastNameFirstName = 
              (compare `on` lastName) `mappend` (compare `on` firstName) 

Upvotes: 9

asm
asm

Reputation: 8898

The reason that page mentions newtype is that you cannot have two instances of the same class for a given type. Well, you can but not in the same scope and it's generally a really bad idea. See Orphan Instances for more information (as C. A. McCann pointed out this isn't about orphan instances. I included the link because it has a good description of why duplicate instances, even in cases that superficially work, is a bad idea.).

An example of newtype would be:


newtype SortFirst = SF Person

instance Ord Person where
         compare (Person _ xLast) (Person _ yLast) = compare xLast yLast

instance Ord SortFirst where
         compare (SF (Person xFirst _)) (SF (Person yFirst _)) = compare xFirst yFirst

Then when you want to sort by first name you would have to do:

sort (map SF persons)

It's not very convenient so it may be better to use sortBy which takes an comparison function as an argument.

Upvotes: 4

C. A. McCann
C. A. McCann

Reputation: 77404

It seems that all the functions in Data.List.Ordered have "by" variants that let you provide a comparison function rather than using an Ord instance. If there's no single obvious "standard" ordering, that's not a bad option.

It then becomes your responsibility to ensure any given list is always used with a consistent comparison function, however, which is a potential source of all kinds of fun bugs.

If I needed to work with many ordered lists using a variety of orderings, rather than fussing with newtype wrapping and unwrapping--which is dreadfully tedious--I'd consider a data type to bundle a list with its comparison function, then define proxy functions that use the "by" functions with the associated comparison.

Upvotes: 3

Related Questions