继承在Haskell中扩展数据结构

时间:2009-11-05 00:31:54

标签: oop inheritance haskell

一位C ++程序员试图在这里学习Haskell。请原谅这个可能很简单的问题。我想翻译一个代表3D形状的程序。在C ++中我有类似的东西:

class Shape {
public:
  std::string name;
  Vector3d position;
};

class Sphere : public Shape {
public:
  float radius;
};

class Prism : public Shape {
public:
  float width, height, depth;
};

我正在尝试将其转换为Haskell(使用记录?),以便我可以拥有一些知道如何操作Shape的函数(比如访问它的名称和位置),以及其他只知道如何操作球体的函数,比如根据位置和半径计算某些东西。

在C ++中,成员函数可以只访问这些参数,但我很难弄清楚如何在Haskell中使用记录,类型类或其他任何方法来执行此操作。

感谢。

4 个答案:

答案 0 :(得分:22)

直接的翻译。

type Vector3D = (Double, Double, Double)

class Shape shape where
    name :: shape -> String
    position :: shape -> Vector3D

data Sphere = Sphere {
    sphereName :: String,
    spherePosition :: Vector3D,
    sphereRadius :: Double
}

data Prism = Prism {
    prismName :: String,
    prismPosition :: Vector3D,
    prismDimensions :: Vector3D
}

instance Shape Sphere where
    name = sphereName
    position = spherePosition

instance Shape Prism where
    name = prismName
    position = prismPosition
但是,你通常不会这样做;它是重复的,多态列表需要语言扩展。

相反,将它们粘贴到一个封闭的数据类型中可能是您​​应该采用的第一个解决方案。

type Vector3D = (Double, Double, Double)

data Shape
  = Sphere { name :: String, position :: Vector3D, radius :: Double }
  | Prism { name :: String, position :: Vector3D, dimensions :: Vector3D }

您当然可以通过创建更多类型类来模拟多个继承级别:

class (Shape shape) => Prism shape where
    dimensions :: Vector3D
data RectangularPrism = ...
data TriangularPrism = ...
instance Prism RectangularPrism where ...
instance Prism TriangularPrism where ...

您也可以通过嵌入数据类型来模拟它。

type Vector3D = (Double, Double, Double)

data Shape = Shape { name :: String, position :: Vector3D }

data Sphere = Sphere { sphereToShape :: Shape, radius :: Double }
newSphere :: Vector3D -> Double -> Shape
newSphere = Sphere . Shape "Sphere"

data Prism = Prism { prismToShape :: Shape, dimensions :: Vector3D }

data RectangularPrism = RectangularPrism { rectangularPrismToPrism :: Prism }
newRectangularPrism :: Vector3D -> Vector3D -> RectangularPrism
newRectangularPrism = (.) RectangularPrism . Prism . Shape "RectangularPrism"

data TriangularPrism = TriangularPrism { triangularPrismToPrism :: Prism }
newTriangularPrism :: Vector3D -> Vector3D -> TriangularPrism
newTriangularPrism = (.) TriangularPrism . Prism . Shape "TriangularPrism"

但是,在Haskell中模拟OO并不像Haskellish方式那样令人满意。你想做什么?

(另请注意,所有这些解决方案仅允许向上转发,向下转换是不安全的并且不允许。)

答案 1 :(得分:13)

与阻止使用类型类的趋势相反,我建议(正如您所学习的)探索没有类型类的解决方案,以及一种方法,以了解各种方法的不同权衡。< / p>

“单一闭合数据类型”解决方案当然比类型更具“功能性”。这意味着你的形状模块“固定”了你的形状列表,而不是外部的新形状可扩展。在形状上添加新功能仍然很容易。

如果你有一个仅在单一形状类型上运行的函数,那么你会有一些不便,因为你放弃了静态编译器检查传入的形状对于函数是否正确(参见Nathan的例子)。如果你有很多这些部分函数仅适用于你的数据类型的一个构造函数,我会重新考虑这种方法。

对于类型类解决方案,我个人宁愿镜像形状类层次结构,而是为“具有表面区域的东西”,“具有卷的东西”,“具有卷的东西”创建类型类半径“,......

这允许你编写采用特定形状的函数,比如球体,只有(因为每个形状都是它自己的类型),但是你不能编写一个带有“任何形状”的函数然后区分各种具体的各种形状。

答案 2 :(得分:7)

像Nathan所说,建模数据类型在Haskell与C ++完全不同。您可以考虑以下方法:

data Shape  = Shape  { name        :: String, position :: Vector3d }
data Sphere = Sphere { sphereShape :: Shape,  radius   :: Float }
data Prism  = Prism  { prismShape  :: Shape,  width    :: Float, height :: Float, depth :: Float }

换句话说,将对超类的引用建模为数据类型中的额外字段。它很容易扩展到更长的继承链。

不要使用类型类,如ephemient暗示。这些用于重载功能,这根本不是问题:您的问题涉及数据的建模,而不是行为

希望这有帮助!

答案 3 :(得分:2)

一个简单的翻译突破了变化的部分,但避免了类型类:

type Vector3D = (Float,Float,Float)
data Body = Prism Vector3D | Sphere Double
radius (Prism position) = -- code here
radius (Sphere r) = r

然后

data Shape = Shape {
  name :: String,
  position :: Vector3D,
  body :: Body
}

shapeOnly (Shape _ pos _) = -- code here

both = radius . body

sphereOnly (Shape _ _ (Sphere radius)) = -- code here
sphereOnly _ = error "Not a sphere"

这是一个非常简单的问题。 C ++和Haskell之间的数据结构设计非常不同,所以我敢打赌,大多数来自OO语言的人都会问同样的事情。 不幸的是,最好的学习方法是做;你最好的选择是逐个尝试,直到你了解Haskell的工作原理。

我的回答非常简单,但它并不适用于单个C ++子类具有其他人没有的方法的情况。它会引发运行时错误,需要额外的代码才能启动。您还必须决定“子类”模块是否决定是抛出错误还是“超类”模块。