I have been working through the week 2 homework exercises, and had a bit of a rocky start. I got it working without much trouble, but I had a real mess of code:
-- parses individual line from the log file
parseMessage :: String -> LogMessage
parseMessage line = case words line of
"I" : time : message -> case maybeRead time :: (Maybe Int) of
Just t -> LogMessage Info t (unwords message)
_ -> Unknown line
"W" : time : message -> case maybeRead time :: (Maybe Int) of
Just t -> LogMessage Warning t (unwords message)
_ -> Unknown line
"E" : severity : time : message -> case maybeRead severity :: (Maybe Int) of
Just s -> case maybeRead time :: (Maybe Int) of
Just t -> LogMessage (Error s) t (unwords message)
_ -> Unknown line
_ -> Unknown line
This is awful – so full of duplication and really not pleasant to read. I couldn’t leave it like that, so I set about at least separating out the code for determining the message type. I ended up with something at least moderately readable:
parseMessage line = let (messageType, rest) = maybeGetMessageTypeAndRemainder line in
case messageType of
Nothing -> Unknown line
Just aType -> case rest of
[] -> Unknown line
time : message -> case maybeRead time :: (Maybe Int) of
Just t -> LogMessage aType t (unwords message)
_ -> Unknown line
-- partition the message type and remainder of the string
maybeGetMessageTypeAndRemainder :: String -> (Maybe MessageType, [String])
maybeGetMessageTypeAndRemainder line = case words line of
"I" : rest -> (Just Info, rest)
"W" : rest -> (Just Warning, rest)
"E" : severity : rest -> case maybeRead severity :: (Maybe Int) of
Just s -> (Just (Error s), rest)
_ -> (Nothing, words line)
_ -> (Nothing, words line)
It still looked wrong though, with a number of cases mapping to the exact same code. It was at this point that my brain kicked into gear, and I had to laugh out loud when I realized that I was using the Maybe monad and completely ignoring one of the main uses of monads: that thing where you chain them together. I had forgotten what it was called (the back of my mind said ‘bind’, but it is a human mind so I did not trust it fully), but I knew what I wanted it to do, so I plugged this into Hoogle: m a -> (a -> m b) -> m b
, and it obligingly reminded me that I’m looking for (>>=) :: Monad m => m a -> (a -> m b) -> m b
. It was indeed ‘bind’.
After a short break to make some tea and let my brain process the problem a little with its rediscovered knowledge, I set about setting things right.
My approach would be to use Maybe tuples to pass through the parsed and unparsed sections of a log message. The tuples would increase in size until they were used at the end to construct a LogMessage… but wait! It occurred to me that I may even be able to get away with only using pairs, if I could partially apply the LogMessage constructor. I gave it a little try in ghci:
*Log> :t LogMessage
LogMessage :: MessageType -> TimeStamp -> String -> LogMessage
*Log> :t LogMessage Warning
LogMessage Warning :: TimeStamp -> String -> LogMessage
*Log> :t LogMessage Warning 5
LogMessage Warning 5 :: String -> LogMessage
Aha! So I would be able to build up a log message along the way.
Tried a bunch of stuff, got confused about bind (>>=), nutted it out and used (>=>) but had to add return
to the final appendMessage.
At this point I asked on #haskell-beginners on freenode, and got a bit of advice:
jle> so the thing is, if you have m a
jle> you can apply all sorts of functions to it, you just need the right lifter jle> if you have an (a -> b)
, you use fmap
jle> if you have an m (a -> b)
, you use ap
(or (<*>)
) jle> if you have an (a -> m b)
, you use (=<<)
, or (>>=)
jle> fmap :: (a -> b) -> m a -> m b
jle> (<*>) :: m (a -> b) -> m a -> m b
jle> (=<<) :: (a -> m b) -> m a -> m b
jle> so…there ya go
Then went on with a whole lot of information, too much to process for the moment. I was directed to watch the week 4 videos of a functional programming course, so I have added those to the list.
I persevered a bit more and ended up with something that seems about as concise as I can make it:
parseMessageType :: String -> Maybe (TimeStamp -> String -> LogMessage, [String])
parseMessageType line = case words line of
"I" : rest -> Just (LogMessage Info, rest)
"W" : rest -> Just (LogMessage Warning, rest)
"E" : rest -> parseErrorSeverity rest
_ -> Nothing
parseErrorSeverity :: [String] -> Maybe (TimeStamp -> String -> LogMessage, [String])
parseErrorSeverity (severity : rest) = case maybeRead severity :: (Maybe Int) of
Just s -> Just (LogMessage (Error s), rest)
_ -> Nothing
parseErrorSeverity _ = Nothing
parseTimeStamp :: (TimeStamp -> String -> LogMessage, [String]) -> Maybe (String -> LogMessage, [String])
parseTimeStamp (_, []) = Nothing
parseTimeStamp (logMessage, timeStamp : rest) = case maybeRead timeStamp :: (Maybe TimeStamp) of
Just t -> Just (logMessage t, rest)
_ -> Nothing
appendMessage :: (String -> LogMessage, [String]) -> LogMessage
appendMessage (logMessage, message) = logMessage . unwords $ message
parseValidMessage :: String -> Maybe LogMessage
parseValidMessage = fmap appendMessage . (parseMessageType >=> parseTimeStamp)
parseMessage :: String -> LogMessage
parseMessage = liftM2 fromMaybe Unknown parseValidMessage
parseLog :: String -> [LogMessage]
parseLog = map parseMessage . lines
I can probably simplify some of the earlier functions in wonderful ways of which I am not yet aware, but that will do for now. I can always look back at this later when I have learned more, and laugh at the verbosity of the above.