I recently did a practice problem for validating a string.
Use the
Maybe
type to write a function that counts the number of vowels in a string and the number of consonants. If the number of vowels exceeds the number of consonants, the function returns Nothing.
I ended up two functions for each counting vowels and counting consonants:
isVowel :: Char -> Bool
isVowel c = c `elem` "aeiou"
countVowels :: String -> Integer
countVowels [] = 0
countVowels (x:xs) =
if isVowel x
then countVowels xs + 1
else countVowels xs
countConsonants :: String -> Integer
countConsonants [] = 0
countConsonants (x:xs) =
if not $ isVowel x
then countConsonants xs + 1
else countConsonants xs
And then just comparing the values of the two to get my answer.
makeWord :: String -> Maybe String
makeWord [] = Nothing
makeWord s =
if countVowels s < countConsonants s
then Nothing
else Just s
My problem with this is that it traverses the string twice, once to get the number of vowels and another time to get the number of consonants.
I thought perhaps I could fix this by subtracting the number of vowels from the length of the string to begin with, but that also takes two traversals.
So I tried making a function that tracks both vowels and consonants at once, storing the results in a tuple, but since I didn't know adding tuples is actually pretty hard, the result was even more complex.
countVowelsAndConsonants :: String -> [(Integer, Integer)]
countVowelsAndConsonants [] = []
countVowelsAndConsonants (x:xs) =
if isVowel x
then (1, 0) : countVowelsAndConsonants xs
else (0, 1) : countVowelsAndConsonants xs
makeWord :: String -> Maybe String
makeWord [] = Nothing
makeWord s =
if countVowels s < countConsonants s
then Nothing
else Just s
where counts = let unzipped = unzip (countVowelsAndConsonants s)
in (sum $ fst unzipped, sum $ snd unzipped)
and, to be honest, I think this is even worse than what I started off with.
Also, what if I also had to keep track of uppercase and lowercase letters? Then I don't think the tuple approach would scale.
In an imperative language, for example javascript, which I'm more used to, it would be as simple as this to traverse it just once.
const word = "somestring"
let numVowels = 0
let numConsonants = 0
for (var s of word) isVowel(s) ? numVowels++ : numConsonants++
I'm sure Haskell has a way that's just as simple, but unfortunately I'm unfamiliar.
What's the idiomatic way of keeping tabs of multiple properties of a String
without having to keep traversing it multiple times?
Using foldr
with a state is somewhat simple:
countVCs :: String -> (Int, Int)
countVCs str = foldr k (0, 0) str
where
k x (v, c) = if isVowel x then (v + 1, c ) else (v , c + 1 )
An alternative way would be Data.List.partition
followed by length
on both elements of the pair:
countVCs :: String -> (Int, Int)
countVCs str = both length (partition isVowel str)
where
both f (x,y) = (f x, f y)
Yet another way is to use foldMap
with (Sum Int, Sum Int)
:
countVCs :: String -> (Int, Int)
countVCs str = both getSum (foldMap k str)
where
k x = if isVowel x then (Sum 1, Sum 0) else (Sum 0, Sum 1)
both f (x,y) = (f x, f y)