简体   繁体   中英

In Haskell, how can I use the built in sortBy function to sort a list of pairs(tuple)?

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!

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")]

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)

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 .) Then we can give this ordering function to sortBy and it will sort it's input according to this ordering.

Since you want the first components to have first priority, we check that first and if they are equal we check the second argument,if the first components is not equal we give it the opposite value of it's original ordering, so that it is ordered highest to lowest.

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")]

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")]

The following solution works best for me, a Haskell newbie. It looks a lot like 3lectrologos ' answer: I actually only added the function definition and the List import, but it can cause some confusion if left out.

Create a function 'myCompare' and don't forget to import the List module. You'll need it to make the sortBy work. The function should look like this:

import Data.List

myCompare :: (Ord a, Ord b) => (a,b) -> (a,b) -> Ordering  
myCompare (a1,b1) (a2,b2)
     | a1 < a2     = GT  
     | a2 == a1    = EQ  
     | otherwise = LT

After loading the Haskell file, you can write the following in your terminal:

*Main> sortBy myCompare [(1, "b"), (1, "a"), (2, "b"), (2, "a")]

Which will return:

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

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.

I like the comparing function in Data.Ord. This is basically Greg's answer in even more compact form:

Prelude Data.Ord Data.List Data.Function> (reverse.sortBy (comparing fst)) [(1, "b"), (1, "a"), (2, "b"), (2, "a")]

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

"comparing fst" gives an ordering based on the first element of the tuple.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM