3
\$\begingroup\$

I am new to Haskell and took it upon myself to make a simple generic list-splitting (e.g. string-splitting) function:

-- Helper for splitting: reduce into a list of lists, splitting on delimiters
splitInner :: (Eq a) => [a] -> a -> [[a]] -> [[a]]
splitInner c s (x:y)
    | s `elem` c = [] : (x:y)
    | otherwise = (s : x) : y
splitInner c s []
    | s `elem` c = []
    | otherwise = [[s]]

-- Split a list into a list of lists on a given set of delimiters
split :: (Eq a) => [a] -> [a] -> [[a]]
split delim = foldr (splitInner delim) []

-- Remove empty lists from a list of lists
collapse :: (Eq a) => [[a]] -> [[a]]
collapse = filter (/= [])

-- Split a string on any span of whitespace within
splitSpaces = collapse . split "\t\r\n "

main = (print . splitSpaces) "\t \r\nhello     world, this is code!\nFoo bar  "
-- prints ["hello", "world,", "this", "is", "code!", "Foo", "bar"]

I'm aware that there's a split package that already implements functionality like this.

However, I'm curious: is there any way I could have expressed this functionality better, or more concisely or clearly, still using just the standard library? Or, perhaps, does my code deviate from a typical Haskell solution in some other way? I'm much more used to imperative than functional programming and don't yet have the best sense on evaluating the 'readability' or clarity of code like this.

I'd also like to point out the fact that I couldn't figure out a good way to clearly label the arguments to a function in its definition - the signature [a] -> [a] -> [[a]] certainly doesn't reveal which [a] is the delimiter, for example.

\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

Regarding identification of function arguments; it's often enough to just name them well. For example it's clear that split's first arg is the delimiter because it's named. Sometimes that's not enough, and it doesn't work for arguments you're not binding, so deffer to a more formal style of comments, which in Haskell means Haddock. This will help with splitInner, but you should still rename those arguments!

The behavior of split as written is kinda weird. Basically, a long chain of delimiters will behave differently (append a bunch of empty lists, or not) depending if they're at the beginning or end of the input.

Do you want to have separate split and collapse functions? If you do then you should do something to normalize the behavior of split, but probably it's better to just fix split so you don't need collapse (which needed a better name anyway).

split :: (Eq a) => [a] -> [a] -> [[a]]
split deliminators = discard . foldr inner [[]]
  where discard xs(x : xs') = if null x then xs' else xs
        inner item acc@(current : closedChunks)
          | item `elem` deliminators = [] : (discard acc)
          | otherwise = (item : current) : closedChunks

As for the most idiomatic Haskell way to do all this...
You could read the source code for the pre-existing library, but that might not be what you need right now. The basic idea of what they're doing is they're acknowledging that there are a lot of different things people might mean by "I want a function to split a list.", so they've built a framework within which it's easy to build lots of "split" functions out of components. That's certainly idiomatic Haskell! but it usually isn't one's first strategy.

\$\endgroup\$
3
\$\begingroup\$

I agree with the previous poster that behavior of split is unexpected and should be changed, but instead of talking about that, I will add a few style suggestions.

My first suggestion is to put your helper functions below their callers. Most people will read your file top-down, and it's much easier to understand the functions after seeing the context in which they are used. I would also put them in a where clause. Using a where clause will limit the scope of the helper functions to only where they are needed, which keeps things clean when modules get larger.

My second suggestion is to follow the common Haskell convention of using (x:xs), instead of writing (x:y). Using xs makes it read as "plural", which is a cue to the reader that it is a list. Third the word delim should probably be plural (delims) to inform the reader that it is a list. Fourth, I would inline collapse. After applying these suggestions, your code would look like this.

main = (print . filter (/= "") . split "\t\r\n ") "\t \r\nhello     world, this is code!\nFoo bar  "

split :: (Eq a) => [a] -> [a] -> [[a]]
split delims = foldr (splitInner delims) []
  where
    splitInner delims c (x:y)
        | c `elem` delims = [] : (x:y)
        | otherwise = (c : x) : y
    splitInner c s []
        | s `elem` c = []
        | otherwise = [[s]]
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.