Haskell中的异构多态(正确方式)

3

让一个模块来抽象区域操作(糟糕的定义)


将一个模块用来抽象区域操作(定义不好)。
class Area someShapeType where
  area :: someShapeType -> Float

-- module utilities
sumAreas :: Area someShapeType => [someShapeType]
sumAreas = sum . map area

让后验明确形状类型模块(好或可接受的定义)
data Point = Point Float Float

data Circle = Circle Point Float
instance Surface Circle where
  surface (Circle _ r) = 2 * pi * r

data Rectangle = Rectangle Point Point
instance Surface Rectangle where
  surface (Rectangle (Point x1 y1) (Point x2 y2)) = abs $ (x2 - x1) * (y2 - y1)

让一些数据

c1 = Circle (Point 0 0) 1
r1 = Rectangle (Point 0 0) (Point 1 1)

然后,尝试使用
totalArea = sumAreas [c1, r1]

必须将 [c1, r1] 类型扩展为 [Circle] 或者 [Rectangle]!(并且不合法)

我可以使用 forall 和额外的 data 类型来实现这个目标。

data Shape = forall a . Surface a => Shape a

sumSurfaces :: [Shape] -> Float
sumSurfaces = sum . map (\(Shape x) -> surface x)

然后,接下来的代码成功运行。
sumSurfaces [Shape c1, Shape r1]

我认为,在[Shape c1, ...]和lambda参数中使用data ShapeShape构造函数是不美观的(我的第一种[且不好的]方法比较美观)。

在Haskell中实现"异质多态"的正确方式是什么?

非常感谢您的时间!


嗯...我正在阅读http://www.haskell.org/haskellwiki/Existential_type,那么`data Shape`是正确的方式吗? - josejuan
要不要在 class Area 中添加一个实例,像这样 class Surface a => Area a where area = surface - ErikR
3个回答

8
你的第一种(不好的)方式并不漂亮,它是Lispy。这在静态类型语言中是不可能的;即使你在例如Java中这样做,你实际上也是通过使用基类指针引入了一个单独的量化步骤,这类似于 data Shape = forall a. Surface a
关于存在量化是否好的问题存在争议,我认为大多数Haskeller并不太喜欢它。当然,在这里使用它肯定不是正确的选择:sum [ area c1, area c2 ] 更容易且同样有效。但是在更复杂的问题上,情况可能会有所不同;当你“需要”异构多态性时,存在量化就是正确的选择。
只要记住,你总是可以绕过这个问题:由于Haskell是惰性的,你可以“预先”应用所有可能的操作(在这个例子中只有 area),将所有结果存储在某个记录中,而不是存储多态对象的列表。这样你就保留了所有信息。
或者,更符合习惯的方法是根本不要产生这些对象的列表。你想对这些对象进行一些操作,为什么不直接将这些操作传递到生成不同Shape的函数中,并在现场应用它们!这种反转将存在量化换成了普遍量化,而后者更为广泛地被接受。

不,sum [ area c1, area c2 ] 不是一个有效的回答,多态性并没有封装到所有过程中(sum...)。因此需要使用 data Shape。我想知道是否存在一种直接的方法来执行 data Shape。是的,这是可能的,当 Haskell 编译器检测到 [c1, r1] 数组时,它可以创建一个虚拟的 data Shape(我认为这与静态语言无关)。无论如何,谢谢! - josejuan
4
“data Shape = ...”不是一种直接的方式吗?“ 可以创建一个虚拟的“data Shape”,但这将破坏 Haskell 的类型推断机制。您需要给每个对象一个显式类型,这将使语言看起来像 C++03 一样丑陋。 - leftaroundabout
3
你可以按照 Haskell 的惯用方式来做,这种情况下你需要根据建议改变你的方法。或者你可以保持原来的方法,但是要接受违背语言本身自然规则所带来的不便之处。 - Zopa

5
你的存在解决方案还可以。也许使用一个 GADT 会更“漂亮”,例如:
{-# LANGUAGE GADTs #-}
data Shape where
    Shape :: (Surface a) => a -> Shape

...正如leftaraoundabout所建议的那样,您可能能够以不同的方式构建您的代码。

但我认为您在这里基本上遇到了 表达式问题; 或者更准确地说:通过试图聪明地组织您的代码(每个形状都有不同的类别),以预期解决表达式问题,您为自己引入了新的困难。

查看Wouter Swierstra的有趣 Data Types a la Carte,希望它与您的问题有关的优雅解决方案。也许有人可以评论一下hackage上受该论文启发的好软件包。


1
哇!DataTypesALaCarte.pdf 正是我在寻找的,再次感谢你! - josejuan

5
你最初所做的是遇到了存在性反模式。
这里为什么要使用类呢?
data Shape = Shape { area :: Double }

data Point = Point Double Double

circle :: Point -> Double -> Shape
circle p r =
    Shape $ 2 * pi * r

rectangle :: Point -> Point -> Shape
rectangle (Point x1 y1) (Point x2 y2) =
    Shape $ abs $ (x2 - x1) * (y2 - y1)

现在您可以轻松地获得您想要的内容(一个形状列表):

*Main> map area [circle (Point 2 0) 5, rectangle (Point 0 0) (Point 2 10)]
[31.41592653589793,20.0]

谢谢你,但你解决的是我的例子,而不是我的问题(暴露抽象行为“先验”)。jberryman解释了这一点。无论如何还是谢谢! - josejuan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接