Eliminate consecutive duplicates from a string

Viewed 1721

I want to eliminate consecutive duplicates from a string like f "aaabbbcccdeefgggg" = "abcdefg"

This is my code

f :: String -> String
f "" = ""
f "_" = "_"
f (x : xs : xss)
    | x == xs   = f (xs : xss)
    | otherwise = x : f (xs : xss)

I got as error non-exhaustive patterns, I think it's from second line, the program doesn't know how to deal when it's only 1 char left. How should I fix it?

5 Answers

The "_" pattern does not match a string with any character, it matches the string that contains an underscore.

You can use [_] as pattern for the singleton string, so:

f :: String -> String
f "" = ""
f s@[_] = s
f (x : xs : xss)
    | x == xs   = f (xs : xss)
    | otherwise = x : f (xs : xss)

here we use s@ to capture the string with one character as s.

or we can simplify this with:

f :: String -> String
f (x : xs : xss)
    | x == xs   = f (xs : xss)
    | otherwise = x : f (xs : xss)
f s = s

I want to eliminate consecutive duplicates from a string like f "aaabbbcccdeefgggg" = "abcdefg"

You can group the letters which are equal (via Data.List.group), and then take the first of each group (via map head, which applies head to each element of the list and gives you back the list of the results):

import Data.List (group) -- so we write group instead of Data.List.group
map head $ group "aaabbbcccdeefgggg"

This can be seen as the application of map head after the application of group to the input String. Therefore, your f can be defined like the composition of those two functions:

f :: String -> String
f = map head . group

For completeness, as you seem to be new to Haskell, here are a few details:

  • Data.List.group "aaabbbcccdeefgggg" returns ["aaa","bbb","ccc","d","ee","f","gggg"];
  • f $ a b c is the same as f (a b c);
  • . is the composition operator, and it is such that (f . g) x == f (g x).
    • (Unrelated to this question, but important to bear in mind in general, . operates on unary functions, which means that if g takes more than one argument, the composition will pipe a partially applied g to f as soon as you give f . g one argument. In other words, if g is, say, binary, then f (g x y) is not equal to (f . g) x y, which will generally not even compile, but to ((f .) . g) x y. Some more example about this, but in JavaScript, is in this answer of mine, which should be fairly readable for a Haskell programmer.)

Or you can make it even simpler if you don't handle things that can be left unhandled:

f :: String -> String
f (x:y:xs) | x == y = f (y:xs)
f (x:xs) = x:f xs
f _ = ""

To avoid looking ahead further than necessary, you shouldn't try to match on the first two elements. Instead, keep track of the most recent element.

f :: Eq a => [a] -> [a]
f = start
  where
    start [] = []
    start (x : xs) = x : go x xs

    go _old [] = []
    go old (x : xs)
      | x == old
      = go old xs
      | otherwise
      = x : go x xs

If you want, you can also write this as a fold, where you track whether you've seen an element yet using a Maybe:

f :: Eq a => [a] -> [a]
f xs = foldr go stop xs Nothing
  where
    stop _ = []
    go x r (Just old)
      | x == old = r (Just old)
    go x r _ = x : r (Just x)

Some may find the fold easier to read if it's rearranged a bit.

f :: Eq a => [a] -> [a]
f xs = (foldr go stop xs) Nothing
  where
    stop :: Maybe a -> [a]
    stop = \_ -> []

    go :: a -> (Maybe a -> [a]) -> (Maybe a -> [a])
    go x r = \acc -> case acc of
      Just old
        | x == old -> r (Just old)
      _ -> x : r (Just x)

You also could use foldl. the logic is: comparing the last element of accumulator with current element.

f :: Eq a => [a] -> [a]
f xs = foldl (\x y -> if last x == y then x else x++[y] )  [head xs] xs

here we initiate our accumulator with [head x].

Based on @dfeuer hint I change my solution:

-- with foldl
f :: Eq a => [a] -> [a]
f xs = snd $ foldl  opr (Nothing, []) xs
  where
    opr (Just old, acc) n
      | old == n = (Just old, acc)
      | otherwise = (Just n, acc ++ [n])
    opr (Nothing, acc) n = (Just n, acc ++ [n])
-- with foldr
f2 :: Eq a =>[a] -> [a]
f2 xs = snd $ foldr opr (Nothing, []) xs
  where
    opr n (Just old, acc)
      | old == n = (Just old, acc)
      | otherwise = (Just n, n:acc)
    opr n (Nothing, acc) = (Just n, n:acc)

Thanks to @dfeuer, I learned a lot of new things. This is the third version based on comments:

-- with foldl
f :: Eq a => [a] -> [a]
f xs = snd $ foldl opr (Nothing, []) xs
  where
    opr (old, acc) n =
      ( Just n,
        case old of
          Just o
            | o == n -> acc
            | otherwise -> acc ++ [n]
          Nothing -> acc ++ [n]
      )
-- with foldr
f :: Eq a => [a] -> [a]
f xs = snd $ foldr opr (Nothing, []) xs
  where
    opr n (old, acc) =
      ( Just n,
        case old of
          Just o
            | o == n -> acc
            | otherwise -> n : acc
          Nothing -> n : acc
      )
Related