简体   繁体   中英

Inheritance to extend a data structure in Haskell

A C++ programmer trying to learn Haskell here. Please excuse this probably easy question. I want to translate a program that represents 3D shapes. In C++ I have something like:

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

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

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

I am trying to translate this to Haskell (using records?) so that I can have some functions which know how to operate on a Shape (like accessing its name and position), and others than know only how to operate on spheres, like calculating something based on its position and radius.

In C++ a member function could just access these parameters but I'm having a hard time figuring out how to do this in Haskell with records, or type classes, or whatever.

Thanks.

The straight-forward translation.

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

You usually wouldn't do this, though; it's repetitious and polymorphic lists require language extensions.

Instead, sticking them into a single closed datatype is probably the first solution you should go for.

type Vector3D = (Double, Double, Double)

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

You can certainly simulate multiple levels of inheritance by creating more typeclasses:

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

You can also simulate it by embedding datatypes.

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"

But simulating OO in Haskell is not anywhere near as satisfying as actually thinking in a Haskellish way. What are you trying to do?

(Also note that all of these solutions only permit upcasts, downcasting is unsafe and disallowed.)

Contrary to the trend of discouraging the use of typeclasses, I'd recommend (as you're learning) to explore both a solution without typeclasses and one with, to get a feeling for the different tradeoffs of the various approaches.

The "single closed datatype" solution is certainly more "functional" than typeclasses. It implies that your list of shapes is "fixed" by your shape module and not extensible with new shapes from the outside. It is still easy to add new functions operating on the shapes.

You have a slight inconvenience here if you have a function that operates only on a single shape type because you give up the static compiler check that the shape passed in is correct for the function (see Nathan's example). If you have a lot of these partial functions that work only on one constructor of your datatype, I would reconsider the approach.

For a typeclass solution, I'd personally rather not mirror the shape class hierarchy but create type classes for "things with a surface area", "things with a volume", "things with a radius", ...

This allows you to write functions that take particular kinds of shapes, say spheres, only (as each shape is its own type), but you can't write a function that takes "any shape" and then distinguishes the various concrete kinds of shapes.

Like Nathan said, modelling datatypes is completely different in Haskell from C++. You could consider the following approach:

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 }

In other words, model references to super classes as extra fields in your datatype. It easily extends to longer inheritance chains.

Don't use type classes, like ephemient suggests. These are used for overloading functions and that is not the issue here at all: your question concerns the modelling of data , not behaviour .

Hope this helps!

A simple translation breaks out the part that varies but avoids typeclasses:

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

then

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"

This is not a really easy question. Data structure design is very different between C++ and Haskell, so I bet that most people coming from an OO language ask the same thing. Unfortunately, the best way to learn is by doing; your best bet is to try it on a case-by-case basis until you learn how things work in Haskell.

My answer is pretty simple, but it doesn't deal well with the case where a single C++ subclass has methods that the others don't. It throws a runtime error and requires extra code to boot. You also have to decide whether the "subclass" module decides whether to throw the error or the "superclass" module.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM