2011-07-13 7 views
9

Tengo dos registros que tienen un campo que quiero extraer para su visualización. ¿Cómo organizo las cosas para que puedan ser manipuladas con las mismas funciones? Como tienen campos diferentes (en este caso firstName y buildingName) que son sus campos de nombre, cada uno de ellos necesita un código de "adaptador" para asignar firstName a name. Aquí es lo que tengo hasta ahora:¿Cómo se define una clase que permite el acceso uniforme a diferentes registros en Haskell?

class Nameable a where 
    name :: a -> String 

data Human = Human { 
    firstName :: String 
} 

data Building = Building { 
    buildingName :: String 
} 

instance Nameable Human where 
    name x = firstName x 

instance Nameable Building where 
    -- I think the x is redundant here, i.e the following should work: 
    -- name = buildingName 
    name x = buildingName x 

main :: IO() 
main = do 
    putStr $ show (map name items) 
    where 
    items :: (Nameable a) => [a] 
    items = [ Human{firstName = "Don"} 
      -- Ideally I want the next line in the array too, but that gives an 
      -- obvious type error at the moment. 
      --, Building{buildingName = "Empire State"} 
      ] 

Esto no se compila:

TypeTest.hs:23:14: 
    Couldn't match expected type `a' against inferred type `Human' 
     `a' is a rigid type variable bound by 
      the type signature for `items' at TypeTest.hs:22:23 
    In the expression: Human {firstName = "Don"} 
    In the expression: [Human {firstName = "Don"}] 
    In the definition of `items': items = [Human {firstName = "Don"}] 

lo que habría esperado la sección instance Nameable Human sería hacer este trabajo. ¿Alguien puede explicar lo que estoy haciendo mal y, para los puntos de bonificación, qué "concepto" estoy tratando de poner en práctica, ya que tengo problemas para saber qué buscar?

This question parece similar, pero no pude averiguar la conexión con mi problema.

+0

Si elimina la anotación de tipo para 'elementos', entonces funciona ... Sin embargo, los elementos tienen el tipo' [Humano] '(¿qué tal vez no es lo que quiere?) – phynfo

Respuesta

13

considerar el tipo de items:

items :: (Nameable a) => [a] 

Es decir que para cualquier tipo Nameable, items me dará una lista de ese tipo. Lo hace no decir que items es una lista que puede contener diferentes tipos de Nameable, como se podría pensar. Desea algo como items :: [exists a. Nameable a => a], excepto que tendrá que introducir un tipo de contenedor y usar forall. (Ver: Existential type)

{-# LANGUAGE ExistentialQuantification #-} 

data SomeNameable = forall a. Nameable a => SomeNameable a 

[...] 

items :: [SomeNameable] 
items = [ SomeNameable $ Human {firstName = "Don"}, 
      SomeNameable $ Building {buildingName = "Empire State"} ] 

El cuantificador en el constructor de datos de SomeNameable básicamente permite que se olvide todo acerca de exactamente qué se utiliza a, excepto que es Nameable. Por lo tanto, solo se le permitirá usar funciones de la clase Nameable en los elementos.

Para que esto sea más agradable de usar, puede hacer que una instancia de la envoltura:

instance Nameable (SomeNameable a) where 
    name (SomeNameable x) = name x 

ya se puede utilizar de esta manera:

Main> map name items 
["Don", "Empire State"] 
+2

Ojalá GHC no tuviera cuantificación existencial . Confunde más de lo que ayuda. Pregunta: ¿cuál es la diferencia entre 'SomeNameable' y' String'? – luqui

+2

@luqui: en este caso, dado que 'name' es la única operación disponible, ¡también puede aplicarlo de inmediato! Solo vas a obtener 'String's de una lista de' SomeNameable'. Introduzca modificadores combinados como 'changeName :: String -> a -> a' en 'Nameable' y ¡las cosas se ponen interesantes! – yatima2975

+0

Gracias, eso responde mi pregunta. Ahora necesito averiguar si es apropiado para mi problema real o puedo salirme con un tipo de unión. –

3

se puede tratar de utilizar tipos Existencialmente cuantificó y hacerlo de esta manera:

data T = forall a. Nameable a => MkT a 
items = [MkT (Human "bla"), MkT (Building "bla")] 
6

me gusta la respuesta de @ Hammar, y también se debe comprobar fuera this article que proporciona otro ejemplo.

Pero, es posible que desee pensar de manera diferente sobre sus tipos. El boxeo de Nameable en el tipo de datos SomeNameable generalmente me hace comenzar a pensar si un tipo de unión para el caso específico es significativo.

data Entity = H Human | B Building 
instance Nameable Entity where ... 

items = [H (Human "Don"), B (Building "Town Hall")] 
+2

Podría simplemente usar 'items = [" Don "," Town Hall "]', ya que contiene exactamente la misma información. – luqui

+0

@luqui Esto le permite coincidir con el patrón en el constructor 'Entity', por lo que lleva más información que solo un String. 'Human' y' Building' son probablemente innecesarios. – Alex

+0

Oh, whoops. Enseguida vi los signos de un ser existencial y no leí en detalle. – luqui

5

no estoy seguro de por qué desea utilizar la misma función para obtener el nombre de un Human y el nombre de un Building.

Si se utilizan sus nombres en formas fundamentalmente diferentes, excepto quizás por cosas tan simples como imprimirlos, entonces es probable que quieren dos funciones diferentes para eso. El sistema de tipo lo guiará automáticamente para elegir la función correcta para usar en cada situación.

Pero si tener un nombre es algo significativo acerca de la todo propósito de su programa, y ​​una Human y una Building son realmente más o menos la misma cosa al respecto por lo que su programa se ocupa de , entonces lo haría definir su tipo de juntas:

data NameableThing = 
    Human { name :: String } | 
    Building { name :: String } 

eso le da una función polimórfica name que funcione para lo determinado sabor de NameableThing le sucede que tiene, sin necesidad de entrar en las clases de tipos.

Por lo general, se usaría una clase de tipo para un tipo diferente de situación: si tiene algún tipo de operación no trivial que tiene el mismo propósito sino una aplicación diferente para varios tipos diferentes. Incluso entonces, a menudo es mejor utilizar algún otro método, como pasando una función como parámetro (una "función de orden superior" u "HOF").

Las clases de tipo Haskell son una herramienta hermosa y poderosa, pero son totalmente diferentes a las denominadas "clases" en los lenguajes orientados a objetos, y se utilizan con mucha menos frecuencia.

Y ciertamente no recomiendo complicar su programa mediante el uso de una extensión avanzada de a Haskell como calificación existencial para encajar en un patrón de diseño orientado a objetos.

+0

Lo siento, mi ejemplo es artificial. Si tienes curiosidad, aquí está mi programa completo: https://github.com/xaviershay/xtdo-hs/blob/master/Xtdo.hs Necesito poder tratar 'Task' y' RecurringTaskDefinition' de manera similar, tenga en cuenta que 'deleteTaskByName' y' deleteRecurringByName' son prácticamente la misma función. –

8

Todo el mundo está buscando cuantificación existencial o tipos de datos algebraicos. Pero estos son excesivos (dependiendo de sus necesidades, los ADT pueden no serlo).

Lo primero a tener en cuenta es que Haskell no tiene downcasting. Es decir, si se utiliza la siguiente existencial:

data SomeNameable = forall a. Nameable a => SomeNameable a 

entonces cuando se crea un objeto

foo :: SomeNameable 
foo = SomeNameable $ Human { firstName = "John" } 

la información sobre el tipo concreto el objeto se hizo con (aquí Human) está perdido para siempre. Las únicas cosas que sabemos son: es un tipo a, y hay una instancia Nameable a.

¿Qué se puede hacer con un par de este tipo? Bueno, puedes obtener el name del a que tienes, y ... eso es todo. Eso es todo al respecto. De hecho, hay un isomorfismo. Haré un nuevo tipo de datos para que pueda ver cómo surge este isomorfismo en los casos en que todos sus objetos concretos tienen más estructura que la clase.

data ProtoNameable = ProtoNameable { 
    -- one field for each typeclass method 
    protoName :: String 
} 

instance Nameable ProtoNameable where 
    name = protoName 

toProto :: SomeNameable -> ProtoNameable 
toProto (SomeNameable x) = ProtoNameable { protoName = name x } 

fromProto :: ProtoNameable -> SomeNameable 
fromProto = SomeNameable 

Como podemos ver, esta suposición existencial tipo SomeNameable tiene la misma estructura y la información como ProtoNameable, que es isomorfo a String, así que cuando usted está usando este concepto elevado SomeNameable, en realidad está diciendo en String una forma enrevesada Entonces, ¿por qué no simplemente decir String?

Su definición items tiene exactamente la misma información que esta definición:

items = [ "Don", "Empire State" ] 

debería añadir algunas notas acerca de esta "protoization": sólo es tan sencillo como esto cuando la clase de tipos que está existencialmente cuantificando sobre tiene una cierta estructura: es decir, cuando parece una clase OO.

class Foo a where 
    method1 :: ... -> a -> ... 
    method2 :: ... -> a -> ... 
    ... 

Es decir, cada método sólo se utiliza una vez a como argumento. Si usted tiene algo así como Num

class Num a where 
    (+) :: a -> a -> a 
    ... 

que utiliza a en múltiples posiciones de argumentos, o como resultado de ello, a continuación, eliminando la existencial no es tan fácil, but still possible. Sin embargo, mi recomendación para hacer esto cambia de una frustración a una elección sutil dependiente del contexto, debido a la complejidad y la relación distante de las dos representaciones. Sin embargo, cada vez que he visto existenciales usados ​​en la práctica es con el tipo de tyepclass Foo, donde solo agrega complejidad innecesaria, por lo que enfáticamente soy consider it an antipattern. En la mayoría de estos casos, recomiendo eliminar toda la clase de su base de código y usar exclusivamente el tipo de protografía (después de darle un buen nombre).

Además, si do necesita abatir, entonces los existenciales no son su hombre. Puede utilizar un tipo de datos algebraico, como otras personas han respondido, o puede usar Data.Dynamic (que es básicamente un elemento existencial sobre Typeable. Pero no lo haga, un programador de Haskell recurriendo a Dynamic no es caballeroso. Un ADT es el camino ir, donde caracterices todos los tipos posibles podría estar en un solo lugar (que es necesario para que las funciones que hacen el "downcasting" sepan que manejan todos los casos posibles).

0

Acabo de echar un vistazo . the code en que esta cuestión está haciendo abstracción de Para esto, yo recomendaría la fusión de las Task y RecurringTaskDefinition tipos:

data Task 
    = Once 
     { name :: String 
     , scheduled :: Maybe Day 
     , category :: TaskCategory 
     } 
    | Recurring 
     { name :: String 
     , nextOccurrence :: Day 
     , frequency :: RecurFrequency 
     } 
type ProgramData = [Task] -- don't even need a new data type for this any more 

Entonces, la función name funciona bien en cualquier tipo, y las funciones que usted se quejaba como deleteTask y deleteRecurring ni siquiera necesitan existir; puede usar la función estándar delete como de costumbre.

+0

Gracias por la sugerencia. Por el momento me he salido con la suya usando una función de orden superior, pero creo que voy a intentarlo también para ver cómo se cae. Gracias. –

Cuestiones relacionadas