你确定你真的想把不同的类型放在一个列表中吗?
你可以使用类似 jetxee 的例子来进行存在量化,但想想它的实际作用:你有一个未知类型的术语列表,而你唯一可以用它们做的事情应用 putOut 以获取 IO () 值。也就是说,如果“接口”只提供一个已知结果类型的函数,那么existentials列表和结果列表没有区别。前者唯一可能的用途是将其转换为后者,那么为什么要添加额外的中间步骤呢?改用这样的东西:
main :: IO ()
main = do
sequence_ lst
where lst :: [IO ()]
lst = [out1 1, out2 1 2]
out1 x = putStrLn $ unwords ["Out1", show x]
out2 x y = putStrLn $ unwords ["Out2", show x, show y]
起初这似乎违反直觉,因为它依赖于 Haskell 的一些不寻常的功能。考虑:
- 没有进行额外的计算——惰性求值意味着
show、unwords等。除非执行 IO 操作,否则不会运行。
- 简单地创建
IO () 值不会产生任何副作用——它们可以存储在列表中,以纯代码的形式传递,等等。只有main 中的sequence_ 函数运行它们。
同样的论点适用于“Show 的实例”列表等等。对于Eq 之类的实例,它效果不佳,您需要两个类型的值,但是存在列表不会更好,因为您不知道是否有两个值是同一类型。在 那种 的情况下,你所能做的就是检查每个元素是否等于它自己,然后你也可以(如上)创建一个 Bools 列表并完成它。
在更一般的情况下,最好记住 Haskell 类型的类不是 OOP 接口。类型类是实现即席多态性的强大手段,但不太适合隐藏实现细节。 OOP 语言倾向于将 ad-hoc 多态性、代码重用、数据封装、行为子类型等混为一谈,将所有内容绑定到同一个类层次结构中;在 Haskell 中,您可以(并且通常必须)分别处理每一个。
OOP 语言中的对象,粗略地说,是(隐藏的、封装的)数据的集合,捆绑了用于操作该数据的函数,每个函数都将封装的数据作为隐式参数(this、self , 等等。)。要在 Haskell 中复制这一点,您根本不需要类型类:
- 将每个“类方法”写为常规函数,并明确
self 参数。
- 将每个函数部分应用于“封装”数据的值
- 将部分应用函数合并到一个单一的记录类型中
记录类型替换接口;任何具有正确签名的函数集合都代表接口的实现。在某些方面,这实际上是更好的面向对象风格,因为私有数据被完全隐藏,只有外部行为被暴露。
正如上面更简单的情况,这几乎完全等同于存在版本;函数的记录是通过将类型类的每个方法应用于每个存在而得到的。
有些类型类不能很好地使用函数记录——例如Monad——它们通常也是相同的类型类,不能用传统的OOP接口表达,正如现代版本的 C# 所证明的那样,它广泛使用了 monadic 样式,但没有提供任何类型的通用 IMonad 接口。
另请参阅 this article 涵盖我所说的相同内容。您可能还想查看 Graphics.DrawingCombinators 库的示例,该库提供可扩展、可组合的图形而不使用类型类。