Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Avoid repeating code when reusing for multiple value constructor pattern matches

Tags:

haskell

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.

like image 957
ely Avatar asked Feb 07 '16 17:02

ely


2 Answers

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.

like image 112
comingstorm Avatar answered Sep 28 '22 13:09

comingstorm


Simple, won't make people hate you

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

Complicated, might make people hate you

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)
like image 36
hao Avatar answered Sep 28 '22 12:09

hao