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
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.)
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.
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)
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.