HiveBrain v1.2.0
Get Started
← Back to all entries
patternMinor

Parsing n lines to count vowels - HackerEarth

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
vowelsparsingcounthackerearthlines

Problem

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  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!

Solution

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:

  • If n is 0, do nothing



  • If n is greater than zero, do



  • read a line



  • count the vowels of the line



  • print the number of vowels



  • call processLine with n decreased by one



Or we can interpret it as

  • repeat the following action n times:



  • read a line



  • count the vowels of the line



  • print the number of vowels



  • 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 >= print . countVowels

countVowels :: String -> Int
countVowels xs = length $ filter (`elem` "aeuioAEUIO") xs

Code Snippets

processLine 0 = return 0
main :: IO ()
main = …

processLine :: Int -> IO ()
processLine n = …
countVowels :: String -> Int
countVowels line = length $ filter (`elem` "aeiou") $ map toLower line
processAllLines :: Int -> IO ()
processAllLines n = replicateM_ n processSingleLine

processSingleLine :: IO () 
processSingleLine = do
    line <- getLine
    print $ countVowels line
processList :: [String] -> [Int]

Context

StackExchange Code Review Q#160369, answer score: 2

Revisions (0)

No revisions yet.