3
\$\begingroup\$

I have written a haskell program for the following 'Code Monk' Challenge at HackerEarth. Here is the challenge description. Basically, we are looking for the number of vowels in a string. The first input n is the number of string to parse, followed by n number of random strings.

And here is my implementation:

import Data.Char
main = do
    numberOfTestCases <- getLine
    processLine (read numberOfTestCases :: Int)

processLine 0 = return()
processLine n = do
    line <- getLine
    print $ countVowels line
    processLine (n-1)

countVowels :: String -> Int
countVowels line = length $ filter (`elem` ['a','e','i','o','u']) $ map toLower line

My code is working correctly but as I'm fairly new to functional programming and Haskell I would like some comments about my code. More specifically, I would like the processLine function to not have a side effect, but I don't know how can I do that. Any other tips or comments would be greatly appreciated!

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

Type signatures

You should add type signatures to all top-level bindings. processLine's returned value and main should have the same type, which can be checked with the type checker, if you add type signatures. The following code would compile, since main will have the type IO Int:

processLine 0 = return 0

But that's not what you've intended. Therefore, make sure to add type-signatures to all your top-level bindings:

main :: IO ()
main = …

processLine :: Int -> IO ()
processLine n = …

By the way, processLine's type signature makes n's type unambigous, and therefore you don't need an explicit type in (read numberOfTestCases :: Int) anymore.

Use syntactic sugar

['a','e','i','o','u'] is "aeiou", which is easier to change and to read (in my opionion). That way, we get:

countVowels :: String -> Int
countVowels line = length $ filter (`elem` "aeiou") $ map toLower line

By the way, if you use "aeiouAEIOU", you don't need to import Data.Char.

Keeping things pure(?)

More specifically, I would like the processLine function to not have a side effect, but I don't know how can I do that.

At some point, you need to read the line. You've chosen processLine to do so. Note that it is not feasible to remove IO from processLine, because you would end up with processLine = countVowels.

Still, we can have a look at processLine to see what we can do. We can read the code literally:

  1. If n is 0, do nothing
  2. If n is greater than zero, do
    1. read a line
    2. count the vowels of the line
    3. print the number of vowels
    4. call processLine with n decreased by one

Or we can interpret it as

  1. repeat the following action n times:
    1. read a line
    2. count the vowels of the line
    3. print the number of vowels
  2. return nothing

If we have a look at the modules in base, we'll notice that Control.Monad contains replicateM_, which does exactly the "repeat-n-times"-part:

processAllLines :: Int -> IO ()
processAllLines n = replicateM_ n processSingleLine

processSingleLine :: IO () 
processSingleLine = do
    line <- getLine
    print $ countVowels line

Note that processSingleLine can get shortened into getLine >>= print . countVowels, but I'm not sure whether you're ready for that yet.

Keeping things actually pure

Alright, we've seen how one could use standard functions to process all those lines. But it's still in IO. Can't we do anything?

Well, what would the alternative be? We still need to process a bunch of lines. A "bunch of something" can be easily expressed with a list, so let us write the type signature of our potential candidate.

processList :: [String] -> [Int]

The implementation comes rather easy:

processList = map countVowels

Now we just need to get a list of strings. Since we now want to get something from the user, we have to use IO:

getUserLines :: Int -> IO [String]

Seems familiar, doesn't it? "repeat-an-action-n-times". This time, it's getLine. Since we don't want to throw the result away, we use replicateM (note the missing _):

getUserLines n = replicateM n getLine

We're now missing two last ingredients. First, we need to print a list of Int so that the challenged site accepts it:

printSolutions :: [Int] -> IO ()

Exercise: write printSolution. Note that there is a function that might help you in the process.

We end up with the following main:

main :: IO ()
main = do
  numberOfTestCases <- readLn
  userLines         <- getUserLines numberOfTestCases
  printSolutions (processList userLines)

Further remarks

Search some of the base modules whether they provide the functions you're looking for. And have a look at Prelude. You've used read after getLine, which is provided as a single function readLn (see above).

For completion, here's how I would (probably) solve the challenge:

import Control.Monad (replicateM_)

main :: IO ()
main = do
  numberOfTest <- readLn
  replicateM_ numberOfTest $ getLine >>= print . countVowels

countVowels :: String -> Int
countVowels xs = length $ filter (`elem` "aeuioAEUIO") xs
\$\endgroup\$
  • \$\begingroup\$ I just wanted to thank you, I won't have the time to check your answer until this evening, but it's seems that you have gone above and beyond to help me. Thanks a lot! \$\endgroup\$ – Jonathan Chouraki Apr 11 '17 at 15:58
  • \$\begingroup\$ I learned a lot with your answer. Like, as much as a book really. It's great! Thanks! \$\endgroup\$ – Jonathan Chouraki Apr 11 '17 at 23:54

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.