通过假设正确的解决方案是将不同的类型存储在列表中,您将 OOP 思维方式带入 Haskell。我将从检验这个假设开始。
通常我们将不同类型存储在同构列表中,因为它们支持公共接口。为什么不直接将通用接口分解出来并将其存储在列表中呢?
不幸的是,您的问题没有描述该通用接口是什么,因此我将仅介绍一些常见示例作为演示。
第一个例子是一堆值,x
,y
和z
,它们都支持Show
具有签名的函数:
(Show a) => a -> String
show
我们可以直接调用值并将结果字符串存储在列表中,而不是存储我们稍后要显示的类型:
list = [show x, show y, show z] :: String
过早调用不会受到惩罚,因为 Haskell 是一种惰性语言,并且在我们真正需要字符串之前show
不会实际评估s。show
或者该类型可能支持多种方法,例如:
class Contrived m where
f1 :: m -> String -> Int
f2 :: m -> Double
我们可以将上述形式的类转换为等效的字典,其中包含将方法部分应用于我们的值的结果:
data ContrivedDict = ContrivedDict {
f1' :: String -> Int,
f2' :: Double }
...我们可以使用这个字典将任何值打包到我们期望它支持的公共接口中:
buildDict :: (Contrived m) => m -> ContrivedDict
buildDict m = ContrivedDict { f1' = f1 m, f2' = f2 m }
然后我们可以将这个通用接口本身存储在列表中:
list :: [buildDict x, buildDict y, buildDict z]
同样,我们没有存储不同类型的值,而是将它们的共同元素分解出来存储在列表中。
然而,这个技巧并不总是奏效。病态的例子是任何期望两个相同类型的操作数的二元运算符,例如类中的(+)
运算符Num
,它具有以下类型:
(Num a) => a -> a -> a
据我所知,没有很好的基于字典的解决方案来部分应用二元运算并将其存储以保证将其应用于相同类型的第二个操作数。在这种情况下,存在类型类可能是唯一有效的方法。但是,我建议您尽可能坚持使用基于字典的方法,因为它允许比基于类型类的方法更强大的技巧和转换。
有关此技术的更多信息,我建议您阅读 Luke Palmer 的文章:Haskell Antipattern: Existential Typeclass。