Alejandro De Cicco
Alejandro De Cicco

Reputation: 1236

What is a clean way of writing this function?

I would like some help with a problem I'm trying to solve. Let's say I have a type called Thing:

data Thing = ....

And I want to write a function that, given a string, tries to match it with some stuff in my state and return a Thing:

findFirstMatch :: String -> State (Maybe Thing)
    

The thing is, to match that string, it needs a list of possible strings to match it with. That list is provided by a function defined for my state:

getPossibilities :: State String

Now, I need to call a third function that receives the original string and one of the possibilities, and returns a Maybe Thing:

tryToMatch :: String -> String -> State (Maybe Thing)

How can I write findFirstMatch? I thought of doing this but it doesn't seem that clean, and it feels like there might be something already implemented:

findFirstMatch :: String -> State (Maybe Thing)
findFirstMatch str = do
    xs <- getPossibilities
    firstNotNull (map (tryToMatch str) xs)

firstNotNull :: [State (Maybe Thing)] -> State (Maybe Thing)
firstNotNull [] = return Nothing
firstNotNull (x:xs) = do
    r <- x
    case r of
        Just _ -> return r
        Nothing -> firstNotNull xs

Upvotes: 0

Views: 176

Answers (1)

DDub
DDub

Reputation: 3924

First off, you can clean this up quite a bit if you write firstNotNull without using State. A very simple pass would be:

firstNotNull :: [Maybe Thing] -> Maybe Thing
firstNotNull [] = Nothing
firstNotNull (Just x:_) = Just x
firstNotNull Nothing:xs = firstNotNull xs

Furthermore, you can simplify even further by using some functions from Data.Maybe:

import Data.Maybe (catMaybes, listToMaybe)

firstNotNull :: [Maybe a] -> Maybe a
firstNotNull = listToMaybe . catMaybes

Now, let's turn our attention to findFirstMatch to see how we can use this simplified version of firstNotNull. The first question is: Does tryToMatch really need to live in State? After all, it already has access to both Strings that it's matching on. If you can change its type to tryToMatch :: String -> String -> Maybe Thing, then you're basically good to go.

On the other hand, if tryToMatch really does need to live in State, then there's just a little bit more to do: we need to pass firstNotNull a [Maybe Thing], but we have a [State (Maybe Thing)]. We can fix this by using sequenceA, as in:

findFirstMatch :: String -> State (Maybe Thing)
findFirstMatch str = do
    xs <- getPossibilities
    fmap firstNotNull $ sequenceA (map (tryToMatch str) xs)

Note that this only works if your State monad is lazy enough. If it's too strict, it will end up finding all matches, doing far too much work (and screwing up performance) and then return the first one.

From here, we can recognize that the usage of sequenceA and map can be reduced to a single call to traverse, as in:

    fmap firstNotNull $ traverse (tryToMatch str) xs

This seems much cleaner!


Of course, we can still go further if we really want. It's not clear that the below changes actually make the code cleaner (rather, there's a strong argument that they make it harder to read), but let's have some fun anyway.

Rather than use do, we can choose to make this a one-liner with an appropriate use of monadic bind:

findFirstMatch str = getPossibilities $ \xs -> (fmap firstNotNull $ sequenceA (map (tryToMatch str) xs))

The inner lambda can be nicely eta-reduced to:

findFirstMatch str = getPossibilities >>= fmap firstNotNull . sequenceA . map (tryToMatch str)

And this too can be eta-reduced:

findFirstMatch = (getPossibilities >>=) . ((fmap firstNotNull . sequenceA) .) . map . tryToMatch

And while we're at it, why even have a definition of firstNotNull when we can inline it!

findFirstMatch :: String -> State (Maybe Thing)
findFirstMatch = (getPossibilities >>=) . ((fmap (listToMaybe . catMaybes) . sequenceA) .) . map . tryToMatch

There, your whole function in one messy line!

Upvotes: 2

Related Questions