I was writing a function (named listenString
) in haskell to see if a string contains one of the strings in a list of strings, and to then return a value (of the type m ()
) that corresponded with the string it found. The type of that function is (Eq a, Monad m) => [a] -> [([a], m ())] -> m ()
.
The reason why I used a list to store the strings and their corresponding values ([([a], m ())]
) instead of a Map was because I wanted to make sure that the function evaluated strings in the order that they were in the list, e.g. listenString "hello there" [("lo",print True),("hello",print False)]
would printed True
. If I used a map, the order might have been lost, and the function might have printed False
.
Here's the code. I'm specifically wondering if there are any bugs, and if I'm using ListT
correctly (of if I should be using something other than ListT
), but any other feedback would be helpful and appreciated.
import Data.Either
import Control.Monad.Trans.List
searchListMapMaybe f (Search a b c) = Search a (mapMaybe f b) c
data Search a b = Search a [a] b
type Searching a b = ListT (Either b) (Search a b)
listenString [] _ = return ()
listenString a l =
let list = filter (\(x,_) -> if (x == []) then False else True) l
noEmptyList [] = error "listenForString: You need to have at least one item in the list"
noEmptyList x = x
in listenStringSub a $ ListT $ Data.Either.Right $ fmap (\(x,y) -> Search x [] y) $ noEmptyList list
where
listenStringSub :: (Eq a) => [a] -> Searching [a] (m b) -> m b
listenStringSub _ (ListT (Data.Either.Left a)) = a
listenStringSub (c:cs) x = listenStringSub cs $ (listenStringMap c x) >>= searchFinal
where
listenStringMap char x = fmap ((searchReduce char) . (searchPop char)) x
searchPop :: (Eq a) => a -> Search [a] b -> Search [a] b
searchPop char (Search a@(x:_) l b)
| char == x = Search a (a:l) b
| otherwise = Search a l b
searchReduce :: (Eq a) => a -> Search [a] b -> Search [a] b
searchReduce char search = searchListMapMaybe (function char) search
where
function _ [] = Just []
function c l@(x:xs)
| c == x = Just xs
| otherwise = Nothing
searchFinal :: (Eq a) => Search [a] b -> Searching [a] b
searchFinal s@(Search a b c)
| [] `elem` b = ListT (Data.Either.Left c)
| otherwise = return s
Edit: listenString
should decide what values to return in the following order:
- Which list comes first:
listenString "hello there" [("lo",print True),("he",print False)]
would outputFalse
. - If two or more strings end in the exact same place, e.g.
listenString "hello there" [("lo",print True),("hello",print False)]
, I want the tuple closest to the beginning of the list to be displayed (the above expression would outputTrue
.
1 Answer 1
Why do you need the element type to be a monad? The only point you require it is when you do return ()
in order to signal failure. I'd propose implementing a more general function
lookupSublist :: Eq a => [a] -> [([a], b)] -> Maybe b
Which could be implemented as:
lookupSublist xs = fmap snd . listToMaybe . filter (flip isInfixOf xs . fst)
So we first filter out all alternatives where the first element doesn't have xs
as infix. Then we return the second element of the first remaining element, if any.
Some random notes on your code:
Using
noEmptyList
as a guard mid-expression isn't a good idea. Usingerror
is pretty "evil" in the first place, you don't want to bury it like that. Putting thelist
in thewhere
would allow you to saylistenString a l | null list = error...
(\(x,_) -> if (x == []) then False else True)
is simplynot . null . fst
. I'm not quite sure why you filter for that, to be honest. Wouldn't having[]
in the list be a pretty elegant way to implement a default fallback value?I am actually quit struggling to understand the rest of your code. I am pretty sure that your usage of
ListT
actually hurts more than it helps - generally when you start pattern-matching on the contents of a monad you are doing something wrong. Could you explain what you tried to do?
Edit: If the position of the match end should take priority, I would propose going over the prefixes of the string and suffix-test the options:
lookupFirstSublist str opts = listToMaybe $ mapMaybe f $ inits opts
where f s = fmap snd $ listToMaybe $ filter (flip isSuffixOf s . fst) opts
-
\$\begingroup\$ I like this save one thing, I would instead of Maybe b have Maybe [a] -> b and use ap on it this way you could compose chains \$\endgroup\$Jimmy Hoffa– Jimmy Hoffa2013年05月13日 01:47:48 +00:00Commented May 13, 2013 at 1:47
-
\$\begingroup\$
lookupSublist xs = fmap (\x -> Just \y -> (snd x) y) . filter (isInFixOf xs . fst)
forlookupSublist :: Eq [a] => [a] -> [([a], [a] -> b)] -> [Maybe [a] -> b]
\$\endgroup\$Jimmy Hoffa– Jimmy Hoffa2013年05月13日 14:47:49 +00:00Commented May 13, 2013 at 14:47 -
\$\begingroup\$
lookupSublist
doesn't work at all: I'm assuming that you meantlistenList a xs = fmap snd $ listToMaybe (filter (\x -> isInfixOf (fst x) a) xs)
. Even then, your implementation oflookupSublist
has some problems:listenList "hello there" [("lo",True),("he",False)]
returnsTrue
(It should returnFalse
, because"he"
comes first in the"hello there"
). \$\endgroup\$user4253– user42532013年05月14日 23:31:24 +00:00Commented May 14, 2013 at 23:31 -
\$\begingroup\$ Thanks for your help though: I'll post an explanation of the code and I'll respond to your other points when I have time to edit it. \$\endgroup\$user4253– user42532013年05月14日 23:46:29 +00:00Commented May 14, 2013 at 23:46
-
1\$\begingroup\$ Nevermind, there was actually a
flip
missing. Sorry about that :) \$\endgroup\$Peter Wortmann– Peter Wortmann2013年05月15日 13:48:58 +00:00Commented May 15, 2013 at 13:48