Reputation: 1236
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
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 String
s 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