如何在haskell中从类型重构公共逻辑

1wnzp6jl  于 2022-11-14  发布在  其他
关注(0)|答案(3)|浏览(139)

我有一堆保存时间序列数据的类型。比如,一个带有日期的日记帐列表,一个带有交易列表的银行对账单。我想创建一个class来注解这些类型slice-able,就像Python切片[:]一样
这会给所有类型实现TsList带来一场噩梦,因为:有很多filter ... txns是共同的。有没有办法把这个共同的逻辑分解出来呢?

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
3b6akqbq

3b6akqbq1#

一种选择是在普通的旧列表上实现这些函数,然后类示例就是新类型的 Package 器/非 Package 器。
例如,您可以定义:

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

然后您的示例就是:

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

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

一个潜在的麻烦是,如果你在TsList中有一堆类方法(或者一堆示例),而你甚至不想用新类型 Package /展开的相同方式定义每一个方法。你可以利用DefaultSignaturesCoercible类型类来编写:

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

因为StatementJournal只是[Date]周围的新类型 Package 器,它们将能够使用subByRange的默认版本,所以您所需要做的就是声明它们是示例。

rdrgkggo

rdrgkggo2#

正如@FyodorSoikin在评论中提到的,最简单的方法可能是使用GHC语言扩展DerivingVia。要做到这一点,请在文件顶部添加以下声明:

{-# LANGUAGE DerivingVia #-}

现在,假设您的类型被定义为newtype s,如果您告诉GHC这样做,它将能够自动从另一个示例派生一个示例:

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这仅在StatementJournal都是相同类型的newtype Package 器时有效。)

polhcujo

polhcujo3#

如果您希望它能够处理更复杂的数据类型,而不是简单的[Date] Package 器,那么您将需要一些更高级的工具。幸运的是,GHC提供了一些工具。您可以使用Data.Data module来更广泛地处理这些工具。
首先,我们需要一些工具。许多库导出了与此函数等价的东西,但它足够简单,我将只包含它,而不是引入一个额外的依赖。

{-# Language ScopedTypeVariables, GADTs #-}

import Data.Data

-- Apply the function anywhere in the data type where it has the
-- correct type. Don't recurse further when the type matches.
deepMap :: forall a s. (Typeable a, Data s) => (a -> a) -> s -> s
deepMap f = go
  where
    go :: forall x. Data x => x -> x
    go d = case eqT :: Maybe (a :~: x) of
        Just Refl -> f d
        Nothing -> gmapT go d

(It在边缘情况下,可以大大提高该函数的性能。如果你关心这一点,你应该使用来自库的版本。)
有了这项功能,您就可以利用DefaultSignatures扩充功能,在类别中提供适用于任何Data执行严修的预设实作。

{-# Language DefaultSignatures, DeriveDataTypeable #-}

-- whatever, this needed some definition
data Date = Date deriving (Eq, Ord, Show, Data)

data BoundDate = Include Date
               | Exclude Date
    deriving (Eq, Ord, Show, Data)

-- factor out the common logic
filterSubRange :: Maybe BoundDate -> Maybe BoundDate -> [Date] -> [Date]
filterSubRange Nothing Nothing txns = txns
filterSubRange  Nothing (Just ed) txns =
    case ed of
       Include d -> filter (<= d) txns
       Exclude d -> filter (< d) txns
filterSubRange (Just sd) Nothing txns =
    case sd of
       Include d -> filter (>= d) txns
       Exclude d -> filter (> d) txns
filterSubRange (Just sd) (Just ed) txns =
    filterSubRange Nothing (Just ed) txns'
  where
    txns' = filterSubRange (Just sd) Nothing txns

-- provide a default implementation that uses filterSubRange on any
-- [Date] the type contains
class TsList a where
    subByRange :: a -> Maybe BoundDate -> Maybe BoundDate -> a

    default subByRange :: Data a => a -> Maybe BoundDate -> Maybe BoundDate -> a
    subByRange s lo hi = deepMap (filterSubRange lo hi) s

-- some definitions containing whatever.
data Statement = Statement (Maybe String) Bool [Date]
    deriving (Eq, Ord, Show, Data)
newtype Journal = Journal [Date]
    deriving (Eq, Ord, Show, Data)

-- empty instances use the default definition
instance TsList Statement
instance TsList Journal

Statement的定义中可以看出,它适用于更复杂的数据类型。如果一个类型中有多个[Date]值,它可能无法完成您想要的操作--在这种情况下,您可以手动写出示例。而且,您可能可以利用filterSubRange在该过程中是一个独立函数这一事实。

相关问题