CarloDePieri
CarloDePieri

Reputation: 65

Select head of a list in Haskell

Now, I'm still learning haskell (to configure my xmonad wm), so bear with me.

I've written this function:

doesNameBeginWith :: String -> Query Bool
doesNameBeginWith str = fmap ( str `isPrefixOf`) (stringProperty "WM_NAME")  

Which will check the start of the WM_NAME X property against my str. I can compose it like this:

isMusic = doesNameBeginWith "MPD:" <||> doesNameBeginWith "ncmpcpp"

It works. Now I want to write a functions with this signature:

[String] -> Query Bool

so that I can call it like this:

isMusic = doesNameBeginWith ["MPD:", "ncmpcpp"]

The idea was to use foldr1 to parse the string list, but I can't figure out the sintax to get the head of the list... something like this:

doesNameBeginWith :: [String] -> Query Bool
doesNameBeginWith str = foldr1 (<||>) . fmap ( (head str) `isPrefixOf`) (stringProperty "WM_NAME") 

EDIT 1: as leftaroundabout suggested, I can combine two functions to get what I want:

doesNameBeginWithL :: [String] -> Query Bool
doesNameBeginWithL = foldr1 (<||>) . map doesNameBeginWith

But I'm still hoping for a more direct way and to understand how list heads could be used in this situation!

EDIT 2: Thank you all again for your answers, all of them have been quite informative! :)

Upvotes: 0

Views: 2210

Answers (3)

leftaroundabout
leftaroundabout

Reputation: 120751

If you already have a list, you don't need to parse it – you can simply deconstruct it.

isThisStuff :: [String] -> Query Bool
isThisStuff [c,d] = doesNameBeginWith c <||> doesNameBeginWith d

Then

isMusic = isThisStuff ["MPD:", "ncmpcpp"]

Note that this fails if the supplied list doesn't have exactly two elements.

Arguably, a fold-based solution is better. As you've noted, this can be written

foldr1 (<||>) . map doesNameBeginWith

Now, all you need to do to get rid of doesNameBeginWith in there is to inline it. Note that

doesNameBeginWith = \str -> fmap ( str `isPrefixOf`) (stringProperty "WM_NAME")

So you can use

foldr1 (<||>) . map (\str -> fmap ( str `isPrefixOf`) (stringProperty "WM_NAME"))

If you don't like the lambda, you can formulate this as a list comprehension:

doesNameBeginWithAny strs = foldr1 (<||>)
     [fmap ( str `isPrefixOf`) (stringProperty "WM_NAME") | str <- strs]

Alternatively, you can make doesNameBeginWith point-free

doesNameBeginWith = \str -> fmap (isPrefixOf str) (stringProperty "WM_NAME")            
                  = \str -> flip fmap (stringProperty "WM_NAME") (isPrefixOf str)
                  = \str -> flip fmap (stringProperty "WM_NAME") . isPrefixOf $ str
                  = flip fmap (stringProperty "WM_NAME") . isPrefixOf
                  = (stringProperty "WM_NAME" `fmap`) . isPrefixOf

so

doesNameBeginWithAny = foldr1 (<||>)
                     . map ((stringProperty "WM_NAME" `fmap`) . isPrefixOf)

However

all of this is overly complicated. It gets much simpler when you split up the problem:

  1. Retrieve stringProperty "WM_NAME" in the Query monad.
  2. Match any of the supplied strings with it.

    doesNameBeginWithAny :: String -> Query Bool
    doesNameBeginWithAny prefs = do
       wmName <- stringProperty "WM_NAME"
       return $ any (`isPrefixOf`wmName) prefs
    

This can also written with fmap, as Freerich Raabe showed.

Upvotes: 1

Frerich Raabe
Frerich Raabe

Reputation: 94499

You just need to change the function you're mapping over the the string property a bit; the any function comes in handy. Instead of

doesNameBeginWith str = fmap ( str `isPrefixOf`) (stringProperty "WM_NAME")  

try

doesNameBeginWith xs  = fmap (\x -> any (`isPrefixOf` x) xs) (stringProperty "WM_NAME") 

Upvotes: 2

Dan Hulme
Dan Hulme

Reputation: 15290

As you realised, head is how you can get the first element of the list, but the solution I'd teach to newbies is to use pattern-matching and recursion to access list elements. Using your first definition of doesNameBeginWith:

doesNameBeginAny :: [String] -> Query Bool
doesNameBeginAny (x:xs) = doesNameBeginWith x <||> doesNameBeginAny xs
doesNameBeginAny [] = return False

After the type declaration, there are two cases. One case takes a list with at least one item (x), and calls your first function on that one item, combining the result with a recursive call on the rest of the list (xs, which may be empty). The pattern matching is responsible for taking apart the list to get at the first item each time, so you don't need to call head. The second case handles the empty list.

As you've already found, the function foldr1 (as well as the other folds) abstracts away this structure of the function. You can use a map to transform the list into a list of Query Bool, and then a fold to reduce the list to a single item by or'ing the elements together.

doesNameBeginAny xs = foldr (<||>) False queries
    where queries = map doesNameBeginWith xs

I've used where to define an intermediate to make the code easier to follow. I've also used foldr rather than foldr1 so the function also works on empty lists. Again, you don't need to call head because map and fold are responsible for taking apart the list.

If you're looking for an answer that pulls apart the list explicitly, more like a for-each loop in Python or Java or some other imperative language, that's just not how you write Haskell.

Upvotes: 1

Related Questions