对我来说,您似乎总是可以传递函数参数,而不是使用类型类。例如,而不是定义相等类型类:
class Eq a where
(==) :: a -> a -> Bool
并且在其他函数中使用它来指示类型实参必须是Eq
的实例:
elem :: (Eq a) => a -> [a] -> Bool
我们是否不能不使用类型类而仅定义elem
函数,而是传递可以完成工作的函数参数?
是。这被称为“字典通过风格”。有时,当我做一些特别棘手的事情时,我需要废弃一个类型类并将其转换为字典,因为字典传递功能更强大1,但通常非常麻烦,从而使概念上简单的代码看起来相当复杂。有时,我在不是Haskell的语言中使用字典传递样式来模拟类型类(但已经了解到,通常这并不像听起来那样好)。
当然,只要表达能力有差异,就需要进行权衡。如果使用DPS编写给定的API,则可以通过更多方式使用它,但如果不这样做,则该API将获得更多信息。实践中显示的一种方式是在Data.Set
中,它依赖于每个类型只有一个Ord
词典的事实。 Set
存储根据Ord
排序的元素,如果您用一个字典构建一个集合,然后使用DPS插入一个不同的元素(如DPS),则可能会破坏Set
的不变式并使其崩溃。可以使用phantom existential类型标记字典来缓解这种唯一性问题,但是同样,这会以API中相当烦人的复杂性为代价。这在Typeable
API中也以几乎相同的方式显示。
唯一性位很少出现。类型类最擅长为您编写代码。例如,
catProcs :: (i -> Maybe String) -> (i -> Maybe String) -> (i -> Maybe String)
catProcs f g = f <> g
[需要两个“处理器”,它们接受一个输入并可能提供一个输出,并将它们连接起来,展平Nothing
,必须在DPS中编写如下所示:
catProcs f g = (<>) (funcSemi (maybeSemi listSemi)) f g
即使我们已经在类型签名中将其拼写出来了,并且编译器已经知道了,我们基本上不得不再次拼写我们正在使用它的类型。因为只有一种方法可以在类型上构造给定的Semigroup
,所以编译器会为您完成此操作。当您开始定义许多参数实例并使用类型的结构来为您计算时,这会产生“复合兴趣”类型的效果,如Data.Functor.*
组合器中那样,并且在Data.Functor.*
的情况下,这种效果非常有用您基本上可以为您编写所有类型的“标准”代数结构。
而且甚至不让我开始学习MPTC和Fundeps,它们将信息反馈给类型检查和推断。我从来没有尝试过将这种东西转换为DPS-我怀疑这会涉及传递很多类型相等性证明-但无论如何,我敢肯定,这对我的大脑来说会比我舒适的工作多得多与。
-
1 U 除非使用deriving via
,否则它们的功率相等-但deriving via
的使用也很麻烦。
是。基本上,编译器所做的(称为字典传递)就是要进行类型分类的。对于该功能,从字面上看,它看起来像这样:
reflection
呼叫reflection
现在等效于reflection
。在这种特定情况下,您可以走得更远:elemBy :: (a -> a -> Bool) -> a -> [a] -> Bool
elemBy _ _ [] = False
elemBy eq x (y:ys) = eq x y || elemBy eq x ys
每次都有相同的第一个参数,因此您可以使调用者有责任使用该参数,并最终得到:
elemBy (==) x xs
呼叫elem x xs
现在等效于eq
。
...哦,等等。就是elemBy2 :: (a -> Bool) -> [a] -> Bool
elemBy2 _ [] = False
elemBy2 eqx (y:ys) = eqx y || elemBy2 eqx ys
。 (实际上是elemBy2 (x ==) xs
。)