2009-12-13 13 views
11

Supongamos que tengo dos tipos de datos Foo y Bar. Foo tiene los campos xey. Bar tiene los campos x y z. Quiero poder escribir una función que tome un Foo o una Barra como parámetro, extrae el valor de x, realiza algunos cálculos en él, y luego devuelve un Foo o Bar nuevo con el valor x establecido en consecuencia.Haskell sintaxis de registro y clases de tipo

Aquí es uno de los enfoques:

class HasX a where 
    getX :: a -> Int 
    setX :: a -> Int -> a 

data Foo = Foo Int Int deriving Show 

instance HasX Foo where 
    getX (Foo x _) = x 
    setX (Foo _ y) val = Foo val y 

getY (Foo _ z) = z 
setY (Foo x _) val = Foo x val 

data Bar = Bar Int Int deriving Show 

instance HasX Bar where 
    getX (Bar x _) = x 
    setX (Bar _ z) val = Bar val z 

getZ (Bar _ z) = z 
setZ (Bar x _) val = Bar x val 

modifyX :: (HasX a) => a -> a 
modifyX hasX = setX hasX $ getX hasX + 5 

El problema es que todos estos captadores y definidores son dolorosas para escribir, especialmente si reemplazo Foo y Bar con tipos de datos del mundo real que tienen una gran cantidad de campos.

La sintaxis de registro de Haskell ofrece una manera mucho más agradable de definir estos registros. Pero, si lo intento de definir los registros como esto

data Foo = Foo {x :: Int, y :: Int} deriving Show 
data Bar = Foo {x :: Int, z :: Int} deriving Show 

voy a conseguir un error que dice que x se define en múltiples ocasiones. Y, no veo ninguna forma de hacer estas partes de una clase de tipo para poder pasarlos a modifyX.

¿Hay alguna manera limpia de resolver este problema, o me veo obligado a definir mis propios getters y setters? Dicho de otra manera, ¿hay alguna forma de conectar las funciones creadas por la sintaxis del registro con las clases de tipos (tanto los getters como los setters)?

EDITAR

Aquí es el verdadero problema que estoy tratando de resolver. Estoy escribiendo una serie de programas relacionados que usan System.Console.GetOpt para analizar sus opciones de línea de comandos. Habrá una gran cantidad de opciones de línea de comandos que son comunes en todos estos programas, pero algunos de los programas pueden tener opciones adicionales. Me gustaría que cada programa pueda definir un registro que contenga todos sus valores de opción. Luego empiezo con un valor de registro predeterminado que luego se transforma a través de una mónada StateT y GetOpt para obtener un registro final que refleje los argumentos de la línea de comandos. Para un solo programa, este enfoque funciona realmente bien, pero estoy tratando de encontrar una forma de reutilizar el código en todos los programas.

+3

Si tenía un solo tipo de datos 'datos FooBar = Foo {x :: Int, y :: Int} | Bar {x :: Int, z :: Int} 'no tendrías este problema. Si sus tipos de datos estaban en módulos diferentes, podría usar '{- # LANGUAGE DisambiguateRecordFields # -}'. ¿Alguna razón para seguir con tu diseño actual? – ephemient

Respuesta

5

Quiere extensible records que, en mi opinión, es uno de los temas más comentados en Haskell. Parece que actualmente no hay mucho consenso sobre cómo implementarlo.

En su caso, parece que quizás en lugar de un registro ordinario, podría utilizar una lista heterogénea como las implementadas en HList.

Por otra parte, parece que solo tiene dos niveles aquí: común y programa. Así que tal vez solo deba definir un tipo de registro común para las opciones comunes y un tipo de registro específico del programa para cada programa, y ​​use StateT en una tupla de esos tipos. Para las cosas comunes, puede agregar alias que componen fst con los accesores comunes, por lo que es invisible para las personas que llaman.

+0

Interesante idea sobre tener un tipo de registro común y un tipo específico de programa. Es posible que tenga más agrupaciones de características comunes además de las comunes y el programa (no estoy seguro aún), pero podría extender fácilmente su enfoque para apoyar esto. (Por ejemplo, podría pasar como una lista asociativa de registros a través del StateT, y cada opción sabría cómo buscar su registro correspondiente en la lista por nombre). –

3

Se puede usar un código como

data Foo = Foo { fooX :: Int, fooY :: Int } deriving (Show) 
data Bar = Bar { barX :: Int, barZ :: Int } deriving (Show) 

instance HasX Foo where 
    getX = fooX 
    setX r x' = r { fooX = x' } 

instance HasX Bar where 
    getX = barX 
    setX r x' = r { barX = x' } 

¿Qué está modelando en su código? Si supiéramos más sobre el problema, podríamos sugerir algo menos incómodo que este diseño orientado a objetos calzados en un lenguaje funcional.

+0

Gracias por la respuesta. Su enfoque simplifica las cosas de alguna manera. Modifiqué mi pregunta original para explicar más sobre el problema real que estoy tratando de resolver. Estoy de acuerdo en que usar un enfoque OO con un lenguaje funcional no suele ser la mejor manera. Soy nuevo en lenguajes funcionales y aún estoy tratando de salir de mi pensamiento OO. :-) –

1

Si realiza las instancias de tipos de Plegable, obtiene una función toList que puede usar como base de su acceso.

Si Plegable no hace nada por ti, entonces tal vez el enfoque correcto es definir la interfaz que deseas como una clase de tipo y descubrir una buena forma de autogenerar los valores derivados.

Tal vez mediante la derivación de hacer

deriving(Data) 

podría utilizar combinadores gmap basar su acceso fuera.

2

Me parece un trabajo para genéricos. Si se pudiera etiquetar su Int con diferentes Newtypes, entonces sería capaz de escribir (con Uniplate, PlateData módulo):

data Foo = Foo Something Another deriving (Data,Typeable) 
data Bar = Bar Another Thing deriving (Data, Typerable) 

data Opts = F Foo | B Bar 

newtype Something = S Int 
newtype Another = A Int 
newtype Thing = T Int 

getAnothers opts = [ x | A x <- universeBi opts ] 

Esto extraer todos de otro desde cualquier lugar dentro de los TPO.

También es posible la modificación.

Cuestiones relacionadas