2009-11-05 20 views
22

Un programador de C++ que intenta aprender Haskell aquí. Disculpe esta pregunta, probablemente fácil. Quiero traducir un programa que represente formas 3D. En C++ Tengo algo como:Herencia para extender una estructura de datos en Haskell

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

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

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

Estoy tratando de traducir esto a Haskell (utilizando registros?) Para que yo pueda tener algunas funciones que saben cómo operar en una forma (como el acceso a su nombre y cargo) y otros que solo saben cómo operar en esferas, como calcular algo en función de su posición y radio.

En C++, una función de miembro podría simplemente acceder a estos parámetros, pero me está costando trabajo encontrar la forma de hacerlo en Haskell con registros, o clases de tipo, o lo que sea.

Gracias.

Respuesta

13

Contrariamente a la tendencia a desalentar el uso de clases de tipos, recomendaría (como están aprendiendo) explorar tanto una solución sin clases de tipo y una con, para tener una idea de las diferentes ventajas de los diferentes enfoques.

La solución de "solo tipo de datos cerrado" es ciertamente más "funcional" que las clases de tipos. Implica que su lista de formas es "fija" por su módulo de forma y no extensible con nuevas formas desde el exterior. Todavía es fácil agregar nuevas funciones que operan en las formas.

Tiene un pequeño inconveniente aquí si tiene una función que opera solo en un tipo de forma única porque abandona la comprobación del compilador estático de que la forma ingresada es correcta para la función (consulte el ejemplo de Nathan). Si tiene muchas de estas funciones parciales que solo funcionan en un constructor de su tipo de datos, reconsideraría el enfoque.

Para una solución de clase de tipos, yo personalmente en lugar no reflejar la jerarquía de clases de forma, pero crear clases de tipo de "cosas con una superficie", "cosas con un volumen", "cosas con un radio",. ..

Esto le permite escribir funciones que toman tipos particulares de formas, digamos esferas, solo (ya que cada forma es de su propio tipo), pero no puede escribir una función que tome "cualquier forma" y luego distingue los diversos tipos concretos de formas.

20

La traducción directa.

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 

Por lo general, no harías esto; es repetitivo y las listas polimórficas requieren extensiones de lenguaje.

En su lugar, pegarlas en un solo tipo de datos cerrado es probablemente la primera solución que debe elegir.

type Vector3D = (Double, Double, Double) 

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

Por supuesto que puede simular múltiples niveles de herencia mediante la creación de más clases de tipos:

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

También puede simular mediante la incorporación de tipos de datos.

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" 

Pero simular OO en Haskell no es ni de lejos tan satisfactorio como pensar realmente de una manera Haskellish. ¿Que estás tratando de hacer?

(También tenga en cuenta que todas estas soluciones sólo permiten upcasts, downcasting no es seguro y no permitido.)

+2

¿cómo representarías una cadena de herencia que tiene más de un nivel de profundidad? p.ej. 'shape' ->' Prism' -> 'RectangularPrism' – barkmadley

2

una simple traducción estalla la parte que varía, pero no ofrece clases de tipos:

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

continuación

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" 

Esto es no una pregunta realmente fácil. El diseño de la estructura de datos es muy diferente entre C++ y Haskell, así que apuesto a que la mayoría de las personas que provienen de un lenguaje OO preguntan lo mismo. Desafortunadamente, la mejor manera de aprender es haciendo; su mejor opción es probarlo caso por caso hasta que sepa cómo funcionan las cosas en Haskell.

Mi respuesta es bastante simple, pero no trata bien el caso en que una sola subclase de C++ tiene métodos que los otros no. Lanza un error de tiempo de ejecución y requiere un código adicional para arrancar. También debe decidir si el módulo "subclase" decide si lanzar el error o el módulo "superclase".

6

Como dijo Nathan, el modelado de los tipos de datos es completamente diferente en Haskell desde C++.Podría considerar el siguiente enfoque:

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 } 

En otras palabras, las referencias al modelo de las superclases como campos adicionales en su tipo de datos. Se extiende fácilmente a cadenas de herencia más largas.

No utilice clases tipo, como sugiere ephemient. Estos se utilizan para funciones de sobrecarga y ese no es el problema aquí: su pregunta se relaciona con el modelo datos, no comportamiento.

Espero que esto ayude!

+0

¿Sugiere que al modelar la herencia de datos, use campos adicionales, al modelar la herencia del comportamiento, use clases de tipos? ¿Puede escribir clases para parametrizar sobre otras clases de tipos? – CMCDragonkai

Cuestiones relacionadas