Aus*_*ett 26 oop haskell data-structures
来自OOP背景,Haskell的类型系统以及数据构造函数和类型类交互的方式很难概念化.我可以理解每个用于简单示例的方法,但是非常适合OOP风格的一些更复杂的数据结构示例被证明是非常重要的,可以转换为类似的优雅和可理解的类型.
特别是,我在组织数据层次结构时遇到问题,如下所示.

这是一个深度嵌套的层次继承结构,缺乏对子类型的支持使得不清楚如何将这个结构转换为Haskell中的自然感觉替代方案.替换类似于Polygon和数据类型的东西,声明它可能是好的
data Polygon
= Quad Point Point
| Triangle Point Point Point
| RegularNGon Int Radius
| ...
Run Code Online (Sandbox Code Playgroud)
但是这会丢失一些结构,并且只能在层次结构的一个级别上真正令人满意地完成.类型类可用于实现继承的一种形式,子结构,一个Polygon类型类可能的一个子类Shape,所以也许所有Polygon情况下都实现了centroid :: Point,也vertices :: [Point],但这似乎不能令人满意.捕获Haskell中图片结构的好方法是什么?
Ben*_*Ben 31
您可以使用sum类型来表示整个层次结构,而不会丢失结构.像这样的东西会这样做:
data Shape = IsPoint Point
| IsLine Line
| IsPolygon Polygon
data Point = Point { x :: Int, y :: Int }
data Line = Line { a :: Point, b :: Point }
data Polygon = IsTriangle Triangle
| IsQuad Quad
| ...
Run Code Online (Sandbox Code Playgroud)
等等.基本模式是将每个OO抽象类转换为Haskell和类型,其每个直接OO子类(可能本身都是抽象的)作为sum类型中的变体.具体类是产品/记录类型,其中包含实际数据成员.1
与通过这种方式建模的OOP相比,你失去的东西不是表示层次结构的能力,而是能够在不触及现有代码的情况下扩展它.Sum类型是"封闭的",其中OO继承是"开放的".如果您以后决定要Circle为其提供选项Shape,则必须将其添加到Shape,然后在a模式匹配的任何位置添加案例Shape.
但是,这种层次结构可能需要在OO中进行相当自由的向下转型.例如,如果你想要一个能够判断两个形状是否相交的函数,它可能是一个Shape类似的抽象方法Shape.intersects(Shape other),那么每个子类型都可以编写自己的实现.但是当我写Rectangle.intersects(Shape other)这篇文章时,基本上不可能一般,不知道Shape那里有什么其他的子类.我将不得不使用isinstance支票来查看other实际情况.但这实际上意味着我可能无法在Circle不重新访问现有代码的情况下添加新的子类; 需要进行实例检查的OO层次结构实际上就像Haskell和类型层次结构一样"闭合".基本上,通过应用此模式生成的和类型之一上的模式匹配等同于OO版本中的isinstancing和downcasting.只是因为编译器已经详尽地知道了和类型(只有可能因为它们已经关闭),如果我Circle向Shape编译器添加一个大小写能够告诉我有关处理该情况需要重新访问的所有地方.2
如果你有一个等级制度并不需要大量的向下转换的,这意味着不同的基类有:保证可用丰富实用的接口,你平时用的东西,通过该接口,而不是什么它可能切换那么你可以使用类型类.您仍然需要所有"叶子"数据类型(具有实际数据字段的产品类型),而不是添加和类型包装器来对它们进行分组,您可以为公共接口添加类型类.如果您可以使用这种翻译风格,那么您可以更轻松地添加新案例(只需添加新Circle数据类型,并添加一个实例来说明它如何实现Shape类型类;类中任何类型的多态性Shape都将是现在处理Circles).但是,如果你在OO中这样做的话,那么当你发现你无法处理形状时,你总是可以作为逃生舱.在Haskell中使用这种设计是不可能的.3
但我对"如何在Haskell中表示OO类型层次结构"的"真实"回答不幸是陈词滥调:我没有.我在Haskell中的设计与在OO语言4中的设计不同,在实践中它不是一个大问题.但是要说我如何以不同的方式设计这个案例,我必须更多地了解你正在使用它们的内容.例如,你可以做一些事情,比如将一个形状表示为一个Point -> Bool函数(它告诉你任何给定的点是否在形状内),并且有类似于circle :: Point -> Int -> (Point -> Bool)生成与普通形状相对应的函数的东西; 这种表示形式对于形成复合交集/联合形状非常棒,而不知道任何关于它们的东西(intersect shapeA shapeB = \point -> shapeA point && shapeB point),但是对于计算区域和周长等事物很糟糕.
1如果您有具有数据成员的抽象类,或者您具有也具有其他子类的具体类,则可以手动将数据成员推送到"叶子"中,将继承的数据成员分解为共享记录并使所有"叶子"包含其中一个,分割一个层,以便你有一个产品类型包含继承的数据成员和一个和类型(其中和类型然后"分裂"到子类的选项),像这样的东西.
2如果您使用全部捕捉模式,那么警告可能并非详尽无遗,因此它并不总是防弹,但它的防弹方式取决于您的编码方式.
3除非您选择使用类似解决方案的运行时类型信息Typeable,但这不是一个无形的变化; 您的呼叫者也必须选择加入.
4实际上,即使在OO语言中,我也可能不会设计这样的层次结构.我发现它并没有像你在实际程序中所认为的那样有用,因此"赞成合成而不是继承"的建议.
您可能正在寻找Haskell等效的动态分派,这样您就可以存储支持不同通用Shape接口实现的异构值列表.
Haskell的存在类型支持这种用法.Haskell程序实际上需要存在类型是相当罕见的 - 正如Ben的答案所示,sum类型可以处理这类问题.但是,存在类型适用于大型,开放式的案例集合:
{-# LANGUAGE ExistentialQuantification #-}
...
class Shape a where
bounds :: a -> AABB
draw :: a -> IO ()
data AnyShape = forall a. Shape a => AnyShape a
Run Code Online (Sandbox Code Playgroud)
这允许您以开放式样式声明实例:
data Line = Line Point Point
instance Shape Line where ...
data Circle= Circle {center :: Point, radius :: Double}
instance Shape Circle where ...
...
Run Code Online (Sandbox Code Playgroud)
然后,您可以构建异构列表:
shapes = [AnyShape(Line a b),
AnyShape(Circle a 3.0),
AnyShape(Circle b 1.8)]
Run Code Online (Sandbox Code Playgroud)
并以统一的方式使用它:
drawIn box xs = sequence_ [draw s | AnyShape s <- xs, bounds s `hits` box]
Run Code Online (Sandbox Code Playgroud)
请注意,您需要打开包装AnyShape才能使用class Shape界面功能.另请注意,您必须使用类函数来访问异构数据 - 没有其他方法可以"向下"展开未包装的存在价值s!它的类型只在本地范围内有意义,因此编译器不会让它逃脱.
如果你试图使用存在类型,但发现自己需要"向下"它们,总和类型可能更适合.