5

I'm trying to grapple my head around Haskell and I'm having a hard time pinning down the general procedure/algorithm for this specific task. What I want to do is basically give Haskell a list [1,2,3,5,6,9,16,17,18,19] and have it give me back [1-3, 5, 6, 9, 16-19] so essentially turning three or more consecutive numbers into a range in the style of lowestnumber - highestnumber. What I have issue with it I suppose is the all too common difficulty grappling with with the functional paradigm of Haskell. So I would really appreciate a general algorithm or an insight into how to view this from an "Haskellian" point of view.

Thanks in advance.

Will Ness
  • 70,110
  • 9
  • 98
  • 181
  • Not a dupe but [a very similar question here](https://stackoverflow.com/q/14403293/4543207). It may help you to construct the result that you want. – Redu May 08 '20 at 14:48

3 Answers3

4

If I understand the question correctly, the idea is to break up the input lists in chunks, where a chunk is either a single input element or a range of at least three consecutive elements.

So, let's start by defining a datatype for representing such chunks:

data Chunk a = Single a | Range a a

As you can see, the type is parametric in the type of input elements.

Next, we define a function chunks to actually construct a list of chunks from a list of input elements. For this, we require the ability to compare input elements and to obtain the immediate consecutive for a given input element (that is, its successor). Hence, the type of the function reads

chunks :: (Eq a, Enum a) => [a] -> [Chunk a]

Implementation is relatively straightforward:

chunks = foldr go []
 where
  go x (Single y : Single z : cs) | y == succ x && z == succ y = Range x z : cs
  go x (Range y z : cs) | y == succ x = Range x z : cs
  go x cs                             = Single x : cs

We traverse the list from right to left, generating chunks as we go. We generate a range if an input element precedes its two immediate consecutive elements (the first case of the helper function go) or if it precedes a range that starts with its immediate consecutive (the second case). Otherwise, we generate a single element (the final case).

To arrange for pretty output, we declare applications of the type constructor Chunk to be instances of the class Show (given that the type of input elements is in Show):

instance Show a => Show (Chunk a) where
  show (Single x ) = show x
  show (Range x y) = show x ++ "-" ++ show y

Returning to the example from the question, we then have:

> chunks [1,2,3,5,6,9,16,17,18,19]
[1-3,5,6,9,16-19]

Unfortunately, things are slightly more complicated if we need to account for bounded element types; such types have a largest element for which succ is undefined:

> chunks [maxBound, 1, 2, 3] :: [Chunk Int]
*** Exception: Prelude.Enum.succ{Int}: tried to take `succ' of maxBound

This suggests that we should abstract from the specific approach for determining whether one elements succeeds another:

chunksBy :: (a -> a -> Bool) -> [a] -> [Chunk a]
chunksBy succeeds = foldr go []
 where
  go x (Single y : Single z : cs) | y `succeeds` x && z `succeeds` y =
    Range x z : cs
  go x (Range y z : cs) | y `succeeds` x = Range x z : cs
  go x cs = Single x : cs

Now, the version of chunks that was given above, can be expressed in terms of chunksBy simply by writing

chunks :: (Eq a, Enum a) => [a] -> [Chunk a]
chunks = chunksBy (\y x -> y == succ x)

Moreover, we can now also implement a version for bounded input types as well:

chunks' :: (Eq a, Enum a, Bounded a) => [a] -> [Chunk a]
chunks' = chunksBy (\y x -> x /= maxBound && y == succ x)

That merrily gives us:

> chunks' [maxBound, 1, 2, 3] :: [Chunk Int]
[9223372036854775807,1-3]
Stefan Holdermans
  • 7,990
  • 1
  • 26
  • 31
  • Thanks a ton for the very elaborate explanation! – Bob Pettersson May 08 '20 at 14:49
  • 2
    This is wrong for bounded types. `chunks ([127, 1, 2, 3] :: [Int8])` should be `[127, 1-3]`, but it actually errors with ``*** Exception: Enum.succ{Int8}: tried to take `succ' of maxBound``. – Joseph Sible-Reinstate Monica May 08 '20 at 17:31
  • @JosephSible-ReinstateMonica Yes, that's a good catch. So, depending on the actual use case, I'd argue to either use a monomorphic implementation of `chunks` (if needed adapted to protect against overflows) or otherwise implement a version that accounts for bounded types as well. I'll update the answer. Thanks a lot for pointing this out! – Stefan Holdermans May 08 '20 at 20:53
  • 1
    Definitely better now. Suggestion: since most `Enum`s also have a reasonable `Ord` instance, you could do `chunksBy (\y x -> y > x && y == succ x)` to have just one function that works on almost any type you'd want to use it with, whether bounded or not. – Joseph Sible-Reinstate Monica May 08 '20 at 21:19
  • @JosephSible-ReinstateMonica That's a nice one as well. `Ord` seems like a reasonable constraint for most occurrences of this problem in the wild. – Stefan Holdermans May 08 '20 at 21:23
  • 2
    @StefanHoldermans After asking [my own question about this](https://stackoverflow.com/q/61688753/7509065), it turns out there's an even better approach: `chunksBy (\y x -> case [x..y] of [_,_] -> True; _ -> False)` And it doesn't even need an `Eq` constraint! – Joseph Sible-Reinstate Monica May 08 '20 at 22:16
1

First, all elements of a list must be of the same type. Your resulting list has two different types. Ranges (for what ever that means) and Ints. We should convert one single digit into a range with lowest and highest been the same.

Been said so, You should define the Range data type and fold your list of Int into a list of Range

data Range = Range {from :: Int , to :: Int}

intsToRange :: [Int] -> [Range]
intsToRange [] = []
intsToRange [x] = [Range x x]
intsToRange (x:y:xs) = ...  -- hint: you can use and auxiliar acc which holds the lowest value and keep recursion till find a y - x differece greater than 1.

You can also use fold, etc... to get a very haskelly point of view

lsmor
  • 4,698
  • 17
  • 38
1

Use recursion. Recursion is a leap of faith. It is imagining you've already written your definition and so can ("recursively") call it on a sub-problem of your full problem, and combine the (recursively calculated) sub-result with the left-over part to get the full solution -- easy:

ranges xs  =  let  (leftovers, subproblem) = split xs
                   subresult = ranges subproblem
                   result = combine leftovers subresult
              in
                   result
   where
   split xs  =  ....
   combine as rs  =  ....

Now, we know the type of rs in combine (i.e. subresult in ranges) -- it is what ranges returns:

ranges :: [a] -> rngs

So, how do we split our input list xs? The type-oriented design philosophy says, follow the type.

xs is a list [a] of as. This type has two cases: [] or x:ys with x :: a and ys :: [a]. So the easiest way to split a list into a smaller list and some leftover part is

    split (x:xs)  =  (x, ys)
    split []      =  *error* "no way to do this"   -- intentionally invalid code

Taking note of the last case, we'll have to tweak the overall design to take that into account. But first things first, what's the rngs type could be? Going by your example data, it's a list of rngs, naturally, rngs ~ [rng].

A rng type though, we have a considerable degree of freedom to make it to be whatever we want. The cases we have to account for are pairs and singletons:

data Rng a  =  Single a 
            |  Pair a a

.... and now we need to fit the jagged pieces together into one picture.

Combining a number with a range which starts from consecutive number is obvious.

Combining a number with a single number will have two obvious cases, for whether those numbers are consecutive or not.

I think / hope you can proceed from here.

Will Ness
  • 70,110
  • 9
  • 98
  • 181