在Haskell中编写更大的程序时,我发现自己经常遇到问题。我发现自己经常想要多个不同的类型,这些类型共享一个内部表示和几个核心操作。
有两种相对明显的方法可以解决这个问题。
一个是使用类型类和GeneralizedNewtypeDeriving
扩展名。将足够的逻辑放入类型类中以支持用例所需的共享操作。创建具有所需表示的类型,并为该类型创建类型类的实例。然后,对于每个用例,使用newtype为它创建包装器,并派生公共类。
另一种是使用幻像类型变量声明类型,然后使用EmptyDataDecls
为每个不同的用例创建不同的类型。
我主要关注的不是混合共享内部表示和操作的值,而是在我的代码中有不同的含义。这两种方法都解决了这个问题,但感觉非常笨拙。我的第二个问题是减少所需的样板量,两种方法都做得很好。
每种方法有哪些优缺点?是否有一种技术更接近于我想做的事情,提供没有样板代码的类型安全性?
答案 0 :(得分:3)
还有另一种直截了当的方法。
data MyGenType = Foo | Bar
op :: MyGenType -> MyGenType
op x = ...
op2 :: MyGenType -> MyGenType -> MyGenType
op2 x y = ...
newtype MySpecialType {unMySpecial :: MyGenType}
inMySpecial f = MySpecialType . f . unMySpecial
inMySpecial2 f x y = ...
somefun = ... inMySpecial op x ...
someOtherFun = ... inMySpecial2 op2 x y ...
可替换地,
newtype MySpecial a = MySpecial a
instance Functor MySpecial where...
instance Applicative MySpecial where...
somefun = ... fmap op x ...
someOtherFun = ... liftA2 op2 x y ...
我认为如果您想要使用任何频率的“裸”普通类型,并且有时只想标记它,这些方法会更好。另一方面,如果您通常想要使用标记,那么幻像类型方法更直接地表达您想要的内容。
答案 1 :(得分:2)
我对玩具示例进行了基准测试,并未发现两种方法之间存在性能差异,但使用情况通常有所不同。
例如,在某些情况下,您有一个泛型类型,其构造函数已公开,并且您希望使用newtype
包装器来指示更具语义特定的类型。使用newtype
s会导致调用网站,例如
s1 = Specific1 $ General "Bob" 23
s2 = Specific2 $ General "Joe" 19
在不同的特定新类型之间内部表示相同的事实是透明的。
类型标记方法几乎总是伴随着表示构造函数隐藏,
data General2 a = General2 String Int
和智能构造函数的使用,导致数据类型定义和调用网站,如
mkSpecific1 "Bob" 23
部分原因是你想要一些语法上很轻的方法来指示你想要的标签。如果你没有提供智能构造函数,那么客户端代码通常会选择类型注释来缩小范围,例如,
myValue = General2 String Int :: General2 Specific1
一旦采用了智能构造函数,就可以轻松添加额外的验证逻辑来捕获标记的误用。幻像类型方法的一个很好的方面是对于可以访问表示的内部代码,模式匹配根本不会改变。
internalFun :: General2 a -> General2 a -> Int
internalFun (General2 _ age1) (General2 _ age2) = age1 + age2
当然,您可以将newtype
与智能构造函数和内部类一起用于访问共享表示,但我认为此设计空间中的关键决策点是您是否要保持表示构造函数的公开。如果表示的共享应该是透明的,并且客户端代码应该可以自由地使用它希望的任何标记而无需额外的验证,那么newtype
GeneralizedNewtypeDeriving
包装器可以正常工作。但是如果你要采用智能构造函数来处理不透明的表示,那么我通常更喜欢幻像类型。
答案 2 :(得分:1)
将足够的逻辑放入类型类中以支持用例所需的共享操作。创建具有所需表示的类型,并为该类型创建类型类的实例。然后,对于每个用例,使用newtype为它创建包装器,并派生公共类。
这会带来一些陷阱,具体取决于类型的性质以及涉及的操作类型。
首先,它强制许多函数不必要地具有多态性 - 即使实际上每个实例对不同的包装器都做同样的事情,类型类的开放世界假设意味着编译器必须考虑其他实例的可能性。虽然GHC绝对比普通编译器更聪明,但是你可以提供的信息越多,它就可以帮助你。
其次,这可能会为更复杂的数据结构造成瓶颈。包装类型上的任何泛型函数都将被约束到类型类所呈现的接口,因此除非该接口在表达性和效率方面都是详尽无遗的,否则您可能会遇到使用该类型的阻塞算法或更改类型类的风险反复找到缺失的功能。
另一方面,如果包装类型已经保持抽象(即,它不导出构造函数),则瓶颈问题无关紧要,因此类型类可能很有意义。否则,我可能会使用幻像类型标记(或者可能是sclv描述的标识Functor
方法)。