views:

199

answers:

5

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.

A: 

Chances are its not going to just translate over like you are expecting but you may want to check out http://homepages.cwi.nl/~ralf/OOHaskell/

James
I thought that was an academic toy for advanced Haskellers, not a tool to ease the transition for C++ programmers. (The giveaway is "OOHaskell lends itself as a sandbox for typed OO language design.")
Nathan Sanders
+6  A: 

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.)

ephemient
how would you represent an inheritance chain thats more than one level deep? e.g. `shape` -> `Prism` -> `RectangularPrism`
barkmadley
A: 

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.

Nathan Sanders
+3  A: 

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!

Martijn
+5  A: 

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.

Rüdiger Hanke