3
\$\begingroup\$

From https://www.codeeval.com/browse/115/:

You have a string of words and digits divided by comma. Write a program which separates words with digits. You shouldn't change the order elements.

I tried to do it using Haskell (learning Haskell).

Can you give me some good practices/shortcuts/advice on how to make this code better?

{-# LANGUAGE OverloadedStrings #-}
import qualified Data.Text as T
import System.Environment (getArgs)

main = do
    args <- getArgs
    content <- readFile(args !! 0)
    let fileLines = lines content
    mapM (\x -> putStrLn (mixedContent (generateContent (wordsWhen (==',') x)) [] [])) fileLines


data Content =  String String | Num Int deriving (Show)

generateContent :: [String] -> [Content]
generateContent [] = []
generateContent (x:xs) = if isNumeric x then [(Num (read x))] ++ generateContent xs else [String x] ++ generateContent xs 


mixedContent :: [Content] -> [Int] -> [String] -> String
mixedContent  [] d w = foldl (\r x -> r ++ x) "" $ giveResults (generateCommaDelimitedList w) ["|"]  (generateCommaDelimitedList (intToChar d))
mixedContent  ((String x):xs) d w = mixedContent xs d (w ++ [x])
mixedContent  ((Num x):xs) d w = mixedContent xs (d ++ [x]) w


giveResults :: [String] -> [String] -> [String] -> [String]
giveResults [] _ [] = []
giveResults [] _ d = d
giveResults w _ [] = w
giveResults w delimitor d = w ++ delimitor ++ d


intToChar :: [Int] -> [String]
intToChar l = map (\i -> show i) l

generateCommaDelimitedList :: [String] -> [String]
generateCommaDelimitedList [] = []
generateCommaDelimitedList (x:[]) = [x]
generateCommaDelimitedList (x:xs) = (x ++ ","):(generateCommaDelimitedList xs)


isInteger s = case reads s :: [(Integer, String)] of
  [(_, "")] -> True
  _         -> False

isDouble s = case reads s :: [(Double, String)] of
  [(_, "")] -> True
  _         -> False

isNumeric :: String -> Bool
isNumeric s = isInteger s || isDouble s


-- split string equivalent
wordsWhen :: (Char -> Bool) -> String -> [String]
wordsWhen p s =  case dropWhile p s of
                    "" -> []
                    s' -> w : wordsWhen p s''
                            where (w, s'') = break p s'

EDIT: I follow advice given in the answer. I just wanted to show how the main should be to respect what the exercise was asking.

main = do
    args <- getArgs
    content <- readFile(args !! 0)
    let l = lines content
    let lf = filter (\x -> length x > 0) l
    mapM_ (putStrLn . arrange) lf

The interesting part is mapM_, which makes me able to print the lines re-arranged and ignore the result returned by the mapM

\$\endgroup\$
2
\$\begingroup\$

I think you've overcomplicated the problem because you're approaching it the way you would in an imperative language. I'll describe the way I would approach the problem. Notice that I do a lot of my "thinking" in GHCi!

Your code reads the file and splits it up into lines just fine, so I'll move onto the part where we handle each line of text. First, we need to split the line up into tokens, where the tokens are separated by commas. The words function is close to what we want, but it breaks at whitespace, not commas. So we'll write our own:

tokens :: String -> [String]
tokens [] = []
tokens as = 
  case break (==',') as of
    (xs,[])   -> [xs]
    (xs,_:ys) -> xs:tokens ys

I put that in the program I'm writing, load it, and try it out:

λ> tokens "wombat,7,789,tiger,33"
["wombat","7","789","tiger","33"]

Now we need to separate the words from the integers. So we need a function that will tell us if a token is an integer. The problem statement implies we only need to deal with integers. So a token is an integer if it only contains digits. (The example given didn't have any spaces, but we could allow them, and even decimal points, if we want to.)

So how do we tell if a character is a digit? Hmm... there's probably a function to do that for us.The best way to answer questions of the form "Is there a Haskell function to do X" is usually:

  1. Figure out what the type signature of the function you want would be.

  2. Search hoogle or hayoo. If you don't find it in one, try the other. Usually it doesn't matter much whether you get the order of the input parameters exactly right, but you may need to experiment.

What would the type of the function we're interested in be? Logically, it has to be:

Char -> Bool

Unfortunately, in the case there are a lot of functions with that signature, but eventually we find isDigit in Data.Char.

λ> import Data.Char
λ> isDigit '7'
True
λ> isDigit 'w'
False

Look at the source for that function. (You'll see that it would be easy to write our own function to allow blanks or decimal points as well as digits.)

So now we have a way to tell if an individual character is valid in an integer. How do we tell if all of the characters in a token pass the test? Again, I suspect there's a function that applies a boolean test to all elements in a sequence, and tells us if they all pass. The type signature of such a function would be one of the following:

(a -> Bool) -> [a] -> Bool
[a] -> (a -> Bool) -> Bool

Checking Hoogle, we find the all function in Data.List.

λ> import Data.List
λ> all isDigit "123"
True
λ> all isDigit "wombat"
False

We could use this expression as is, but would be more readable to define a function.

isInteger :: String -> Bool
isInteger s = all isDigit s

We could also write that last line using pointfree notation.

isInteger = all isDigit

I add that to my program, reload it, and try it out:

λ> isInteger "123"
True
λ> isInteger "wombat"
False

Next we need to divide our tokens up into those that are integers, and those that aren't. Again, there's probably a function that segregates a list into those items that satisfy a test, and those that don't. It would have this signature (possibly with the arguments switched):

(a -> Bool) -> [a] -> ([a],[a])

This time a Hayoo or Hoogle search turns up break and partition. Looking at the documentation, we see that partition is what we want. Let's try it:

λ> partition isInteger ["wombat","7","789","tiger","33"]
(["7","789","33"],["wombat","tiger"])

That preserved the order of the elements, but it gave us the integers first. That's easily fixed:

λ> partition (not . isInteger) ["wombat","7","789","tiger","33"]
(["wombat","tiger"],["7","789","33"])

Let's wrap that in a function to make it more readable:

segregate :: [String] -> ([String], [String])
segregate = partition (not . isInteger)

I'm going to move a little more quickly now, because you're probably getting the hang of how to approach these problems. Next we need to format the results. Let's focus on re-inserting the comma between tokens. I'll write a dual to the tokens function:

untokens :: [String] -> String
untokens = concat . intersperse ","

Let's try it:

λ> untokens ["wombat","tiger"]
"wombat,tiger"

Now all we need to combine the two parts:

format :: ([String],[String]) -> String
format (xs,ys) = untokens xs ++ '|' : untokens ys

Let's try it out:

λ> format (["wombat","tiger"],["7","789","33"])
"wombat,tiger|7,789,33"

Now we can put all the steps together:

parse :: String -> String
parse = format . segregate . tokens

See how simple and readable the parse function is? And it works:

λ> parse "wombat,7,789,tiger,33"
"wombat,tiger|7,789,33"

EDIT: Here's the entire program, with main.

import Data.Char
import Data.List

main = do
    args <- getArgs
    content <- readFile(args !! 0)
    mapM_ (putStrLn . lines) content

tokens :: String -> [String]
tokens [] = []
tokens as = 
  case break (==',') as of
    (xs,[])   -> [xs]
    (xs,_:ys) -> xs:tokens ys

untokens :: [String] -> String
untokens = concat . intersperse ","

segregate :: [String] -> ([String], [String])
segregate = partition (not . isInteger)

isInteger :: String -> Bool
isInteger = all isDigit

format :: ([String],[String]) -> String
format (xs,ys) = untokens xs ++ '|' : untokens ys

parse :: String -> String
parse = format . segregate . tokens
\$\endgroup\$
  • \$\begingroup\$ First, thank you for such a good answer, and for taking your time doing this. I can definitely see what you said when saying I approached it the imperative way. I didn't rely enough and hoogle and type signatures of function to create my solution. Thanks for pointing this out. I am kinda amazed by how short, readable is your solution. I will look more into it and try to do other problem following this pattern and hopefully improve my skills and my mindset using FP. \$\endgroup\$ – Jeremy D Nov 22 '13 at 19:59
  • \$\begingroup\$ There is an error though. format wont work if ys is empty (it will add a '|' char even if it shouldn't. Adding patterns will fix it \$\endgroup\$ – Jeremy D Nov 22 '13 at 20:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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