Haskell - Capitalize all letters in a list [String] with toUpper

16,899

Solution 1

Mapping one level deeper

You need to use map (map toUpper).

This is because you have [String] instead of String.

toUpper           :: Char -> Char
map toUpper       :: [Char] -> [Char]

i.e.

map toUpper       :: String -> String
map (map toUpper) :: [String] -> [String]

map toUpper capitalises a String, by making each letter uppercase, so map (map toUpper) capitalises each String in a list of Strings.

Your function becomes

delAndUpper myList = map (map toUpper) (filter (\x -> not('p' `elem` x || 'q' `elem` x)) myList)

dave4420 made a good suggestion that (map.map) toUpper is a neat way of writing map (map toUpper) that helps you think two list levels in quite simply and naturally - have a look at his answer too.

Can we un-hardwire the p and q?

You asked if there was a shorter way to write the condition, and didn't like hard coding the `q` and `p`. I agree those multiple `elem` bits aren't pretty. Let's pass in the list of disallowed letters and tidy up a bit:

delAndUpper omit strings = map (map toUpper) (filter ok strings) where
            ok xs = not (any (`elem` omit) xs)

Here (`elem` omit) checks a character if it's in the list of ones that would cause us to omit the word, so (any (`elem` omit) xs) checks if any of the characters of xs are forbidden. Of course if none are forbidden, it's ok.

Your original delAndUpper would now be delAndUpper "pq", or if you also want to disallow capital P and Q, delAndUpper "pqPQ". (More on this later.)

Can we make it more concise?

Let's see if we can't write ok a little shorter. My first thought was to use pointfree on it (see my answer to another question for details of how to get it running in ghci), but it seemed to hang, so using some standard transformation tricks, we can compose not with a function that takes two arguments before giving us a Bool by doing (not.).f instead of not.f as we would with a function which just gave us a Bool after the first input. any is taking (`elem` omit) as its first argument. This gives us

ok xs = ((not.).any) (`elem` omit) xs

from which we can remove the trailing xs:

ok = ((not.).any) (`elem` omit) 

and inline:

delAndUpper omit strings = map (map toUpper) (filter (((not.).any) (`elem` omit)) strings)

I'm not keen on the trailing strings either:

delAndUpper omit = map (map toUpper).filter (((not.).any) (`elem` omit))

(We could get rid of the omit argument as well and go completely point free, but that would go quite a bit too far down the hard-to-read road for my taste.)

Whither Q?

> delAndUpper "pq" $ words "The Queen has probably never had a parking ticket."
["THE","QUEEN","HAS","NEVER","HAD","A","TICKET."]

Is this the required behaviour? It seems strange to carefully exclude the lowercase variants and then make everything uppercase. We could do it the other way round:

upperAndDel omit = filter (((not.).any) (`elem` omit)).map (map toUpper)

giving

> upperAndDel "PQ" $ words "The Queen has probably never had a parking ticket."
["THE","HAS","NEVER","HAD","A","TICKET."]

Solution 2

I know, that toUpper in this line of code gets a list as value and therefore it can't work, but know how to go a level down into the list and the apply map toUpper.

Use (map . map) instead of map.

n.b. (map . map) toUpper is the same as map (map toUpper) as suggested by the other answers. I mention it because, personally, I find it clearer: it looks more like it is going down two levels to apply toUpper. (You may not be familiar with the function composition operator (.), look it up or ask about it if you need to.)

Other functions with a similar type ((a -> b) -> something a -> something b), such as fmap, Data.Map.map, first and second, can be combined with each other and with map in a similar way.

Perhaps you don't find (map . map) toUpper clearer than map (map toUpper); fair enough.

Solution 3

You're almost there, you just need a second map.

map (map toUpper) (filter (\x -> not('p' `elem` x || 'q' `elem` x)) myList)

This is because String is completely synonymous with [Char] in vanilla haskell. Since the type of map is

(a->b) -> [a] -> b

and we have

toUpper :: Char -> Char
String  :: [Char]

We'll get back another String, except capitalized.

By the way, that ugly-ish filter can be replaced made prettier with by making it use arrows :) (Think of these like more structured functions)

map (map toUpper) . filter $ elem 'p' &&& elem 'q' >>> arr (not . uncurry (||))

Gratuitousness? Maybe, but kinda cool.

Share:
16,899
John
Author by

John

Updated on June 04, 2022

Comments

  • John
    John almost 2 years

    I have a list [String] the task ist to remove those elements in the list, which have "q" or "p" and then capitalize all letters in the list with toUpper.

    What I tried yet is as follow:

    delAndUpper :: [String] -> [String]
    delAndUpper myList = filter (\x -> not('p' `elem` x || 'q' `elem` x)) myList
    

    It removes the unwanted elements from the list properly, however I can't apply toUpper on this list since the type of toUpper is Char.

    I tried it with map and it does not work.

    delAndUpper myList = map toUpper (filter (\x -> not('p' `elem` x || 'q' `elem` x)) myList)
    

    I know, that toUpper in this line of code gets a list as value and therefore it can't work, but know how to go a level down into the list and the apply map toUpper.

    Could you please help me.

    Thanks in advance!

    Greetings

  • John
    John about 11 years
    Hello all, thank you very much indeed for so much help! I had tried every thing for about 1 hour. On of my solutions/attempts was map map but I sat the brace wrong like this: delAndUpper myList = map (map toUpper (filter (\x -> not('p' elem` x || 'q' elem x)) myList))`. and I thought it's the wrong path :-(
  • John
    John about 11 years
    is it possible to rewrite this conditional statement in a more compact manner like list or so ´'p' elem x || 'q' elem x´. e.g if there are not two characters as condition but three and four and so on, imho it would be better, if one could have them as a concatanation and pass it as a parameter to the function instead of hard coding it. Any idea? Thanks