记忆化和类型类

5
为了简单起见,我将使用以下虚构示例类(重点是我们有一些从方法派生出的昂贵数据):

class HasNumber a where
  getNumber :: a -> Integer
  getFactors :: a -> [Integer]
  getFactors a = factor . getNumber

当然,我们可以创建这个类的记忆化实现,例如:
data Foo = Foo {
  fooName :: String,
  fooNumber :: Integer,
  fooFactors :: [Integer]
}

foo :: String -> Integer -> Foo
foo a n = Foo a n (factor n) 

instance HasNumber Foo where
    getNumber = fooNumber
    getFactors = fooFactors

但是,手动添加一个“因素”字段到每个将成为HasNumber实例的记录中似乎有点丑陋。下一个想法:
data WithFactorMemo a = WithFactorMemo {
    unWfm :: a,
    wfmFactors :: [Integer]
}

withFactorMemo :: HasNumber a => a -> WithFactorMemo a
withFactorMemo a = WithFactorMemo a (getFactors a)

instance HasNumber a => HasNumber (WithFactorMemo a) where
    getNumber = getNumber . unWfm
    getFactors = wfmFactors

这将需要大量的样板代码,将原始的所有其他操作从a提升到WithFactorMemo a中。但是,有没有更优雅的解决方案呢?

我刚想到的另一个解决方案是将factor函数变成记忆化函数,但如果getNumber的结果是一些较大的数据结构,则这种方法会不太实用,并且(据我所知)这些条目永远不会被垃圾回收(与我的问题中的两个解决方案相反)。 - FunctorSalad
1个回答

7
这是解决方案:去掉类型类。我已经在这里这里谈论过这个问题。对于每个成员都以单个a作为参数的任何类型类TC a都是同构于一个数据类型。这意味着你HasNumber类的每个实例都可以表示为此数据类型:
data Number = Number {
    getNumber' :: Integer,
    getFactors' :: [Integer]
}

换句话说,通过这种转换:
toNumber :: (HasNumber a) => a -> Number
toNumber x = Number (getNumber x) (getFactors x)

而且,Number 显然也是 HasNumber 的一个实例。
instance HasNumber Number where
    getNumber = getNumber'
    getFactors = getFactors'

这种同构关系告诉我们,这个类实际上是一个数据类型的伪装,它应该被淘汰。只需要使用Number即可替代。起初可能不太明显如何做到这一点,但有了一点经验就可以很快地做到。例如,您的Foo类型变成了:
data Foo = Foo {
    fooName :: String,
    fooNumber :: Number
}

您的记忆化将免费进行,因为因子存储在Number数据结构中。


实际上,在你发帖之前,这也是我决定尝试的 :) 我同意将操作放入单个类型(Number在这里),但也许有一个class HasNumber a where numberDict :: a -> Number以及包装器getNumber = getNumber' . numberDict等仍然是一个好主意。但必须实际存储要成为HasNumber的记录中的Number,而不是在numberDict实现中从整数创建Number(这当然会使我们再次失去记忆化)。 - FunctorSalad
我强烈建议在这种情况下不要使用typeclass,它只会妨碍你。只需具体地对其进行建模,FP工具箱更适合这种类型的编程,该语言更擅长于抽象数据类型而不是typeclass,并且它不会让您欺骗自己正在进行OO建模(即使您没有意识到,如果您以这种方式思考,语言最终会限制您)。 - luqui

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接