views:

762

answers:

5

I am a beginner in Haskell so please bear with me. (Just started learning yesterday!) How can I sort a list of tuples primarily by their first components (highest to smallest) and secondarily by their second components (smallest to highest)? A sample input/output would be:

[(1, "b"), (1, "a"), (2, "b"), (2, "a")] (input)

[(1, "a"), (2, "a"), (1, "b"), (2, "b")] (middle step)

[(2, "a"), (2, "b"), (1, "a"), (1, "b")] (output)

I tried using the following but it gave wrong output:

sortGT a b = GT

sortBy sortGT lst

I am sure that I can do this by using sortBy only, but I can't figure it out myself. Any help would be highly appreciated!

+6  A: 

You need to construct your function sortGT, so that it compares pairs the way you want it:

sortGT (a1, b1) (a2, b2)
  | a1 < a2 = GT
  | a1 > a2 = LT
  | a1 == a2 = compare b1 b2


Using this you get the following results (I used ghci):

*Main Data.List> sortBy sortGT [(1, "b"), (1, "a"), (2, "b"), (2, "a")]
[(2,"a"),(2,"b"),(1,"a"),(1,"b")]
3lectrologos
@haskell noob Note that the comparison of the first arguments (the a's) actually comes first, because that comparison takes precedence over the b's. I would probably describe this as "sorting by the first argument first" but of course it's clear what you meant, since you gave an example.
MatrixFrog
Thanks 3lectrologos! Worked like a charm.@MatrixFrog - Sorry for the confusing english. Not my first language anyways!
eclipseNoob
This is what "compare" does by default anyway. compare (1,"b") (2, "a") = LT. The original poster wanted to do it the other way around.
Paul Johnson
@Paul Johnson: The above implementation gives sortGT (1,"b") (2,"a") = GT, because a1 = 1 < 2 = a2, so it's not what "compare" does by default.
3lectrologos
A: 

It's always tricky composing two-argument functions. Here's an implementation:

invert :: Ordering -> Ordering
invert GT = LT
invert LT = GT
invert EQ = EQ


sosort :: (Ord a, Ord b) => [(a, b)] -> [(a, b)]
sosort = sortBy (\p p' -> invert $ uncurry compare $ double fst p p') . 
         sortBy (\p p' ->          uncurry compare $ double snd p p')
  where double f a a' = (f a, f a')

Because sortBy expects a function of two arguments, function composition isn't so nice.

I have tested this code, and it works on your example.

As Fred points out, you can write compare EQ instead of invert. As Dario points out, I could be using on from Data.Function, but in fact on compare == comparing, which I can use instead. Now the code can be read only by a Haskell Master:

sosort :: (Ord a, Ord b) => [(a, b)] -> [(a, b)]
sosort = sortBy (compare EQ `post` comparing fst) . sortBy (comparing snd)
  where post f g x x' = f (g x x')

I have compiled and run this code and it works on the original example.

(I haven't got any votes for this answer, but thanks to good comments, I sure have learned a lot about the Haskell library. Who knows what function post is equivalent to? Not Hoogle...)

It would be more idiomatic to write a suitable comparison function for pairs, but your question asked for consecutive sorts.

Norman Ramsey
Wow. This is too confusing. It works but it's hard and beyond my level of haskell knowledge. Thanks though!
eclipseNoob
You can simplify the invert function to `invert = compare EQ` ;)
FredOverflow
@Fred: Sweet! But I do worry about making the code completely impenetrable. I've put your version up for comparison.
Norman Ramsey
Can't you even get rid of explicit lambda parameters `p` and `p'` and make your code pointlessly point-free ;) ? Would be nice too ...
Dario
Isn't `\p p' -> uncurry compare $ double fst p p'` equal to `compare 'on' fst`?
Dario
@Dario: Nice work. I always forget about `on`. But we can do better: `on compare == comparing`. Now if only there were a predefined version of `post`...
Norman Ramsey
+3  A: 

May I suggest the following?

import Data.List (sortBy)
import Data.Monoid (mconcat)

myPredicate (a1, a2) (b1, b2) = mconcat [compare b1 a1, compare a2 b2]

You can then sort by writing sortBy myPredicate lst. The function mconcat simply scans through the list and obtains the first non-EQ occurence (or EQ if all elements are EQ and thus both pairs are considered equal).

On second thought, building the list isn't necessary.

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

myPredicate (a1, a2) (b1, b2) = compare b1 a1 `mappend` compare a2 b2

The definition of mappend for Ordering is essentially:

EQ `mappend` x = x
x  `mappend` _ = x

Which is exactly what we need.

Just for fun, generalizing gbacon's answer and making the use a little more flexible:

import Data.Ord
import Data.List
import Data.Monoid

ascending  = id
descending = flip

sortPairs f x g y = f (comparing x) `mappend` g (comparing y)

mySort = sortBy (sortPairs descending fst ascending snd)
FredOverflow
+4  A: 

Congratulations on taking your first steps to learn Haskell. It's a great journey!

Riffing on FredOverflow's answer:

import Data.Ord
import Data.List
import Data.Monoid

main :: IO ()
main = do
  print $ sortBy cmp [(1, "b"), (1, "a"), (2, "b"), (2, "a")]
  where
    cmp = flip (comparing fst) `mappend` comparing snd

Output:

[(2,"a"),(2,"b"),(1,"a"),(1,"b")]
Greg Bacon
+1  A: 

First we should make the ordering function wich takes two touples and returns either EQ,LT or GT (ie. sortGT :: (a,b)->(a,b)->Ordering.) Since you want the first argument to have first priority and reversed order, we check that first and if they are equal (compare returns EQ) we check the second argument, otherwise we return LT if its GT and GT if its LT. This is what I think is easiest on the eyes :

sortGT (a1,b1) (a2,b2) = case compare a1 a2 of
                              EQ -> compare b1 b2
                              LT -> GT
                              GT -> LT

Now we use sortBy as you suggested :

*Main> sortBy sortGT [(1, "b"), (1, "a"), (2, "b"), (2, "a")]
[(2,"a"),(2,"b"),(1,"a"),(1,"b")]
HaskellElephant
+1 for a noob-friendly solution.
Nefrubyr