-1

My goal is to take an input like:

[(8, P1), (8, P2), (10, P3)]

And turn it into something like:

[(8, [P1, P2]), (10, P3)]

Given that numbers like 8 and 10 are the datatype Time (wrapping and Int) and P1 and P2 are the datatypes Person (which wraps a String). This is what I did so far:

groupTogether :: [(Time, Person)] -> [(Time, Person)]
groupTogether [] = []
groupTogether ((x, y) : (a, b) : ks)
  | x == a = (x, (y : (b))) : groupTogether ks
  | otherwise = (x, y) : groupTogether ((a, b) : ks)

And It "kinda" works but usually the outputs are things like (8, Name1Name2) instead of (8, [Name1, Name1]). And I just don't know how to write what the function should do once there is only one element in the list. It says that there is an "exaustive pattern" missing. What am I doing wrong? If I try to put the elements togheter using : the code won't run.

Enlico
  • 23,259
  • 6
  • 48
  • 102
guib
  • 115
  • 5

2 Answers2

2

What about this?

import Data.List
import Data.Function

data P = P1 | P2 | P3 deriving Show

x = [(8, P1), (8, P2), (10, P3)]

fun list = let lists = groupBy ((==) `on` fst) x -- [[(8,P1),(8,P2)],[(10,P3)]]
               nums  = map (fst . head) lists    -- [8,10]
               ps    = (map . map) snd lists     -- [[P1,P2],[P3]]
           in zip nums ps                        -- [(8,[P1,P2]),(10,[P3])]      

In lists I've grouped the items by equality on the number, in nums I've extracted the number which is common to all items in each group, in ps I've extracted those P* things, whatever they are, via (map . map) . snd which applies snd through two functorial layers.

Note that if in general the items in x with equal number are not necessarily adjacent (in your example they are), you might want to sort the list before using groupBy, using an appropriate sorting algorithm, as suggested in the comments below.


As regards your desired output

[(8, [P1, P2]), (10, P3)]

this is simply not possible to obtain, because in Haskell all the elemnts of a list have the same type, but (8, [P1, P2]) and (10, P3) have to different types, namely (Time, [Person]) and (Time, Person). This was already implied by some of the comments under your question, but you haven't corrected your question yet (you should). In my answer I've assumed you meant to write [(8, [P1, P2]), (10, [P3])].

As regards your attempt

groupTogether :: [(Time, Person)] -> [(Time, Person)]
groupTogether [] = []
groupTogether ((x, y) : (a, b) : ks)
  | x == a = (x, (y : (b))) : groupTogether ks
  | otherwise = (x, y) : groupTogether ((a, b) : ks)

there are several syntactic problems with it:

  • the signature is wrong, as it signals that the output has the same type of the input; this is certainly possible, but does not reflect the (corrected) desired output; probably you meant to write groupTogether :: [(Time, Person)] -> [(Time, [Person])]
  • groupTogether [] = [] handles an empty list input, whereas groupTogether ((x, y) : (a, b) : ks) handles a two-elements-or-more list input, but there's no way to deal with a singleton list, which is exactly what the "exaustive pattern missing" error alludes to;
  • since y and b have the same type, Person, the expression y : (b) is incorrect because it's equivalent to y:b, and : wants an a on the left and a [a] on the right; you might want to change that to y:[b], or maybe [y,b];
  • in a similar way, y in the otherwise case should be [y].

However, even if you apply the corrections above, there would still be something that is not quite right. Look at this:

groupTogether ((x, y) : (a, b) : ks)
  | x == a = (x, y : [b]) : groupTogether ks

You're pattern matching the first two pair in the list and putting them in one single pair, but what if the first pair in ks has another a as its first element? You're leaving it in ks, not grouping it with the other two. This is either wrong or not clear from the text of your question, in which case you should improve it.

Enlico
  • 23,259
  • 6
  • 48
  • 102
  • 2
    It would be worth mentioning that `groupBy` only groups **adjacent** items. It's unclear from the OP's question whether similar items are assumed to be adjacent or not (they are in the very small given example, so that depends what the OP means by "an input like"). If items are not assumed to be adjacent, a simple solution is to sort the list of tuples first (sorting by the first element of the tuple, with a stable sort preferably). – Stef Aug 28 '21 at 16:56
  • 2
    @Stef Haskell's library `sort` [is stable](https://hackage.haskell.org/package/sort-1.0.0.0/docs/Data-Sort.html), FYI. – Will Ness Aug 28 '21 at 17:04
  • 2
    @WillNess Thanks for the precision; I'm not very familiar with Haskell. Still, the emphasis on "stable sort by first element of the tuple" is important; for instance, a "stable sort by lexicographical order of the tuples" would in particular be a sort by the first element of the tuples, but it would be an unstable one. – Stef Aug 28 '21 at 17:07
  • @Stef a stable sort by one criterion can be unstable when judged by another, naturally. – Will Ness Aug 28 '21 at 17:09
  • 2
    @WillNess Yes. As simple as it is, it's worth mentioning. It would be too easy to use `sort` and rely on the default lexicographical ordering of tuples, thinking "hey, if it sorts the tuples, then it sorts by the first elements of the tuple", rather than bothering with `sortBy` and an explicit specification to sort by the first element; but accidentally losing the stability of the sort in the process. – Stef Aug 28 '21 at 17:16
  • 1
    So many notifications and no upvotes :D However, I'll mention the sorting problem. – Enlico Aug 28 '21 at 17:29
  • @Enlico but how do I turn the function into something that will rearrange any given "x" that I want? Lets say, I have a sorting function and this "list" function, how can I do something like: `sort(list([(8, P1), (8, P2), (10, P3)]))`? Because that's what I am kind of going for and I need to solve this to any "x" that another function is providing. And yes, I am sorting the input first, so your solution looks great to me! I just don't know how to adapt. – guib Aug 28 '21 at 21:20
  • @guib, I don't know what _a sorting function this "list" function_ are in your case. Please, clarify **_in the question_**, providing a well written explanation and maybe the signatures and or definitions of these functions. – Enlico Aug 28 '21 at 21:46
  • @Stef I never implied it shouldn't be mentioned, I just reacted to the word "preferably". :) – Will Ness Aug 29 '21 at 08:10
  • 1
    @guib see about using `sortBy (comparing fst)` . – Will Ness Aug 29 '21 at 08:12
2

How about this? (Note that the Foldable t can be though of as a list)

% ghci
λ> :m +Data.Map
λ> data P = P1 | P2 | P3 deriving (Eq, Ord, Show)
λ> let input = [(8, P1), (8, P2), (10, P3)] :: [(Int, P)]
λ> :type Prelude.foldl
_ :: Foldable t => (b -> a -> b) -> b -> t a -> b
λ> :type Prelude.foldl (\mp (k, p) -> insertWith (<>) k [p] mp)
_ :: (Foldable t, Ord k) => Map k [a] -> t (k, a) -> Map k [a]
λ> :type Prelude.foldl (\mp (k, p) -> insertWith (<>) k [p] mp) mempty
_ :: (Foldable t, Ord k) => t (k, a) -> Map k [a]
λ> :type Prelude.foldl (\mp (k, p) -> insertWith (<>) k [p] mp) mempty input
_ :: Map Int [P]
λ> Prelude.foldl (\mp (k, p) -> insertWith (<>) k [p] mp) mempty input
fromList [(8,[P2,P1]),(10,[P3])]
λ> toList (Prelude.foldl (\mp (k, p) -> insertWith (<>) k [p] mp) mempty input)
[(8,[P2,P1]),(10,[P3])]
David Fox
  • 654
  • 4
  • 10
  • 1
    `foldl`+`insertWith` is probably better spelled `fromListWith`. This is the answer I suggest in the linked question, "How to group similar items in a list using Haskell?". I consider that question not a duplicate (and therefore this is not an answer) because the question asked here was "What am I doing wrong?" and not "What should I do instead?". – Daniel Wagner Aug 30 '21 at 19:42