在课堂和唱片之间进行选择

时间:2011-11-12 18:56:57

标签: haskell record typeclass

基本问题:在选择使用类或使用记录(具有多态字段)时,应该遵循哪些设计原则?

首先,我们知道类和记录本质上是等价的(因为在Core中,类被人们看作字典,这只是记录)。然而,存在差异:类是隐式传递的,记录必须是显式的。

更深入地看,课程在以下情况下非常有用:

  1. 我们有许多不同的表示“同一件事”和
  2. 在实际使用中,可以推断使用哪种表示。
  3. 当我们(仅参数多态)只有一个数据表示时,类很尴尬,但我们有多个实例。如果我们不想打开,这会导致必须使用 newtype 添加额外标记(仅存在于我们的代码中,因为我们知道这些标记会在运行时被删除)的语法噪音各种麻烦的扩展(即重叠和/或不可判断的实例)。

    当然,事情变得更加混乱:如果我想对我的类型有限制怎么办?让我们选一个真实的例子:

    class (Bounded i, Enum i) => Partition a i where
        index :: a -> i
    

    我可以轻松完成

    data Partition a i = Partition { index :: a -> i}
    

    但是现在我已经失去了约束,我将不得不将它们添加到特定的功能中。

    是否有设计指南可以帮助我?

3 个答案:

答案 0 :(得分:6)

我倾向于看到只需要对函数进行约束的问题。我想问题是,您的数据结构不再精确地模拟您的意图。另一方面,如果您首先将其视为数据结构,那么这应该更少。

我觉得我不一定能很好地掌握这个问题,而且这个问题虽然模糊不清,但我的经验法则往往是类词类是遵守法律(或模型意义)的东西。和数据类型是编码一定数量信息的东西。

当我们想要以复杂的方式对行为进行分层时,我发现类型类开始引人入胜,但很快就会变得痛苦,而切换到字典传递会使事情变得更加简单。也就是说,当我们希望实现可以互操作时,我们应该回归到统一的字典类型。


这需要两个,在一个具体的例子上扩展一点,但仍然只是一些旋转的想法......

假设我们想要在实数上模拟概率分布。脑海中浮现出两种自然的表现形式。

A)类型驱动

class PDist a where
        sample :: a -> Gen -> Double

B)字典驱动

data PDist = PDist (Gen -> Double)

前者让我们做

data NormalDist = NormalDist Double Double -- mean, var
instance PDist NormalDist where...

data LognormalDist = LognormalDist Double Double
instance PDist LognormalDist where...

后者让我们做

mkNormalDist :: Double -> Double -> PDist...
mkLognormalDist :: Double -> Double -> PDist...

在前者中,我们可以写

data SumDist a b = SumDist a b
instance (PDist a, PDist b) => PDist (SumDist a b)...

在后者我们可以简单地写

sumDist :: PDist -> PDist -> PDist

那么有什么权衡?类型类驱动让我们指定我们给出的分布。权衡是我们必须明确地构建分布代数,包括它们组合的新类型。数据驱动不会让我们限制我们给出的分布(或者即使它们格式正确),但作为回报,我们可以做任何我们想做的事情。

此外,我们可以相对容易地编写parseDist :: String -> PDist,但是我们必须经历一些焦虑来为类型类方法做等效。

从某种意义上说,这是另一个级别的类型化/非类型化静态/动态权衡。我们可以给它一个转折,并且认为类型类以及相关的代数定律指定了概率分布的语义。并且PDist类型确实可以成为PDist类型类的实例。同时,我们可以让自己几乎无处不在地使用PDist类型(而不是类型类),而认为作为实例塔和数据类型的iso,以便更加“丰富”地使用类型类。 / p>

实际上,我们甚至可以在类型类函数的术语中定义基本的PDist函数。即mkNormalPDist m v = PDist (sample $ NormalDist m v)因此,设计空间中有很多空间可以在两个表示之间滑动......

答案 1 :(得分:4)

注意:我不确定我是否完全理解OP。建议/意见改进赞赏!


<强>背景

当我第一次了解Haskell中的类型类时,我提到的一般经验法则是,与类似Java的语言相比:

  • 类型类似于接口
  • data与类
  • 类似

Here's另一个SO问答,描述了使用接口的指南(也是接口过度使用的一些缺点)。我的解释是:

  • 记录/ Java类是什么
  • 接口/类型类是具体可以实现的角色
  • 多个不相关的结构可以发挥相同的作用

我打赌你已经知道了这一切。


我尝试遵循自己的代码的指南是:

  • 类型类用于抽象
  • 记录是针对具体结果的

所以在实践中这意味着:

  • 让数据的需求决定记录
  • 让客户端代码确定接口是什么 - 客户端应该依赖于抽象,从而推动类型类的创建和设计

示例:

类型Show,函数show :: (Show s) => s -> String:用于可以表示为String的数据。

  • 客户只想将数据转换为字符串
  • 客户不关心数据(具体化)是什么 - 只关心它可以表示为字符串
  • 实施数据的作用:可以是字符串式的
  • 如果没有类型类,这是无法实现的 - 每个数据类型都需要一个具有不同名称的转换函数,这是一个多么痛苦的处理!

答案 2 :(得分:1)

类型类有时可以提供额外的类型安全性(例如OrdData.Map.union。如果你有类似的情况选择类型类可能有助于你的类型安全 - 那么使用类型类。

我将提供一个不同的例子,我认为类型类不会提供额外的安全性:

class Drawing a where
    drawAsHtml :: a -> Html
    drawOpenGL :: a -> IO ()

exampleFunctionA :: Drawing a => a -> a -> Something
exampleFunctionB :: (Drawing a, Drawing b) => a -> b -> Something

exampleFunctionA无法做到,exampleFunctionB无法做到(我觉得很难解释原因,欢迎见解)。

在这种情况下,我认为使用类型类没有任何好处。

(根据Jacques的反馈和misso的问题编辑)