2010-07-09 12 views
46

Tengo una clase de tipo MyClass, y hay una función en ella que produce un String. Quiero utilizar esto para dar a entender una instancia de Show, de modo que pueda pasar los tipos que implementan MyClass a . Hasta ahora tengo,Cómo escribo, "si typeclass a, entonces a también es una instancia de b según esta definición".

class MyClass a where 
    someFunc :: a -> a 
    myShow :: a -> String 

instance MyClass a => Show a where 
    show a = myShow a 

que da el error Constraint is no smaller than the instance head. También probé,

class MyClass a where 
    someFunc :: a -> a 
    myShow :: a -> String 

instance Show (MyClass a) where 
    show a = myShow a 

que da el error, utilizan Class MiClase como type`.

¿Cómo puedo expresar correctamente esta relación en Haskell? Gracias.

Debo añadir que deseo hacer un seguimiento de esto con instancias específicas de MyClass que emiten cadenas específicas según su tipo. Por ejemplo,

data Foo = Foo 
data Bar = Bar 

instance MyClass Foo where 
    myShow a = "foo" 

instance MyClass Bar where 
    myShow a = "bar" 

main = do 
    print Foo 
    print Bar 
+1

FYI He respondido esta pregunta porque es detallada pero concisa, clara y bien formada. Una pregunta modelo, y la recitaría nuevamente si fuera posible. –

Respuesta

28

(Edit: abandonar el cuerpo para la posteridad, pero saltar al final de la solución real)

En la declaración instance MyClass a => Show a, vamos a examinar el error "Restricción no es menor que la cabeza instancia." La restricción es la restricción de clase de tipo a la izquierda de '=>', en este caso MyClass a. El "encabezado de instancia" es todo después de la clase para la que está escribiendo una instancia, en este caso a (a la derecha de Show). Una de las reglas de inferencia de tipo en GHC requiere que la restricción tenga menos constructores y variables que el encabezado. Esto es parte de lo que se llama 'Paterson Conditions'. Estos existen como una garantía de que la verificación de tipos termina.

En este caso, la restricción es exactamente la misma que la del encabezado, es decir, a, por lo que no pasa esta prueba. Puede eliminar las verificaciones de condición de Paterson habilitando UndecidableInstances, muy probablemente con el pragma {-# LANGUAGE UndecidableInstances #-}.

En este caso, esencialmente está utilizando su clase MyClass como sinónimo de clase de clase para la clase Show. Crear sinónimos de clase como este es uno de los usos canónicos de la extensión UndecidableInstances, por lo que puede usarlo de forma segura aquí.

'Indecidible' significa que GHC no puede probar que la verificación de tipo terminará. Aunque parezca peligroso, lo peor que puede pasar al habilitar las UndecidableInstances es que el compilador realizará un bucle, que finalmente terminará después de agotar la pila. Si se compila, entonces obviamente la verificación de tipo terminó, por lo que no hay problemas. La extensión peligrosa es IncoherentInstances, que es tan mala como suena.

Editar: otro problema hecho posible por este enfoque surge de esta situación:

instance MyClass a => Show a where 

data MyFoo = MyFoo ... deriving (Show) 

instance MyClass MyFoo where 

Ahora bien, hay dos instancias de mostrar para MyFoo, el de la cláusula de derivar y el uno para MyClass casos. El compilador no puede decidir cuál usar, por lo que rescatará con un mensaje de error. Si intenta realizar instancias MyClass de tipos que no controla que ya tienen instancias Show, deberá usar newtypes para ocultar las instancias Show existentes. Incluso los tipos sin MyClass casos se sigue el conflicto, porque la definición instance MyClass => Show a porque la definición de hecho proporciona una implementación para todos los posibles a (el cheque contexto viene en adelante; no es involucrado con la selección ejemplo)

Así que ese es el mensaje de error y cómo UndecidableInstances lo hace desaparecer Desafortunadamente, es un gran problema usarlo en el código real, por razones que Edward Kmett explica. El ímpetu original fue evitar especificar una restricción Show cuando ya existe una restricción MyClass. Dado que, lo que haría es simplemente usar myShow desde MyClass en lugar de show. No necesitará la restricción Show en absoluto.

+0

Gracias, de hecho funciona si activo UndecidableInstances. Extraño. ¿Es esto un abuso del sistema de clases de tipos? Intenté principalmente evitar tener que especificar la restricción "Mostrar" para todas las funciones que toman "MyClass" y también deben mostrarse. Dado que todas las instancias de MyClass deberían tener una cadena asociada a ellas, calculé que la obtención automática de Show sería de una sola manera. Sin embargo, parece mejor simplemente evitar el problema especificando explícitamente Mostrar cuando sea necesario, en lugar de habilitar extensiones de lenguaje esotérico. ¿Qué piensas? – Steve

+3

Usaría myShow en lugar de mostrar en todas las funciones, por lo que no es necesario mostrar la restricción en absoluto. En las definiciones de instancia, puede escribir 'myShow = show' si es apropiado. Evitaría escribir 'instancia MyClass a => Mostrar a' por el problema descrito en la edición. La solución de Dave Hinton es mejor que las IndecisasInstancias, pero no creo que deba agregar una restricción de superclase solo por conveniencia. –

+2

Tenga en cuenta que este truco con estas extensiones solo funciona de forma razonablemente segura dentro de un único módulo. Ni siquiera puede usar las exportaciones desde este módulo, incluso si no exporta MyClass, en cualquier otro módulo que alguna vez tenga un módulo que dependa transitoriamente de él que alguna vez quiera mostrar algo que no sea una instancia de MyClass. –

2

Se puede compilar, pero no con Haskell 98, tiene que habilitar algunas extensiones de lenguaje:

{-# LANGUAGE FlexibleInstances #-} 
{-# LANGUAGE UndecidableInstances #-} 
-- at the top of your file 

casos flexibles está ahí para permitir contexto en el declaración de la instancia. Realmente no sé el significado de IndecidiblesInstancias, pero evitaría todo lo que pueda.

+0

Estaba asumiendo que el hecho de que GHC piense que es indecidible probablemente sea un problema. Si alguien pudiera explicar por qué es indecidible, ¡sería genial! – Steve

+3

Si habilita las UndecidableInstances, FlexibleInstances es superfluo. –

5

creo que sería mejor hacerlo al revés:

class Show a => MyClass a where 
    someFunc :: a -> a 

myShow :: MyClass a => a -> String 
myShow = show 
+0

... excepto que eso no permitirá que el OP declare una versión de 'show' personalizada. Además, no permitirá una declaración de 'MyClass' para un tipo que no sea (todavía) una instancia de Show. – BMeph

+4

Lo que hay que tener en cuenta aquí es que cuando dices show = myShow, ya has perdido la batalla de tener un show personalizado ya que un tipo no puede tener dos funciones de show diferentes, y un tipo que implemente MyClass debe ser una instancia de Mostrar (llamarlo myShow instaid of show no hace diferencia). Es decir, si quiero que T sea una instancia de MyClass, tengo que implementar myShow, que al final resulta ser su función de show como una instancia de Show. Compare eso con la primera implementación de Show y la implementación de MyClass, y verá que la única diferencia es el nombre de la función. – HaskellElephant

+1

Me gusta esta solución porque la forma en que el OP presenta su problema, 'myShow' es equivalente a' show' para todas las instancias 'MyClass' porque' MyClass' implica 'Show' (por el deseo de OP). Sin embargo, no necesitarías la función 'myShow' en este caso. Solo usa 'show'. –

53

deseo de estar en desacuerdo con fuerza con las soluciones rotos planteaba hasta ahora.

instance MyClass a => Show a where 
    show a = myShow a 

Debido a la forma en que funciona la resolución de la instancia, esta es una instancia muy peligrosa para correr!

La resolución de la instancia se realiza por coincidencia de patrones efectiva en el lado derecho de cada instancia de =>, sin tener en cuenta lo que está a la izquierda del =>.

Cuando ninguna de esas instancias se superpone, esto es algo hermoso. Sin embargo, lo que está diciendo aquí es "Aquí hay una regla que debe usar para CADA Mostrar instancia. Cuando se le pida una instancia de show para cualquier tipo, necesitará una instancia de MyClass, así que consiga eso, y aquí está la implementación." - una vez que el compilador se ha comprometido a elegir usar su instancia, (solo en virtud del hecho de que 'a' se unifica con todo) ¡no tiene ninguna posibilidad de retroceder y usar cualquier otra instancia!

Si activa {-# LANGUAGE OverlappingInstances, IncoherentInstances #-}, etc. para compilarlo, obtendrá fallas no tan sutiles cuando vaya a escribir módulos que importen el módulo que proporciona esta definición y que necesitan utilizar cualquier otra instancia de Show. En última instancia, podrás obtener este código para compilar con suficientes extensiones, ¡pero lamentablemente no hará lo que crees que debería hacer!

Si se piensa en ello da:

instance MyClass a => Show a where 
    show = myShow 

instance HisClass a => Show a where 
    show = hisShow 

la que debe recoger el compilador?

Su módulo solo puede definir uno de estos, pero el código de usuario final importará un grupo de módulos, no solo el suyo.Además, si otro módulo define

instance Show HisDataTypeThatHasNeverHeardOfMyClass 

el compilador estaría bien dentro de sus derechos a ignorar a su instancia y tratar de usar el suyo.

La respuesta correcta, por desgracia, es hacer dos cosas.

Para cada individuo instancia de MyClass puede definir una instancia correspondiente del Show con la definición muy mecánico

instance MyClass Foo where ... 

instance Show Foo where 
    show = myShow 

Esto es bastante desafortunado, pero funciona bien cuando hay sólo unos pocos casos de MiClase menores consideración.

Cuando tiene un gran número de instancias, la forma de evitar la duplicación de código (para cuando la clase es considerablemente más complicada que mostrar) es definir.

newtype WrappedMyClass a = WrapMyClass { unwrapMyClass :: a } 

instance MyClass a => Show (WrappedMyClass a) where 
    show (WrapMyClass a) = myShow a 

Esto proporciona el newtype como vehículo para despacho de instancia. y luego

instance Foo a => Show (WrappedFoo a) where ... 
instance Bar a => Show (WrappedBar a) where ... 

es inequívoca, debido a que los 'patrones' de tipo para WrappedFoo a y WrappedBar a son disjuntos.

Hay una serie de ejemplos de este modismo en el paquete base.

En Control.Applicative hay definiciones para WrappedMonad y WrappedArrow por esta misma razón.

Lo ideal sería que usted sería capaz de decir:

instance Monad t => Applicative t where 
    pure = return 
    (<*>) = ap 

pero con eficacia este caso lo que está diciendo es que cada Aplicativo debe ser derivado al encontrar primero una instancia para la Mónada, y luego despachar a ella. Entonces, si bien tendría la intención de decir que cada mónada es aplicable (por cierto, como dice la implicación, como =>), lo que dice es que cada aplicante es una mónada, porque tener un encabezado de instancia 't' coincide con cualquier tipo. En muchos sentidos, la sintaxis para las definiciones de 'instancia' y 'clase' es al revés.

+1

¡Ja, me puse tan al corriente de la explicación del mensaje de error y lo que hace UndecidableInstances que olvidé por completo poner en mi solución real (excepto en un comentario muy pequeño)! He editado mi respuesta para incluirlo. –

+3

¡Gracias! Como estoy seguro de que puede ver en mi pregunta, todavía estoy aprendiendo exactamente qué tipo de clases "son", y esto ayuda muchísimo. Tanto como me desagradan los "patrones de diseño", sería genial ver un libro que detalla las soluciones de libros de cocina para situaciones comunes en los programas de Haskell, incluyendo el "modismo" anterior como lo llamas. – Steve

+0

Por cierto, ¿es seguro decir que "una clase de tipos no puede (automáticamente) derivar (inferir) otra clase de tipos, solo puede requerir (restringirse por) una?" – Steve

1

Como Ed Kmett señaló, esto no es posible en absoluto para su caso. Sin embargo, si usted tiene acceso a la clase que desea proporcionar una instancia predeterminada, usted puede reducir el repetitivo al mínimo con una implementación por defecto y restringir el tipo de entrada con la firma predeterminada que necesita:

{-# LANGUAGE DefaultSignatures #-} 

class MyClass a where 
    someFunc :: a -> Int 

class MyShow a where 
    myShow :: a -> String 
    default myShow :: MyClass a => a -> String 
    myShow = show . someFunc 

instance MyClass Int where 
    someFunc i = i 

instance MyShow Int 

main = putStrLn (myShow 5) 

Tenga en cuenta que la única repetición real (bueno, aparte de todo el ejemplo) reducida a instance MyShow Int.

Consulte aeson s ToJSON para obtener un ejemplo más realista.

Cuestiones relacionadas