Home > Back-end >  How to refactor common logic from types in haskell
How to refactor common logic from types in haskell

Time:09-23

I have a bunch of type which holds time series data. Like, a list of journal with dates ,a bank statement with list of transactions. I want to make a class to annotate these type slice-able like in Python slice [:]

that would create a nightmare to implement TsList to all the types because: there are lots of filter ... txns in common . Is there any way to factor out this common logic ?

data BoundDate = Include Date
               | Exclude Date

class TsList a where
  subByRange :: a -> Maybe BoundDate -> Maybe BoundDate -> a

--- with Statement type 
instance TsList Statement where
  subByRange s@(Statement txns) Nothing Nothing = s
  subByRange s@(Statement txns) Nothing (Just ed) = 
    case ed of 
       Include d -> Statement $ filter ( x -> x <= d ) txns
       Exclude d -> Statement $ filter ( x -> x < d ) txns
  subByRange s@(Statement txns) (Just sd) Nothing =
    case sd of 
       Include d -> Statement $ filter ( x -> x => d ) txns
       Exclude d -> Statement $ filter ( x -> x > d ) txns
  subByRange s@(Statement txns) (Just sd) (Just ed) =
       Statement $ subByRange _s Nothing (Just ed)
    where
       _s = Statement $ subByRange s (Just sd) Nothing

--- now with Journal type 
instance TsList Journal where
  subByRange s@(Journal txns) Nothing Nothing = s
  subByRange s@(Journal txns) Nothing (Just ed) = 
    case ed of 
       Include d -> Journal $ filter ( x -> x <= d ) txns
       Exclude d -> Journal $ filter ( x -> x < d ) txns
  subByRange s@(Statement txns) (Just sd) Nothing =
    case sd of 
       Include d -> Journal $ filter ( x -> x => d ) txns
       Exclude d -> Journal $ filter ( x -> x > d ) txns
  subByRange s@(Statement txns) (Just sd) (Just ed) =
       Journal $ subByRange _s Nothing (Just ed)
    where
       _s = Journal $ subByRange s (Just sd) Nothing

CodePudding user response:

As @FyodorSoikin mentioned in the comments, perhaps the easiest way to do this is to use the GHC language extension DerivingVia. To do this, add the following declaration to the top of your file:

{-# LANGUAGE DerivingVia #-}

Now, assuming that your types are defined as newtypes, GHC will be able to automatically derive one instance from the other if you tell it so:

newtype Statement = Statement [Int]   -- or something like this

instance TsList Statement where
    -- ...as in the question...

newtype Journal = Journal [Int]
    deriving (TsList) via Statement

(Note that this only works if both Statement and Journal are newtype wrappers around the same type.)

CodePudding user response:

One option is to implement these functions over just plain old lists and then the class instances are just newtype wrapper/unwrappers.

For instance, you can define:

subByRangeList :: [Date] -> Maybe BoundDate -> Maybe BoundDate -> [Date]
subByRangeList s Nothing Nothing = s
subByRangeList s Nothing (Just ed) = 
  case ed of 
     Include d -> filter ( x -> x <= d ) s
     Exclude d -> filter ( x -> x < d ) s
subByRangeList s (Just sd) Nothing =
  case sd of 
     Include d -> filter ( x -> x >= d ) s
     Exclude d -> filter ( x -> x > d ) s
subByRangeList s (Just sd) (Just ed) =
     subByRangeList _s Nothing (Just ed)
  where
     _s = subByRangeList s (Just sd) Nothing

And then your instances are simply:

instance TsList Statement where
  subByRange (Statement txns) = Statement $ subByRangeList txns

instance TsList Journal where
  subByRange (Journal txns) = Journal $ subByRangeList txns

One potential annoyance of this is if you have a bunch of class methods in TsList (or a bunch of instances), and you don't want to even have to define each one the same way with the newtype wrapping/unwrapping. You can make use of DefaultSignatures and the Coercible type class to write:

class TsList a where
  subByRange :: a -> Maybe BoundDate -> Maybe BoundDate -> a
  default subByRange :: (Coercible a [Date], Coercible [Date] a) => a -> Maybe BoundDate -> Maybe BoundDate -> a
  subByRange s x y = coerce $ subByRangeList (coerce s) x y

instance TsList Statement
instance TsList Journal

Because Statement and Journal are simply newtype wrappers around [Date], they will be able to use the default version of subByRange, so all you need to do is declare that they're instances.

  • Related