Suppose you have a data structure with multiple value constructors, for example a LogMessage
data structure like so:
data LogMessage = Unknown String
| LogMessage MessageType TimeStamp String
If the message can be parsed properly, it has some extra data and then a String
. If it can't be parsed, then it's just a catch-all Unknown String
.
Or suppose you are working with something like an Either String String
, so that you might be dealing with a Left String
or a Right String
.
Now let's say that you want to apply the same processing steps to the underlying data, regardless of which value constructor it resides in.
For example, I might want to detect a certain word within the LogMessage
strings, so I could have a function like this:
detectWord :: String -> LogMessage -> Bool
detectWord s (Unknown m) = isInfixOf s (map toLower m)
detectWord s (LogMessage _ _ m) = isInfixOf s (map toLower m)
or it could just as easily be written to handle Either String String
as input instead of LogMessage
.
In both cases, I have to repeat the exact same code (the isInfixOf ...
part), because I have to extract the underlying data it will operate on differently due to pattern matching on different value constructors.
It is bad that one must repeat / "copy-paste" the code for each different value constructor match.
How does one write these kinds of Haskell functions without copy/paste code? How can I write the underlying logic just once, but then explain how it should be used across many different value constructor patterns?
Simply moving it to a secondary helper function would reduce the character count, but doesn't really solve the issue. For example, the idea below is not substantively any better about "don't repeat yourself" than the first case:
helper :: String -> String -> Bool
helper s m = isInfixOf s (map toLower m)
detectWord :: String -> LogMessage -> Bool
detectWord s (Unknown m) = helper s m
detectWord s (LogMessage _ _ m) = helper s m
There again we have to say the same thing for every different pattern.
Write a function that gets the message in either case. Then, you won't need to write separate cases for uses that don't care:
getMsg (Unknown m) = m
getMsg (LogMessage _ _ m) = m
detectWord s log = infixOf s (map toLower (getMsg log))
Note that something is going to have to check the cases of your type, and getMsg
is about as minimal as it gets along those lines.
Try using view patterns.
{-# LANGUAGE ViewPatterns #-}
data LogMessage = Unknown String
| LogMessage MessageType TimeStamp String
stringOfLogMessage :: LogMessage -> String
stringOfLogMessage (Unknown s) = s
stringOfLogMessage (LogMessage _ _ s) = s
detectWord :: String -> LogMessage -> Bool
detectWord needle (stringOfLogMessage -> hay) =
needle `isInfixOf` map toLower hay
Use Generics and Generics.Deriving.Lens.
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE NoImplicitPrelude #-}
module Lib where
import BasePrelude
import Control.Lens
import Generics.Deriving.Lens
data LogMessage = Unknown String
| LogMessage () () String
deriving (Generic)
detectWord :: String -> LogMessage -> Bool
detectWord needle =
allOf tinplate (isInfixOf needle . map toLower)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With