类子类结构的设计模式

Design pattern for subclass-like structure

我的目标是以优雅和高效的方式表示一组具有相似行为的类型。为实现这一目标,我创建了一个解决方案,该解决方案使用单一类型,然后是一组执行模式匹配的函数。

我的第一个问题是:有没有一种方法可以使用单一类型-class来表示相同的想法,而不是让每个变体都有一个构造函数来拥有一个实现所述类型-[=的类型38=]?

以下两种方法中哪一种是: - Haskell 中更受认可的设计模式? - 更高效的内存? - 性能更高? - 更优雅,为什么? - 代码的消费者更容易使用?

方法一:单一类型和模式匹配

假设有如下结构:

data Aggregate a
  = Average    <some necessary state keeping>
  | Variance   <some necessary state keeping>
  | Quantile a <some necessary state keeping>

它的构造函数不是 public,因为那样会暴露内部状态保持。相反,存在一组构造函数:

newAverage :: Floating a
  => Aggregate a
newAverage = Average ...

newVariance :: Floating a
  => Aggregate a
newVariance = Variance ...

newQuantile :: Floating a
  => a                     -- ! important, a parameter to the function
  -> Aggregate a
newQuantile p = Quantile p ...

创建对象后,我们可以执行两个功能:put 将值放入其中,一旦我们满意,我们就可以 get 当前值:

get :: Floating a
  => Aggregate a
  -> Maybe a
get (Average <state>) = getAverage <state>
get (Variance <state>) = getVariance <state>
get (Quantile _ <state>) = getQuantile <state>

put :: Floating a
  => a
  -> Aggregate a
  -> Aggregate a
put newVal (Average <state>) = putAverage newVal <state>
put newVal (Variance <state>) = putVariance newVal <state>
put newVal (Quantile p <state>) = putQuantile newVal p <state>

方法 2:类型-classes 和实例

class Aggregate a where
  new :: a
  get :: Floating f => a f -> Maybe f
  put :: Floating f => 

data Average a = Average Word64 a
data Variance a ...

instance Aggregate Average where

instance Aggregate Variance where

instance Aggregate Quantile where

这里明显的问题是 new 不是参数化的,因此 Quantile 不能用 p 参数初始化。向 new 添加参数是可能的,但这会导致所有其他非参数构造函数忽略该值,这不是一个好的设计。

很难给出一般性建议。我倾向于更喜欢方法 1。请注意,您可以使用

data Aggregate a
  = Average    AverageState
  | Variance   VarianceState
  | Quantile a QuantileState

并导出上面的每个构造函数,只保留模块私有的 ...State 类型。

这在某些情况下可能可行,但在其他情况下不可行,因此必须根据具体情况进行评估。

关于方法 2,如果周围有很多构造函数/类型,这可能会更方便。要解决 new 问题,可以使用

中的类型族(或 fundeps)
class Floating f => Aggregate a f where
  type AggregateNew a f
  new :: AggregateNew a f -> a f
  get :: a f -> Maybe f
  put :: ...

instance Floating f => Aggregate Average f where
  type AggregateNew (Average a) f = ()
  new () = ...

instance Floating f => Aggregate Quantile f where
  type AggregateNew (Quantile a) f = a
  new x = ...

上面的命名很糟糕,但我用它来说明问题。 new 接受一个类型为 AggregateNew k f 的参数,如果 new 不需要信息,它可以是 (),或者在需要时使用一些更多信息的类型,比如 a创建 Quantile.

您缺少 "codata" 编码,这听起来可能最适合您的问题。

data Aggregate a = Aggregate 
    { get :: Maybe a
    , put :: a -> Aggregate a
    }

-- Use the closure to keep track of local state.
newAverage :: (Floating a) => Aggregate a
newAverage = Aggregate { get = Nothing, put = go 0 0 }
    where
    go n total x = Aggregate { get = Just ((total + x) / (n+1))
                             , put = go (n+1) (total+x)
                             }

-- Parameters are not a problem.
newQuantile :: (Floating a) => a -> Aggregate a
newQuantile p = Aggregate { get = ... ; put = \x -> ... }

...

出于某种原因,这种方法总是被具有 OO 背景的人所忽视,这很奇怪,因为它与该范式非常接近。

有第三种定义“聚合器”的方法,它既不需要不可扩展的总和类型,也不需要多种数据类型 + 类型类。

方法 3:将状态置于存在背后的单构造函数数据类型

考虑这种类型:

{-# LANGUAGE ExistentialQuantification #-}
data Fold a b = forall x. Fold (x -> a -> x) x (x -> b)

它表示一个聚合器,它吸收类型 a 的值并最终吸收 "returns" 类型 b 的值,同时携带内部状态 x.

构造函数的类型为 (x -> a -> x) -> x -> (x -> b) -> Fold a b。它需要一个阶跃函数、一个初始状态和一个最终的 "summary" 函数。请注意,状态与 return 值 b 解耦。它们可以相同,但不是必需的。

另外,状态是existentially quantified。当我们创建 Fold 时,我们知道状态的类型,当我们在 Fold 上进行模式匹配时,我们可以使用它——以便通过 step 函数向它提供数据——但它不是反映在 Fold 的类型中。我们可以将具有不同内部状态的 Fold 值毫无问题地放入同一个容器中,只要它们摄取和 return 相同的类型即可。

这种图案有时被称为“美丽的褶皱”。有一个名为 foldl 的库基于它,并提供了可能的预制折叠和实用函数。

Fold foldl 中的类型有很多有用的实例。特别是,Applicative 实例让我们可以创建仍然遍历输入数据一次的复合折叠,而不需要多次遍历。